index.ts
1 import { spawnSync } from "node:child_process"; 2 import { existsSync } from "node:fs"; 3 import { 4 type ExtensionContext, 5 ExtensionMode, 6 type Uri, 7 window, 8 workspace, 9 } from "vscode"; 10 import { 11 LanguageClient, 12 type LanguageClientOptions, 13 type ServerOptions, 14 } from "vscode-languageclient/node"; 15 16 let client: LanguageClient; 17 18 const ID = "Duper"; 19 const NAME = "Duper"; 20 const CONFIG_KEY = "duper"; 21 const DUPER_BLOB = "**/*.duper"; 22 23 export const outputChannel = window.createOutputChannel(NAME); 24 25 function getBinaryFromPath(name: string): string | null { 26 const result = spawnSync(process.platform === "win32" ? "where" : "which", [ 27 name, 28 ]); 29 if (result.status === 0) { 30 const path = result.stdout.toString().trim().split("\n")[0].trim(); 31 return path || null; 32 } else { 33 return null; 34 } 35 } 36 37 function getServerOptions(context: ExtensionContext): ServerOptions | null { 38 const extension: string = process.platform === "win32" ? ".exe" : ""; 39 if (context.extensionMode === ExtensionMode.Development) { 40 return { 41 command: context.asAbsolutePath(`../target/debug/duper_lsp${extension}`), 42 args: ["--debug"], 43 }; 44 } 45 46 const config = workspace.getConfiguration(CONFIG_KEY); 47 const lspArgs = config.get("lsp.args") as string[]; 48 const lspBin = config.get("lsp.bin") as string; 49 if (lspBin) { 50 return { 51 command: lspBin, 52 args: lspArgs, 53 }; 54 } 55 56 const lspSystemBinary = config.get("lsp.systemBinary") as boolean; 57 if (lspSystemBinary) { 58 // Try to find LSP in PATH 59 const binaryPath = getBinaryFromPath(`duper_lsp${extension}`); 60 if (binaryPath) { 61 return { 62 command: binaryPath, 63 args: lspArgs, 64 }; 65 } 66 outputChannel.appendLine( 67 `[client] \`duper_lsp${extension}\` not found in \`PATH\`.`, 68 ); 69 outputChannel.appendLine( 70 "[client] = hint: Consider removing the `duper.lsp.systemBinary` configuration to use the bundled binary, or run `cargo install --locked duper_lsp` to build the Duper LSP instead.", 71 ); 72 return null; 73 } else { 74 // Try to get bundled LSP 75 const binary = context.asAbsolutePath(`duper_lsp${extension}`); 76 if (existsSync(binary)) { 77 return { 78 command: binary, 79 args: lspArgs, 80 }; 81 } 82 outputChannel.appendLine( 83 `[client] Binary not found for platform/architecture '${process.platform}/${process.arch}'; attempting to get binary from PATH instead.`, 84 ); 85 outputChannel.appendLine( 86 "[client] = note: If you're seeing this message, it likely means that the Duper extension was bundled incorrectly. " + 87 "Please open an issue at https://github.com/EpicEric/duper/issues", 88 ); 89 // Debug: Load from PATH instead 90 const binaryPath = getBinaryFromPath(`duper_lsp${extension}`); 91 if (binaryPath) { 92 return { 93 command: binaryPath, 94 args: lspArgs, 95 }; 96 } 97 outputChannel.appendLine("[client] Binary not found in PATH."); 98 } 99 100 return null; 101 } 102 103 async function openDocument(uri: Uri) { 104 const doc = workspace.textDocuments.find( 105 (d) => d.uri.toString() === uri.toString(), 106 ); 107 if (doc === undefined) await workspace.openTextDocument(uri); 108 return uri; 109 } 110 111 export async function activate(context: ExtensionContext) { 112 const serverOptions = getServerOptions(context); 113 if (serverOptions === null) { 114 const choice = await window.showErrorMessage( 115 "Unable to find Duper LSP binary.", 116 "Check logs", 117 ); 118 if (choice === "Check logs") { 119 outputChannel.show(); 120 } 121 return; 122 } 123 124 const deleteWatcher = workspace.createFileSystemWatcher( 125 DUPER_BLOB, 126 true, 127 true, 128 false, 129 ); 130 const createChangeWatcher = workspace.createFileSystemWatcher( 131 DUPER_BLOB, 132 false, 133 false, 134 true, 135 ); 136 137 context.subscriptions.push(deleteWatcher); 138 context.subscriptions.push(createChangeWatcher); 139 140 const clientOptions: LanguageClientOptions = { 141 documentSelector: [{ language: "duper", pattern: DUPER_BLOB }], 142 synchronize: { 143 fileEvents: deleteWatcher, 144 }, 145 diagnosticCollectionName: NAME, 146 }; 147 148 client = new LanguageClient(ID, NAME, serverOptions, clientOptions); 149 150 context.subscriptions.push(client.start()); 151 context.subscriptions.push(createChangeWatcher.onDidCreate(openDocument)); 152 context.subscriptions.push(createChangeWatcher.onDidChange(openDocument)); 153 154 const uris = await workspace.findFiles(DUPER_BLOB); 155 await Promise.all(uris.map(openDocument)); 156 } 157 158 export function deactivate(): Thenable<void> | undefined { 159 return client?.stop(); 160 }