/ duper-vs-code / src / client / index.ts
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  }