summaryrefslogtreecommitdiffhomepage
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/example/broken.ts1
-rw-r--r--js/example/cli.ts44
-rw-r--r--js/src/lsp/client.ts58
-rw-r--r--js/src/lsp/index.ts86
-rw-r--r--js/src/lsp/language.ts6
-rw-r--r--js/src/tool/diagnostics.ts56
-rw-r--r--js/src/tool/edit.ts18
-rw-r--r--js/src/tool/index.ts1
-rw-r--r--js/src/tool/view.ts3
9 files changed, 195 insertions, 78 deletions
diff --git a/js/example/broken.ts b/js/example/broken.ts
new file mode 100644
index 000000000..e073ad008
--- /dev/null
+++ b/js/example/broken.ts
@@ -0,0 +1 @@
+export const x: number = "asd";
diff --git a/js/example/cli.ts b/js/example/cli.ts
index bb7522cbb..9c9e07edd 100644
--- a/js/example/cli.ts
+++ b/js/example/cli.ts
@@ -1,27 +1,21 @@
-import { hc } from "hono/client";
-import type { Server } from "../src/server/server";
+import { App } from "../src/app";
+import path from "path";
+import { edit } from "../src/tool";
+import { FileTimes } from "../src/tool/util/file-times";
-const message = process.argv.slice(2).join(" ");
-console.log(message);
-
-const client = hc<Server.App>(`http://localhost:16713`);
-const session = await client.session_create.$post().then((res) => res.json());
-const result = await client.session_chat
- .$post({
- json: {
- sessionID: session.id,
- parts: [
- {
- type: "text",
- text: message,
- },
- ],
+await App.provide({ directory: process.cwd() }, async () => {
+ const file = path.join(process.cwd(), "example/broken.ts");
+ FileTimes.read(file);
+ const tool = await edit.execute(
+ {
+ file_path: file,
+ old_string: "x:",
+ new_string: "x:",
},
- })
- .then((res) => res.json());
-
-for (const part of result.parts) {
- if (part.type === "text") {
- console.log(part.text);
- }
-}
+ {
+ toolCallId: "test",
+ messages: [],
+ },
+ );
+ console.log(tool.output);
+});
diff --git a/js/src/lsp/client.ts b/js/src/lsp/client.ts
index 0c9f9d06c..ce2adebda 100644
--- a/js/src/lsp/client.ts
+++ b/js/src/lsp/client.ts
@@ -5,6 +5,7 @@ import {
StreamMessageReader,
StreamMessageWriter,
} from "vscode-jsonrpc/node";
+import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types";
import { App } from "../app";
import { Log } from "../util/log";
import { LANGUAGE_EXTENSIONS } from "./language";
@@ -16,16 +17,19 @@ export namespace LSPClient {
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[] }) {
+ export async function create(input: { cmd: string[]; serverID: string }) {
log.info("starting client", input);
let version = 0;
@@ -41,14 +45,14 @@ export namespace LSPClient {
new StreamMessageWriter(server.stdin),
);
- const diagnostics = new Map<string, any>();
+ const diagnostics = new Map<string, Diagnostic[]>();
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 });
+ Bus.publish(Event.Diagnostics, { path, serverID: input.serverID });
});
connection.listen();
@@ -114,34 +118,43 @@ export namespace LSPClient {
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 }) {
- 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 }) {
+ 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: 1,
+ text: text,
+ },
+ });
+ files.add(input.path);
+ return;
+ }
+
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(),
+ version,
},
contentChanges: [
{
@@ -154,21 +167,24 @@ export namespace LSPClient {
get diagnostics() {
return diagnostics;
},
- async refreshDiagnostics(input: { path: string }) {
+ async waitForDiagnostics(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) {
+ if (
+ event.properties.path === input.path &&
+ event.properties.serverID === result.clientID
+ ) {
log.info("refreshed diagnostics", input);
clearTimeout(timeout);
unsub?.();
resolve();
}
});
- await result.notify.change(input);
+ await result.notify.open(input);
}),
new Promise<void>((resolve) => {
timeout = setTimeout(() => {
diff --git a/js/src/lsp/index.ts b/js/src/lsp/index.ts
index 80503fb1f..358c6a549 100644
--- a/js/src/lsp/index.ts
+++ b/js/src/lsp/index.ts
@@ -1,6 +1,7 @@
import { App } from "../app";
import { Log } from "../util/log";
import { LSPClient } from "./client";
+import path from "path";
export namespace LSP {
const log = Log.create({ service: "lsp" });
@@ -10,17 +11,8 @@ export namespace LSP {
async () => {
const clients = new Map<string, LSPClient.Info>();
- // QUESTION: how lazy should lsp auto discovery be? should it not initialize until a file is opened?
- clients.set(
- "typescript",
- await LSPClient.create({
- cmd: ["bun", "x", "typescript-language-server", "--stdio"],
- }),
- );
-
return {
clients,
- diagnostics: new Map<string, any>(),
};
},
async (state) => {
@@ -30,7 +22,39 @@ export namespace LSP {
},
);
- export async function run<T>(
+ 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;
+ }
+
+ async function run<T>(
input: (client: LSPClient.Info) => Promise<T>,
): Promise<T[]> {
const clients = await state().then((x) => [...x.clients.values()]);
@@ -39,28 +63,48 @@ export namespace LSP {
}
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",
+ ".ts",
+ ".tsx",
+ ".js",
+ ".jsx",
+ ".mjs",
+ ".cjs",
+ ".mts",
+ ".cts",
+ ".mtsx",
+ ".ctsx",
],
},
{
+ id: "golang",
command: ["gopls"],
- extensions: ["go"],
+ 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}`;
+ }
+ }
}
diff --git a/js/src/lsp/language.ts b/js/src/lsp/language.ts
index 14b32edfa..e28d7a79c 100644
--- a/js/src/lsp/language.ts
+++ b/js/src/lsp/language.ts
@@ -76,8 +76,14 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".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;
diff --git a/js/src/tool/diagnostics.ts b/js/src/tool/diagnostics.ts
new file mode 100644
index 000000000..a01f39c7c
--- /dev/null
+++ b/js/src/tool/diagnostics.ts
@@ -0,0 +1,56 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+import path from "path";
+import { LSP } from "../lsp";
+import { App } from "../app";
+
+export const DiagnosticsTool = Tool.define({
+ name: "diagnostics",
+ description: `Get diagnostics for a file and/or project.
+
+WHEN TO USE THIS TOOL:
+- Use when you need to check for errors or warnings in your code
+- Helpful for debugging and ensuring code quality
+- Good for getting a quick overview of issues in a file or project
+
+HOW TO USE:
+- Provide a path to a file to get diagnostics for that file
+- Results are displayed in a structured format with severity levels
+
+FEATURES:
+- Displays errors, warnings, and hints
+- Groups diagnostics by severity
+- Provides detailed information about each diagnostic
+
+LIMITATIONS:
+- Results are limited to the diagnostics provided by the LSP clients
+- May not cover all possible issues in the code
+- Does not provide suggestions for fixing issues
+
+TIPS:
+- Use in conjunction with other tools for a comprehensive code review
+- Combine with the LSP client for real-time diagnostics`,
+ parameters: z.object({
+ path: z.string().describe("The path to the file to get diagnostics."),
+ }),
+ execute: async (args) => {
+ const app = await App.use();
+ const normalized = path.isAbsolute(args.path)
+ ? args.path
+ : path.join(app.root, args.path);
+ await LSP.file(normalized);
+ const diagnostics = await LSP.diagnostics();
+ const file = diagnostics[normalized];
+ return {
+ metadata: {
+ diagnostics,
+ },
+ output: file?.length
+ ? file.map(LSP.Diagnostic.pretty).join("\n")
+ : "No errors found",
+ };
+ },
+});
+
+const x: number = "asd";
+
diff --git a/js/src/tool/edit.ts b/js/src/tool/edit.ts
index 96d8e0c54..42cd02bb4 100644
--- a/js/src/tool/edit.ts
+++ b/js/src/tool/edit.ts
@@ -118,17 +118,15 @@ export const edit = Tool.define({
FileTimes.read(filePath);
let output = "";
- await LSP.run((client) => client.refreshDiagnostics({ path: filePath }));
- const diagnostics = await LSP.run(async (client) => client.diagnostics);
- for (const diagnostic of diagnostics) {
- for (const [file, params] of diagnostic.entries()) {
- if (params.length === 0) continue;
- if (file === filePath) {
- output += `\nThis file has errors, please fix\n<file_diagnostics>\n${JSON.stringify(params)}\n</file_diagnostics>\n`;
- continue;
- }
- output += `\n<project_diagnostics>\n${JSON.stringify(params)}\n</project_diagnostics>\n`;
+ await LSP.file(filePath);
+ const diagnostics = await LSP.diagnostics();
+ for (const [file, issues] of Object.entries(diagnostics)) {
+ if (issues.length === 0) continue;
+ if (file === filePath) {
+ output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`;
+ continue;
}
+ output += `\n<project_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`;
}
return {
diff --git a/js/src/tool/index.ts b/js/src/tool/index.ts
index d7027c529..b18f85012 100644
--- a/js/src/tool/index.ts
+++ b/js/src/tool/index.ts
@@ -5,3 +5,4 @@ export * from "./glob";
export * from "./grep";
export * from "./view";
export * from "./ls";
+export * from "./diagnostics";
diff --git a/js/src/tool/view.ts b/js/src/tool/view.ts
index 5b429c63d..138f9ad3a 100644
--- a/js/src/tool/view.ts
+++ b/js/src/tool/view.ts
@@ -115,7 +115,8 @@ export const view = Tool.define({
}
output += "\n</file>";
- await LSP.run((client) => client.notify.open({ path: filePath }));
+ // just warms the lsp client
+ LSP.file(filePath);
FileTimes.read(filePath);
return {