feat: add DSH IDE open-file references

This commit is contained in:
2026-08-17 11:52:48 +08:00
commit d96c11ae3d
42 changed files with 6991 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import { basename } from 'node:path'
import * as vscode from 'vscode'
import { createSnapshot, type OpenDocument } from './model.js'
import { SnapshotPublisher } from './publisher.js'
const HEARTBEAT_MS = 15_000
const DEBOUNCE_MS = 150
function localFile(uri: vscode.Uri): string | undefined {
return uri.scheme === 'file' && uri.fsPath ? uri.fsPath : undefined
}
function tabDocuments(tab: vscode.Tab): OpenDocument[] {
const input = tab.input
const paths: string[] = []
if (input instanceof vscode.TabInputText) {
const path = localFile(input.uri)
if (path) paths.push(path)
} else if (input instanceof vscode.TabInputTextDiff) {
const original = localFile(input.original)
const modified = localFile(input.modified)
if (original) paths.push(original)
if (modified) paths.push(modified)
}
return paths.map((path) => ({ path, label: basename(path), dirty: tab.isDirty }))
}
export function collectOpenDocuments(): OpenDocument[] {
return vscode.window.tabGroups.all.flatMap((group) => group.tabs.flatMap(tabDocuments))
}
function workspaceLabel(): string | undefined {
const workspaceFile = vscode.workspace.workspaceFile
if (workspaceFile?.scheme === 'file') return workspaceFile.fsPath
const folders = vscode.workspace.workspaceFolders
if (folders?.length === 1 && folders[0]?.uri.scheme === 'file') return folders[0].uri.fsPath
return vscode.workspace.name
}
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const output = vscode.window.createOutputChannel('DSH Open File Bridge', { log: true })
const publisher = new SnapshotPublisher()
let debounce: NodeJS.Timeout | undefined
let disposed = false
const publish = async () => {
if (disposed) return
const snapshot = createSnapshot({
instanceId: publisher.instanceId,
processId: process.pid,
workspace: workspaceLabel(),
documents: collectOpenDocuments(),
})
try {
await publisher.publish(snapshot)
output.debug(`Published ${snapshot.documents.length} open files`)
} catch (error) {
output.error(`Snapshot publish failed: ${error instanceof Error ? error.message : String(error)}`)
}
}
const schedule = () => {
if (debounce) clearTimeout(debounce)
debounce = setTimeout(() => void publish(), DEBOUNCE_MS)
}
context.subscriptions.push(
output,
vscode.window.tabGroups.onDidChangeTabs(schedule),
vscode.window.onDidChangeActiveTextEditor(schedule),
vscode.workspace.onDidChangeWorkspaceFolders(schedule),
)
const heartbeat = setInterval(() => void publish(), HEARTBEAT_MS)
const onExit = () => publisher.disposeSync()
process.once('exit', onExit)
context.subscriptions.push({
dispose() {
disposed = true
if (debounce) clearTimeout(debounce)
clearInterval(heartbeat)
process.off('exit', onExit)
void publisher.dispose()
},
})
await publish()
}
export function deactivate(): void {
// ExtensionContext disposal owns cleanup.
}
+46
View File
@@ -0,0 +1,46 @@
export type IdeKind = 'vscode'
export interface OpenDocument {
path: string
label: string
dirty: boolean
}
export interface OpenFileSnapshot {
version: 1
instanceId: string
ide: IdeKind
processId: number
workspace?: string
updatedAt: string
documents: OpenDocument[]
}
export function deduplicateDocuments(documents: readonly OpenDocument[]): OpenDocument[] {
const result = new Map<string, OpenDocument>()
for (const document of documents) {
const key = document.path.toLocaleLowerCase('en-US')
const current = result.get(key)
if (!current || (!current.dirty && document.dirty)) result.set(key, document)
}
return [...result.values()].sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })
|| a.path.localeCompare(b.path, undefined, { sensitivity: 'base' }))
}
export function createSnapshot(input: {
instanceId: string
processId: number
workspace?: string
documents: readonly OpenDocument[]
now?: Date
}): OpenFileSnapshot {
return {
version: 1,
instanceId: input.instanceId,
ide: 'vscode',
processId: input.processId,
...(input.workspace === undefined ? {} : { workspace: input.workspace }),
updatedAt: (input.now ?? new Date()).toISOString(),
documents: deduplicateDocuments(input.documents),
}
}
+47
View File
@@ -0,0 +1,47 @@
import { randomUUID } from 'node:crypto'
import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
import { rmSync } from 'node:fs'
import { join } from 'node:path'
import type { OpenFileSnapshot } from './model.js'
export function snapshotRoot(localAppData = process.env.LOCALAPPDATA): string {
if (!localAppData) throw new Error('LOCALAPPDATA is not available')
return join(localAppData, 'Prophet', 'dsh-plugin-coop-with-vs', 'v1', 'instances')
}
export class SnapshotPublisher {
readonly instanceId = randomUUID()
readonly filePath: string
private pending = Promise.resolve()
constructor(private readonly directory = snapshotRoot()) {
this.filePath = join(directory, `vscode-${this.instanceId}.json`)
}
publish(snapshot: OpenFileSnapshot): Promise<void> {
this.pending = this.pending.then(async () => {
await mkdir(this.directory, { recursive: true })
const temporary = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`
await writeFile(temporary, `${JSON.stringify(snapshot)}\n`, { encoding: 'utf8', mode: 0o600 })
try {
await rename(temporary, this.filePath)
} catch (error) {
await rm(this.filePath, { force: true })
await rename(temporary, this.filePath).catch(async (renameError) => {
await rm(temporary, { force: true })
throw renameError
})
}
})
return this.pending
}
async dispose(): Promise<void> {
await this.pending.catch(() => undefined)
await rm(this.filePath, { force: true }).catch(() => undefined)
}
disposeSync(): void {
try { rmSync(this.filePath, { force: true }) } catch { /* best effort during process exit */ }
}
}