| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- import { spawn } from "child_process";
- import path from "path";
- import {
- createMessageConnection,
- Disposable,
- StreamMessageReader,
- StreamMessageWriter,
- } from "vscode-jsonrpc/node";
- import { App } from "../app";
- import { Log } from "../util/log";
- import { LANGUAGE_EXTENSIONS } from "./language";
- import { Bus } from "../bus";
- import z from "zod/v4";
- export namespace LSPClient {
- const log = Log.create({ service: "lsp.client" });
- export type Info = Awaited<ReturnType<typeof create>>;
- export const Event = {
- Diagnostics: Bus.event(
- "lsp.client.diagnostics",
- z.object({
- path: z.string(),
- }),
- ),
- };
- export async function create(input: { cmd: string[] }) {
- log.info("starting client", input);
- let version = 0;
- const app = await App.use();
- const [command, ...args] = input.cmd;
- const server = spawn(command, args, {
- stdio: ["pipe", "pipe", "pipe"],
- cwd: app.root,
- });
- const connection = createMessageConnection(
- new StreamMessageReader(server.stdout),
- new StreamMessageWriter(server.stdin),
- );
- const diagnostics = new Map<string, any>();
- connection.onNotification("textDocument/publishDiagnostics", (params) => {
- const path = new URL(params.uri).pathname;
- log.info("textDocument/publishDiagnostics", {
- path,
- });
- diagnostics.set(path, params.diagnostics);
- Bus.publish(Event.Diagnostics, { path });
- });
- connection.listen();
- await connection.sendRequest("initialize", {
- processId: server.pid,
- initializationOptions: {
- workspaceFolders: [
- {
- name: "workspace",
- uri: "file://" + app.root,
- },
- ],
- tsserver: {
- path: require.resolve("typescript/lib/tsserver.js"),
- },
- },
- capabilities: {
- workspace: {
- configuration: true,
- didChangeConfiguration: {
- dynamicRegistration: true,
- },
- didChangeWatchedFiles: {
- dynamicRegistration: true,
- relativePatternSupport: true,
- },
- },
- textDocument: {
- synchronization: {
- dynamicRegistration: true,
- didSave: true,
- },
- completion: {
- completionItem: {},
- },
- codeLens: {
- dynamicRegistration: true,
- },
- documentSymbol: {},
- codeAction: {
- codeActionLiteralSupport: {
- codeActionKind: {
- valueSet: [],
- },
- },
- },
- publishDiagnostics: {
- versionSupport: true,
- },
- semanticTokens: {
- requests: {
- range: {},
- full: {},
- },
- tokenTypes: [],
- tokenModifiers: [],
- formats: [],
- },
- },
- window: {},
- },
- });
- await connection.sendNotification("initialized", {});
- log.info("initialized");
- const result = {
- get connection() {
- return connection;
- },
- notify: {
- async open(input: { path: string }) {
- log.info("textDocument/didOpen", input);
- diagnostics.delete(input.path);
- const text = await Bun.file(input.path).text();
- const languageId = LANGUAGE_EXTENSIONS[path.extname(input.path)];
- await connection.sendNotification("textDocument/didOpen", {
- textDocument: {
- uri: `file://` + input.path,
- languageId,
- version: 1,
- text: text,
- },
- });
- },
- async change(input: { path: string }) {
- log.info("textDocument/didChange", input);
- diagnostics.delete(input.path);
- const text = await Bun.file(input.path).text();
- version++;
- await connection.sendNotification("textDocument/didChange", {
- textDocument: {
- uri: `file://` + input.path,
- version: Date.now(),
- },
- contentChanges: [
- {
- text,
- },
- ],
- });
- },
- },
- get diagnostics() {
- return diagnostics;
- },
- async refreshDiagnostics(input: { path: string }) {
- log.info("refreshing diagnostics", input);
- let unsub: () => void;
- let timeout: NodeJS.Timeout;
- return await Promise.race([
- new Promise<void>(async (resolve) => {
- unsub = Bus.subscribe(Event.Diagnostics, (event) => {
- if (event.properties.path === input.path) {
- log.info("refreshed diagnostics", input);
- clearTimeout(timeout);
- unsub?.();
- resolve();
- }
- });
- await result.notify.change(input);
- }),
- new Promise<void>((resolve) => {
- timeout = setTimeout(() => {
- unsub?.();
- resolve();
- }, 5000);
- }),
- ]);
- },
- };
- return result;
- }
- }
|