mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-01 06:42:26 +00:00
sync
This commit is contained in:
208
packages/opencode/src/lsp/client.ts
Normal file
208
packages/opencode/src/lsp/client.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { spawn } from "child_process";
|
||||
import path from "path";
|
||||
import {
|
||||
createMessageConnection,
|
||||
StreamMessageReader,
|
||||
StreamMessageWriter,
|
||||
} from "vscode-jsonrpc/node";
|
||||
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types";
|
||||
import { App } from "../app/app";
|
||||
import { Log } from "../util/log";
|
||||
import { LANGUAGE_EXTENSIONS } from "./language";
|
||||
import { Bus } from "../bus";
|
||||
import z from "zod";
|
||||
|
||||
export namespace LSPClient {
|
||||
const log = Log.create({ service: "lsp.client" });
|
||||
|
||||
export type Info = Awaited<ReturnType<typeof create>>;
|
||||
|
||||
export type Diagnostic = VSCodeDiagnostic;
|
||||
|
||||
export const Event = {
|
||||
Diagnostics: Bus.event(
|
||||
"lsp.client.diagnostics",
|
||||
z.object({
|
||||
serverID: z.string(),
|
||||
path: z.string(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
export async function create(input: { cmd: string[]; serverID: string }) {
|
||||
log.info("starting client", input);
|
||||
|
||||
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, Diagnostic[]>();
|
||||
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
||||
const path = new URL(params.uri).pathname;
|
||||
log.info("textDocument/publishDiagnostics", {
|
||||
path,
|
||||
});
|
||||
const exists = diagnostics.has(path);
|
||||
diagnostics.set(path, params.diagnostics);
|
||||
// servers seem to send one blank publishDiagnostics event before the first real one
|
||||
if (!exists && !params.diagnostics.length) return;
|
||||
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID });
|
||||
});
|
||||
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 files = new Set<string>();
|
||||
|
||||
const result = {
|
||||
get clientID() {
|
||||
return input.serverID;
|
||||
},
|
||||
get connection() {
|
||||
return connection;
|
||||
},
|
||||
notify: {
|
||||
async open(input: { path: string }) {
|
||||
const file = Bun.file(input.path);
|
||||
const text = await file.text();
|
||||
const opened = files.has(input.path);
|
||||
if (!opened) {
|
||||
log.info("textDocument/didOpen", input);
|
||||
diagnostics.delete(input.path);
|
||||
const extension = path.extname(input.path);
|
||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext";
|
||||
await connection.sendNotification("textDocument/didOpen", {
|
||||
textDocument: {
|
||||
uri: `file://` + input.path,
|
||||
languageId,
|
||||
version: Date.now(),
|
||||
text,
|
||||
},
|
||||
});
|
||||
files.add(input.path);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("textDocument/didChange", input);
|
||||
diagnostics.delete(input.path);
|
||||
await connection.sendNotification("textDocument/didChange", {
|
||||
textDocument: {
|
||||
uri: `file://` + input.path,
|
||||
version: Date.now(),
|
||||
},
|
||||
contentChanges: [
|
||||
{
|
||||
text,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
get diagnostics() {
|
||||
return diagnostics;
|
||||
},
|
||||
async waitForDiagnostics(input: { path: string }) {
|
||||
log.info("waiting for 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 &&
|
||||
event.properties.serverID === result.clientID
|
||||
) {
|
||||
log.info("got diagnostics", input);
|
||||
clearTimeout(timeout);
|
||||
unsub?.();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
new Promise<void>((resolve) => {
|
||||
timeout = setTimeout(() => {
|
||||
log.info("timed out refreshing diagnostics", input);
|
||||
unsub?.();
|
||||
resolve();
|
||||
}, 5000);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
async shutdown() {
|
||||
log.info("shutting down");
|
||||
connection.end();
|
||||
connection.dispose();
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
131
packages/opencode/src/lsp/index.ts
Normal file
131
packages/opencode/src/lsp/index.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { App } from "../app/app";
|
||||
import { Log } from "../util/log";
|
||||
import { LSPClient } from "./client";
|
||||
import path from "path";
|
||||
|
||||
export namespace LSP {
|
||||
const log = Log.create({ service: "lsp" });
|
||||
|
||||
const state = App.state(
|
||||
"lsp",
|
||||
async () => {
|
||||
log.info("initializing");
|
||||
const clients = new Map<string, LSPClient.Info>();
|
||||
|
||||
return {
|
||||
clients,
|
||||
};
|
||||
},
|
||||
async (state) => {
|
||||
for (const client of state.clients.values()) {
|
||||
await client.shutdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export async function file(input: string) {
|
||||
const extension = path.parse(input).ext;
|
||||
const s = await state();
|
||||
const matches = AUTO.filter((x) => x.extensions.includes(extension));
|
||||
for (const match of matches) {
|
||||
const existing = s.clients.get(match.id);
|
||||
if (existing) continue;
|
||||
const client = await LSPClient.create({
|
||||
cmd: match.command,
|
||||
serverID: match.id,
|
||||
});
|
||||
s.clients.set(match.id, client);
|
||||
}
|
||||
await run(async (client) => {
|
||||
const wait = client.waitForDiagnostics({ path: input });
|
||||
await client.notify.open({ path: input });
|
||||
return wait;
|
||||
});
|
||||
}
|
||||
|
||||
export async function diagnostics() {
|
||||
const results: Record<string, LSPClient.Diagnostic[]> = {};
|
||||
for (const result of await run(async (client) => client.diagnostics)) {
|
||||
for (const [path, diagnostics] of result.entries()) {
|
||||
const arr = results[path] || [];
|
||||
arr.push(...diagnostics);
|
||||
results[path] = arr;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function hover(input: {
|
||||
file: string;
|
||||
line: number;
|
||||
character: number;
|
||||
}) {
|
||||
return run((client) => {
|
||||
return client.connection.sendRequest("textDocument/hover", {
|
||||
textDocument: {
|
||||
uri: `file://${input.file}`,
|
||||
},
|
||||
position: {
|
||||
line: input.line,
|
||||
character: input.character,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function run<T>(
|
||||
input: (client: LSPClient.Info) => Promise<T>,
|
||||
): Promise<T[]> {
|
||||
const clients = await state().then((x) => [...x.clients.values()]);
|
||||
const tasks = clients.map((x) => input(x));
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
const AUTO: {
|
||||
id: string;
|
||||
command: string[];
|
||||
extensions: string[];
|
||||
install?: () => Promise<void>;
|
||||
}[] = [
|
||||
{
|
||||
id: "typescript",
|
||||
command: ["bun", "x", "typescript-language-server", "--stdio"],
|
||||
extensions: [
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".mts",
|
||||
".cts",
|
||||
".mtsx",
|
||||
".ctsx",
|
||||
],
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: "golang",
|
||||
command: ["gopls"],
|
||||
extensions: [".go"],
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
export namespace Diagnostic {
|
||||
export function pretty(diagnostic: LSPClient.Diagnostic) {
|
||||
const severityMap = {
|
||||
1: "ERROR",
|
||||
2: "WARN",
|
||||
3: "INFO",
|
||||
4: "HINT",
|
||||
};
|
||||
|
||||
const severity = severityMap[diagnostic.severity || 1];
|
||||
const line = diagnostic.range.start.line + 1;
|
||||
const col = diagnostic.range.start.character + 1;
|
||||
|
||||
return `${severity} [${line}:${col}] ${diagnostic.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
packages/opencode/src/lsp/language.ts
Normal file
89
packages/opencode/src/lsp/language.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
||||
".abap": "abap",
|
||||
".bat": "bat",
|
||||
".bib": "bibtex",
|
||||
".bibtex": "bibtex",
|
||||
".clj": "clojure",
|
||||
".coffee": "coffeescript",
|
||||
".c": "c",
|
||||
".cpp": "cpp",
|
||||
".cxx": "cpp",
|
||||
".cc": "cpp",
|
||||
".c++": "cpp",
|
||||
".cs": "csharp",
|
||||
".css": "css",
|
||||
".d": "d",
|
||||
".pas": "pascal",
|
||||
".pascal": "pascal",
|
||||
".diff": "diff",
|
||||
".patch": "diff",
|
||||
".dart": "dart",
|
||||
".dockerfile": "dockerfile",
|
||||
".ex": "elixir",
|
||||
".exs": "elixir",
|
||||
".erl": "erlang",
|
||||
".hrl": "erlang",
|
||||
".fs": "fsharp",
|
||||
".fsi": "fsharp",
|
||||
".fsx": "fsharp",
|
||||
".fsscript": "fsharp",
|
||||
".gitcommit": "git-commit",
|
||||
".gitrebase": "git-rebase",
|
||||
".go": "go",
|
||||
".groovy": "groovy",
|
||||
".hbs": "handlebars",
|
||||
".handlebars": "handlebars",
|
||||
".hs": "haskell",
|
||||
".html": "html",
|
||||
".htm": "html",
|
||||
".ini": "ini",
|
||||
".java": "java",
|
||||
".js": "javascript",
|
||||
".jsx": "javascriptreact",
|
||||
".json": "json",
|
||||
".tex": "latex",
|
||||
".latex": "latex",
|
||||
".less": "less",
|
||||
".lua": "lua",
|
||||
".makefile": "makefile",
|
||||
makefile: "makefile",
|
||||
".md": "markdown",
|
||||
".markdown": "markdown",
|
||||
".m": "objective-c",
|
||||
".mm": "objective-cpp",
|
||||
".pl": "perl",
|
||||
".pm": "perl6",
|
||||
".php": "php",
|
||||
".ps1": "powershell",
|
||||
".psm1": "powershell",
|
||||
".pug": "jade",
|
||||
".jade": "jade",
|
||||
".py": "python",
|
||||
".r": "r",
|
||||
".cshtml": "razor",
|
||||
".razor": "razor",
|
||||
".rb": "ruby",
|
||||
".rs": "rust",
|
||||
".scss": "scss",
|
||||
".sass": "sass",
|
||||
".scala": "scala",
|
||||
".shader": "shaderlab",
|
||||
".sh": "shellscript",
|
||||
".bash": "shellscript",
|
||||
".zsh": "shellscript",
|
||||
".ksh": "shellscript",
|
||||
".sql": "sql",
|
||||
".swift": "swift",
|
||||
".ts": "typescript",
|
||||
".tsx": "typescriptreact",
|
||||
".mts": "typescript",
|
||||
".cts": "typescript",
|
||||
".mtsx": "typescriptreact",
|
||||
".ctsx": "typescriptreact",
|
||||
".xml": "xml",
|
||||
".xsl": "xsl",
|
||||
".yaml": "yaml",
|
||||
".yml": "yaml",
|
||||
".mjs": "javascript",
|
||||
".cjs": "javascript",
|
||||
} as const;
|
||||
Reference in New Issue
Block a user