92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
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.
|
||
|
|
}
|