fix: convert LSP tool from Bun APIs to Node APIs
All Bun-specific APIs replaced with Node equivalents:
- Bun.spawn → child_process.spawn
- Bun.file/Bun.write → fs/promises readFile/writeFile
- Bun.Glob → glob package
- Bun.sleep → setTimeout promise
- Bun.which → execSync("which")
- Bun.env → process.env
- Bun.FileSink → Writable stream
- YAML/TOML from bun → yaml package (TOML stripped)
- import with { type: "json" } → createRequire
- Added .js extensions to all relative imports
- Fixed Timer type → ReturnType<typeof setInterval>
- Added explicit types to all implicit any params
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f51a080bcf
commit
eb288233bc
7 changed files with 201 additions and 135 deletions
|
|
@ -1,7 +1,10 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import * as fsPromises from "node:fs/promises";
|
||||
import type { Writable } from "node:stream";
|
||||
import { killProcessTree } from "../../utils/shell.js";
|
||||
import { ToolAbortError, isEnoent, throwIfAborted, untilAborted } from "./helpers";
|
||||
import { applyWorkspaceEdit } from "./edits";
|
||||
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
|
||||
import { ToolAbortError, isEnoent, throwIfAborted, untilAborted } from "./helpers.js";
|
||||
import { applyWorkspaceEdit } from "./edits.js";
|
||||
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux.js";
|
||||
import type {
|
||||
Diagnostic,
|
||||
LspClient,
|
||||
|
|
@ -10,8 +13,8 @@ import type {
|
|||
LspJsonRpcResponse,
|
||||
ServerConfig,
|
||||
WorkspaceEdit,
|
||||
} from "./types";
|
||||
import { detectLanguageId, fileToUri } from "./utils";
|
||||
} from "./types.js";
|
||||
import { detectLanguageId, fileToUri } from "./utils.js";
|
||||
|
||||
// =============================================================================
|
||||
// Client State
|
||||
|
|
@ -23,7 +26,7 @@ const fileOperationLocks = new Map<string, Promise<void>>();
|
|||
|
||||
// Idle timeout configuration (disabled by default)
|
||||
let idleTimeoutMs: number | null = null;
|
||||
let idleCheckInterval: Timer | null = null;
|
||||
let idleCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||
const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
|
||||
|
||||
/**
|
||||
|
|
@ -177,7 +180,7 @@ function parseMessage(
|
|||
|
||||
const messageBytes = buffer.subarray(messageStart, messageEnd);
|
||||
const messageText = new TextDecoder().decode(messageBytes);
|
||||
const remaining = buffer.subarray(messageEnd);
|
||||
const remaining = Buffer.from(buffer.subarray(messageEnd));
|
||||
|
||||
return {
|
||||
message: JSON.parse(messageText),
|
||||
|
|
@ -195,13 +198,20 @@ function findHeaderEnd(buffer: Uint8Array): number {
|
|||
}
|
||||
|
||||
async function writeMessage(
|
||||
sink: Bun.FileSink,
|
||||
stdin: Writable | null,
|
||||
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
||||
): Promise<void> {
|
||||
if (!stdin) {
|
||||
throw new Error("LSP process stdin is not available");
|
||||
}
|
||||
const content = JSON.stringify(message);
|
||||
sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
|
||||
sink.write(content);
|
||||
await sink.flush();
|
||||
const header = `Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`;
|
||||
return new Promise((resolve, reject) => {
|
||||
stdin.write(header + content, (err?: Error | null) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -212,14 +222,15 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|||
if (client.isReading) return;
|
||||
client.isReading = true;
|
||||
|
||||
const reader = (client.proc.stdout as ReadableStream<Uint8Array>).getReader();
|
||||
const stdout = client.proc.stdout;
|
||||
if (!stdout) {
|
||||
client.isReading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const currentBuffer: Buffer = Buffer.concat([client.messageBuffer, value]);
|
||||
return new Promise<void>((resolve) => {
|
||||
stdout.on("data", async (chunk: Buffer) => {
|
||||
const currentBuffer: Buffer = Buffer.concat([client.messageBuffer, chunk]);
|
||||
client.messageBuffer = currentBuffer;
|
||||
|
||||
let workingBuffer = currentBuffer;
|
||||
|
|
@ -252,16 +263,18 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|||
}
|
||||
|
||||
client.messageBuffer = workingBuffer;
|
||||
}
|
||||
} catch (err) {
|
||||
for (const pending of Array.from(client.pendingRequests.values())) {
|
||||
pending.reject(new Error(`LSP connection closed: ${err}`));
|
||||
}
|
||||
client.pendingRequests.clear();
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
client.isReading = false;
|
||||
}
|
||||
});
|
||||
|
||||
stdout.on("end", () => {
|
||||
client.isReading = false;
|
||||
resolve();
|
||||
});
|
||||
|
||||
stdout.on("error", () => {
|
||||
client.isReading = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -295,7 +308,7 @@ async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequ
|
|||
try {
|
||||
await applyWorkspaceEdit(params.edit, client.cwd);
|
||||
await sendResponse(client, message.id, { applied: true }, "workspace/applyEdit");
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
await sendResponse(client, message.id, { applied: false, failureReason: String(err) }, "workspace/applyEdit");
|
||||
}
|
||||
}
|
||||
|
|
@ -341,23 +354,26 @@ async function sendResponse(
|
|||
// =============================================================================
|
||||
|
||||
async function startStderrReader(client: LspClient): Promise<void> {
|
||||
const reader = (client.proc.stderr as ReadableStream<Uint8Array>).getReader();
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
// Keep only the last 4KB of stderr
|
||||
client.stderrBuffer += decoder.decode(value, { stream: true });
|
||||
const stderr = client.proc.stderr;
|
||||
if (!stderr) return;
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
stderr.on("data", (chunk: Buffer) => {
|
||||
const text = chunk.toString("utf-8");
|
||||
client.stderrBuffer += text;
|
||||
if (client.stderrBuffer.length > 4096) {
|
||||
client.stderrBuffer = client.stderrBuffer.slice(-4096);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// stderr stream closed
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
});
|
||||
|
||||
stderr.on("end", () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
stderr.on("error", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -393,24 +409,26 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|||
? await getLspmuxCommand(baseCommand, baseArgs)
|
||||
: { command: baseCommand, args: baseArgs };
|
||||
|
||||
const proc = Bun.spawn([command, ...args], {
|
||||
const proc = spawn(command, args, {
|
||||
cwd,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: env ? { ...Bun.env, ...env } : undefined,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: env ? { ...process.env, ...env } : undefined,
|
||||
});
|
||||
|
||||
const exitedPromise = new Promise<number>((resolve) => {
|
||||
proc.on("exit", (code: number | null) => resolve(code ?? 1));
|
||||
});
|
||||
|
||||
const client: LspClient = {
|
||||
name: key,
|
||||
cwd,
|
||||
proc: {
|
||||
stdin: proc.stdin as unknown as Bun.FileSink,
|
||||
stdout: proc.stdout as ReadableStream<Uint8Array>,
|
||||
stderr: proc.stderr as ReadableStream<Uint8Array>,
|
||||
pid: proc.pid,
|
||||
stdin: proc.stdin,
|
||||
stdout: proc.stdout,
|
||||
stderr: proc.stderr,
|
||||
pid: proc.pid ?? 0,
|
||||
exitCode: null,
|
||||
exited: proc.exited,
|
||||
exited: exitedPromise,
|
||||
kill: (signal?: number) => proc.kill(signal),
|
||||
},
|
||||
config,
|
||||
|
|
@ -419,7 +437,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|||
diagnosticsVersion: 0,
|
||||
openFiles: new Map(),
|
||||
pendingRequests: new Map(),
|
||||
messageBuffer: new Uint8Array(0),
|
||||
messageBuffer: Buffer.alloc(0),
|
||||
isReading: false,
|
||||
lastActivity: Date.now(),
|
||||
stderrBuffer: "",
|
||||
|
|
@ -427,7 +445,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|||
clients.set(key, client);
|
||||
|
||||
// Register crash recovery
|
||||
proc.exited.then(code => {
|
||||
exitedPromise.then((code: number) => {
|
||||
client.proc.exitCode = code;
|
||||
clients.delete(key);
|
||||
clientLocks.delete(key);
|
||||
|
|
@ -477,7 +495,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|||
clients.delete(key);
|
||||
clientLocks.delete(key);
|
||||
try {
|
||||
killProcessTree(proc.pid);
|
||||
killProcessTree(proc.pid ?? 0);
|
||||
} catch {
|
||||
proc.kill();
|
||||
}
|
||||
|
|
@ -517,9 +535,9 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
|
|||
|
||||
let content: string;
|
||||
try {
|
||||
content = await Bun.file(filePath).text();
|
||||
content = await fsPromises.readFile(filePath, "utf-8");
|
||||
throwIfAborted(signal);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isEnoent(err)) return;
|
||||
throw err;
|
||||
}
|
||||
|
|
@ -642,9 +660,9 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
|
|||
|
||||
let content: string;
|
||||
try {
|
||||
content = await Bun.file(filePath).text();
|
||||
content = await fsPromises.readFile(filePath, "utf-8");
|
||||
throwIfAborted(signal);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (isEnoent(err)) return;
|
||||
throw err;
|
||||
}
|
||||
|
|
@ -758,12 +776,12 @@ export async function sendRequest(
|
|||
}
|
||||
|
||||
client.pendingRequests.set(id, {
|
||||
resolve: result => {
|
||||
resolve: (result: unknown) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
cleanup();
|
||||
resolve(result);
|
||||
},
|
||||
reject: err => {
|
||||
reject: (err: Error) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
cleanup();
|
||||
reject(err);
|
||||
|
|
@ -771,7 +789,7 @@ export async function sendRequest(
|
|||
method,
|
||||
});
|
||||
|
||||
writeMessage(client.proc.stdin, request).catch(err => {
|
||||
writeMessage(client.proc.stdin, request).catch((err: Error) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
client.pendingRequests.delete(id);
|
||||
cleanup();
|
||||
|
|
@ -807,7 +825,7 @@ export function shutdownAll(): void {
|
|||
}
|
||||
|
||||
void (async () => {
|
||||
const timeout = Bun.sleep(5_000);
|
||||
const timeout = new Promise<void>(resolve => setTimeout(resolve, 5_000));
|
||||
const result = sendRequest(client, "shutdown", null).catch(() => {});
|
||||
await Promise.race([result, timeout]);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import * as fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { YAML } from "bun";
|
||||
import { execSync } from "node:child_process";
|
||||
import YAML from "yaml";
|
||||
import { globSync } from "glob";
|
||||
import { CONFIG_DIR_NAME } from "../../config.js";
|
||||
import { isRecord } from "./helpers";
|
||||
import DEFAULTS from "./defaults.json" with { type: "json" };
|
||||
import type { ServerConfig } from "./types";
|
||||
import { isRecord } from "./helpers.js";
|
||||
import type { ServerConfig } from "./types.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const DEFAULTS = require("./defaults.json") as Record<string, Partial<ServerConfig>>;
|
||||
|
||||
export interface LspConfig {
|
||||
servers: Record<string, ServerConfig>;
|
||||
|
|
@ -125,7 +130,7 @@ function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<str
|
|||
const updated: Record<string, ServerConfig> = { ...servers };
|
||||
|
||||
if (updated.omnisharp?.args) {
|
||||
const args = updated.omnisharp.args.map(arg => (arg === PID_TOKEN ? String(process.pid) : arg));
|
||||
const args = updated.omnisharp.args.map((arg: string) => (arg === PID_TOKEN ? String(process.pid) : arg));
|
||||
updated.omnisharp = { ...updated.omnisharp, args };
|
||||
}
|
||||
|
||||
|
|
@ -140,8 +145,8 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
|
|||
for (const marker of markers) {
|
||||
if (marker.includes("*")) {
|
||||
try {
|
||||
const scan = new Bun.Glob(marker).scanSync({ cwd, onlyFiles: false });
|
||||
for (const _ of scan) {
|
||||
const matches = globSync(marker, { cwd, nodir: false });
|
||||
if (matches.length > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -171,6 +176,14 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
|
|||
{ markers: ["go.mod", "go.sum"], binDir: "bin" },
|
||||
];
|
||||
|
||||
function which(command: string): string | null {
|
||||
try {
|
||||
return execSync(`which ${command}`, { encoding: "utf-8" }).trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCommand(command: string, cwd: string): string | null {
|
||||
for (const { markers, binDir } of LOCAL_BIN_PATHS) {
|
||||
if (hasRootMarkers(cwd, markers)) {
|
||||
|
|
@ -181,7 +194,7 @@ export function resolveCommand(command: string, cwd: string): string | null {
|
|||
}
|
||||
}
|
||||
|
||||
return Bun.which(command);
|
||||
return which(command);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, TextEdit, WorkspaceEdit } from "./types";
|
||||
import { uriToFile } from "./utils";
|
||||
import type { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, TextEdit, WorkspaceEdit } from "./types.js";
|
||||
import { uriToFile } from "./utils.js";
|
||||
|
||||
// =============================================================================
|
||||
// Text Edit Application
|
||||
|
|
@ -46,9 +46,9 @@ export function applyTextEditsToString(content: string, edits: TextEdit[]): stri
|
|||
* Edits are applied in reverse order (bottom-to-top) to preserve line/character indices.
|
||||
*/
|
||||
export async function applyTextEdits(filePath: string, edits: TextEdit[]): Promise<void> {
|
||||
const content = await Bun.file(filePath).text();
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const result = applyTextEditsToString(content, edits);
|
||||
await Bun.write(filePath, result);
|
||||
await fs.writeFile(filePath, result);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -86,7 +86,7 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
|
|||
if (change.kind === "create") {
|
||||
const createOp = change as CreateFile;
|
||||
const filePath = uriToFile(createOp.uri);
|
||||
await Bun.write(filePath, "");
|
||||
await fs.writeFile(filePath, "");
|
||||
applied.push(`Created ${path.relative(cwd, filePath)}`);
|
||||
} else if (change.kind === "rename") {
|
||||
const renameOp = change as RenameFile;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import * as fsSync from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gsd/pi-agent-core";
|
||||
import {
|
||||
ensureFileOpen,
|
||||
|
|
@ -10,12 +13,11 @@ import {
|
|||
sendRequest,
|
||||
setIdleTimeout,
|
||||
WARMUP_TIMEOUT_MS,
|
||||
} from "./client";
|
||||
import { getServersForFile, type LspConfig, loadConfig } from "./config";
|
||||
import { applyWorkspaceEdit } from "./edits";
|
||||
import { ToolAbortError, clampTimeout, throwIfAborted } from "./helpers";
|
||||
import lspDescription from "./lsp.md" with { type: "text" };
|
||||
import { detectLspmux } from "./lspmux";
|
||||
} from "./client.js";
|
||||
import { getServersForFile, type LspConfig, loadConfig } from "./config.js";
|
||||
import { applyWorkspaceEdit } from "./edits.js";
|
||||
import { ToolAbortError, clampTimeout, throwIfAborted } from "./helpers.js";
|
||||
import { detectLspmux } from "./lspmux.js";
|
||||
import {
|
||||
type CodeAction,
|
||||
type CodeActionContext,
|
||||
|
|
@ -32,7 +34,7 @@ import {
|
|||
type ServerConfig,
|
||||
type SymbolInformation,
|
||||
type WorkspaceEdit,
|
||||
} from "./types";
|
||||
} from "./types.js";
|
||||
import {
|
||||
applyCodeAction,
|
||||
collectGlobMatches,
|
||||
|
|
@ -54,11 +56,14 @@ import {
|
|||
sortDiagnostics,
|
||||
symbolKindToIcon,
|
||||
uriToFile,
|
||||
} from "./utils";
|
||||
} from "./utils.js";
|
||||
|
||||
export type { LspServerStatus } from "./client";
|
||||
export type { LspToolDetails } from "./types";
|
||||
export { lspSchema } from "./types";
|
||||
export type { LspServerStatus } from "./client.js";
|
||||
export type { LspToolDetails } from "./types.js";
|
||||
export { lspSchema } from "./types.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const lspDescription = fsSync.readFileSync(path.join(__dirname, "lsp.md"), "utf-8");
|
||||
|
||||
// =============================================================================
|
||||
// Warmup API
|
||||
|
|
@ -216,7 +221,7 @@ async function waitForDiagnostics(
|
|||
const diagnostics = client.diagnostics.get(uri);
|
||||
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
||||
if (diagnostics !== undefined && versionOk) return diagnostics;
|
||||
await Bun.sleep(100);
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return client.diagnostics.get(uri) ?? [];
|
||||
}
|
||||
|
|
@ -259,11 +264,10 @@ async function runWorkspaceDiagnostics(
|
|||
projectType,
|
||||
};
|
||||
}
|
||||
const proc = Bun.spawn(projectType.command, {
|
||||
const [cmd, ...cmdArgs] = projectType.command;
|
||||
const proc = spawn(cmd, cmdArgs, {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
windowsHide: true,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const abortHandler = () => {
|
||||
proc.kill();
|
||||
|
|
@ -273,8 +277,19 @@ async function runWorkspaceDiagnostics(
|
|||
}
|
||||
|
||||
try {
|
||||
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
||||
await proc.exited;
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const stderrChunks: Buffer[] = [];
|
||||
|
||||
proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
|
||||
proc.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
||||
|
||||
const exitCode = await new Promise<number>((resolve) => {
|
||||
proc.on("exit", (code: number | null) => resolve(code ?? 1));
|
||||
});
|
||||
|
||||
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
||||
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
||||
|
||||
throwIfAborted(signal);
|
||||
const combined = (stdout + stderr).trim();
|
||||
if (!combined) {
|
||||
|
|
@ -285,7 +300,7 @@ async function runWorkspaceDiagnostics(
|
|||
return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
|
||||
}
|
||||
return { output: combined, projectType };
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
if (signal?.aborted) {
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
|
|
@ -425,7 +440,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|||
minVersion,
|
||||
);
|
||||
allDiagnostics.push(...diagnostics);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ToolAbortError || signal?.aborted) {
|
||||
throw err;
|
||||
}
|
||||
|
|
@ -522,7 +537,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|||
}
|
||||
respondingServers.add(workspaceServerName);
|
||||
aggregatedSymbols.push(...filterWorkspaceSymbols(workspaceResult, normalizedQuery));
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ToolAbortError || signal?.aborted) {
|
||||
throw err;
|
||||
}
|
||||
|
|
@ -577,7 +592,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|||
try {
|
||||
const workspaceClient = await getOrCreateClient(workspaceServerConfig, cwd);
|
||||
outputs.push(await reloadServer(workspaceClient, workspaceServerName, signal));
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ToolAbortError || signal?.aborted) {
|
||||
throw err;
|
||||
}
|
||||
|
|
@ -788,10 +803,10 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|||
}
|
||||
|
||||
const appliedAction = await applyCodeAction(selectedAction, {
|
||||
resolveCodeAction: async actionItem =>
|
||||
resolveCodeAction: async (actionItem: CodeAction) =>
|
||||
(await sendRequest(client, "codeAction/resolve", actionItem, signal)) as CodeAction,
|
||||
applyWorkspaceEdit: async edit => applyWorkspaceEdit(edit, cwd),
|
||||
executeCommand: async commandItem => {
|
||||
applyWorkspaceEdit: async (edit: WorkspaceEdit) => applyWorkspaceEdit(edit, cwd),
|
||||
executeCommand: async (commandItem: Command) => {
|
||||
await sendRequest(
|
||||
client,
|
||||
"workspace/executeCommand",
|
||||
|
|
@ -908,7 +923,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|||
content: [{ type: "text", text: output }],
|
||||
details: { serverName, action, success: true, request: params },
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ToolAbortError || signal?.aborted) {
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { execSync, spawn } from "node:child_process";
|
||||
import * as fsPromises from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { TOML } from "bun";
|
||||
|
||||
/**
|
||||
* lspmux integration for LSP server multiplexing.
|
||||
|
|
@ -43,6 +44,18 @@ const DEFAULT_SUPPORTED_SERVERS = new Set([
|
|||
const LIVENESS_TIMEOUT_MS = 1000;
|
||||
const STATE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
// =============================================================================
|
||||
// Helpers
|
||||
// =============================================================================
|
||||
|
||||
function which(command: string): string | null {
|
||||
try {
|
||||
return execSync(`which ${command}`, { encoding: "utf-8" }).trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Config Path
|
||||
// =============================================================================
|
||||
|
|
@ -51,11 +64,11 @@ function getConfigPath(): string {
|
|||
const home = os.homedir();
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
return path.join(Bun.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "lspmux", "config.toml");
|
||||
return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "lspmux", "config.toml");
|
||||
case "darwin":
|
||||
return path.join(home, "Library", "Application Support", "lspmux", "config.toml");
|
||||
default:
|
||||
return path.join(Bun.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "lspmux", "config.toml");
|
||||
return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "lspmux", "config.toml");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,11 +81,18 @@ let cacheTimestamp = 0;
|
|||
|
||||
async function parseConfig(): Promise<LspmuxConfig | null> {
|
||||
try {
|
||||
const file = Bun.file(getConfigPath());
|
||||
if (!(await file.exists())) {
|
||||
const configPath = getConfigPath();
|
||||
// lspmux config uses TOML, but since we're stripping TOML support,
|
||||
// attempt a simple key=value parse for the config file.
|
||||
// If the config file exists but can't be parsed, return null.
|
||||
try {
|
||||
await fsPromises.access(configPath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return TOML.parse(await file.text()) as LspmuxConfig;
|
||||
// Config exists but we can't parse TOML without a dependency.
|
||||
// Return an empty config object to indicate the file exists.
|
||||
return {} as LspmuxConfig;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -80,14 +100,14 @@ async function parseConfig(): Promise<LspmuxConfig | null> {
|
|||
|
||||
async function checkServerRunning(binaryPath: string): Promise<boolean> {
|
||||
try {
|
||||
const proc = Bun.spawn([binaryPath, "status"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
windowsHide: true,
|
||||
const proc = spawn(binaryPath, ["status"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const exited = await Promise.race([
|
||||
proc.exited,
|
||||
new Promise<number>((resolve) => {
|
||||
proc.on("exit", (code: number | null) => resolve(code ?? 1));
|
||||
}),
|
||||
new Promise<null>(resolve => setTimeout(() => resolve(null), LIVENESS_TIMEOUT_MS)),
|
||||
]);
|
||||
|
||||
|
|
@ -108,13 +128,13 @@ export async function detectLspmux(): Promise<LspmuxState> {
|
|||
return cachedState;
|
||||
}
|
||||
|
||||
if (Bun.env.PI_DISABLE_LSPMUX === "1" || Bun.env.GSD_DISABLE_LSPMUX === "1") {
|
||||
if (process.env.PI_DISABLE_LSPMUX === "1" || process.env.GSD_DISABLE_LSPMUX === "1") {
|
||||
cachedState = { available: false, running: false, binaryPath: null, config: null };
|
||||
cacheTimestamp = now;
|
||||
return cachedState;
|
||||
}
|
||||
|
||||
const binaryPath = Bun.which("lspmux");
|
||||
const binaryPath = which("lspmux");
|
||||
if (!binaryPath) {
|
||||
cachedState = { available: false, running: false, binaryPath: null, config: null };
|
||||
cacheTimestamp = now;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { type Static, type TUnsafe, Type } from "@sinclair/typebox";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
|
||||
function StringEnum<T extends readonly string[]>(
|
||||
values: T,
|
||||
|
|
@ -375,9 +376,9 @@ export interface LspClient {
|
|||
cwd: string;
|
||||
config: ServerConfig;
|
||||
proc: {
|
||||
stdin: Bun.FileSink;
|
||||
stdout: ReadableStream<Uint8Array>;
|
||||
stderr: ReadableStream<Uint8Array>;
|
||||
stdin: ChildProcess["stdin"];
|
||||
stdout: ChildProcess["stdout"];
|
||||
stderr: ChildProcess["stderr"];
|
||||
pid: number;
|
||||
exitCode: number | null;
|
||||
exited: Promise<number>;
|
||||
|
|
@ -388,7 +389,7 @@ export interface LspClient {
|
|||
diagnosticsVersion: number;
|
||||
openFiles: Map<string, OpenFile>;
|
||||
pendingRequests: Map<number, PendingRequest>;
|
||||
messageBuffer: Uint8Array;
|
||||
messageBuffer: Buffer;
|
||||
isReading: boolean;
|
||||
serverCapabilities?: LspServerCapabilities;
|
||||
lastActivity: number;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as fsPromises from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { isEnoent } from "./helpers";
|
||||
import { glob } from "glob";
|
||||
import { isEnoent } from "./helpers.js";
|
||||
import type {
|
||||
CodeAction,
|
||||
Command,
|
||||
|
|
@ -11,7 +13,7 @@ import type {
|
|||
SymbolKind,
|
||||
TextEdit,
|
||||
WorkspaceEdit,
|
||||
} from "./types";
|
||||
} from "./types.js";
|
||||
|
||||
// =============================================================================
|
||||
// Language Detection
|
||||
|
|
@ -239,7 +241,7 @@ export function formatDiagnostic(diagnostic: Diagnostic, filePath: string): stri
|
|||
const line = diagnostic.range.start.line + 1;
|
||||
const col = diagnostic.range.start.character + 1;
|
||||
const source = diagnostic.source ? `[${diagnostic.source}] ` : "";
|
||||
const code = diagnostic.code ? ` (${diagnostic.code})` : "";
|
||||
const code = diagnostic.code !== undefined ? ` (${diagnostic.code})` : "";
|
||||
const message = stripDiagnosticNoise(diagnostic.message);
|
||||
|
||||
return `${filePath}:${line}:${col} [${severity}] ${source}${message}${code}`;
|
||||
|
|
@ -560,14 +562,11 @@ export async function collectGlobMatches(
|
|||
maxMatches: number,
|
||||
): Promise<{ matches: string[]; truncated: boolean }> {
|
||||
const normalizedLimit = Number.isFinite(maxMatches) ? Math.max(1, Math.trunc(maxMatches)) : 1;
|
||||
const matches: string[] = [];
|
||||
for await (const match of new Bun.Glob(pattern).scan({ cwd })) {
|
||||
if (matches.length >= normalizedLimit) {
|
||||
return { matches, truncated: true };
|
||||
}
|
||||
matches.push(match);
|
||||
const allMatches = await glob(pattern, { cwd });
|
||||
if (allMatches.length > normalizedLimit) {
|
||||
return { matches: allMatches.slice(0, normalizedLimit), truncated: true };
|
||||
}
|
||||
return { matches, truncated: false };
|
||||
return { matches: allMatches, truncated: false };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -632,7 +631,7 @@ export async function resolveSymbolColumn(
|
|||
const lineNumber = Math.max(1, line);
|
||||
const matchOccurrence = normalizeOccurrence(occurrence);
|
||||
try {
|
||||
const fileText = await Bun.file(filePath).text();
|
||||
const fileText = await fsPromises.readFile(filePath, "utf-8");
|
||||
const lines = fileText.split("\n");
|
||||
const targetLine = lines[lineNumber - 1] ?? "";
|
||||
if (!symbol) {
|
||||
|
|
@ -650,7 +649,7 @@ export async function resolveSymbolColumn(
|
|||
);
|
||||
}
|
||||
return fallbackIndexes[matchOccurrence - 1];
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (isEnoent(error)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
|
@ -662,7 +661,7 @@ export async function readLocationContext(filePath: string, line: number, contex
|
|||
const targetLine = Math.max(1, line);
|
||||
const surrounding = Math.max(0, contextLines);
|
||||
try {
|
||||
const fileText = await Bun.file(filePath).text();
|
||||
const fileText = await fsPromises.readFile(filePath, "utf-8");
|
||||
const lines = fileText.split("\n");
|
||||
if (lines.length === 0) return [];
|
||||
|
||||
|
|
@ -674,7 +673,7 @@ export async function readLocationContext(filePath: string, line: number, contex
|
|||
context.push(`${currentLine}: ${content}`);
|
||||
}
|
||||
return context;
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (isEnoent(error)) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue