diff --git a/bun.lock b/bun.lock index e99fefc76..458df7db2 100644 --- a/bun.lock +++ b/bun.lock @@ -29,17 +29,17 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.10.0", - "oh-my-opencode-darwin-x64": "3.10.0", - "oh-my-opencode-darwin-x64-baseline": "3.10.0", - "oh-my-opencode-linux-arm64": "3.10.0", - "oh-my-opencode-linux-arm64-musl": "3.10.0", - "oh-my-opencode-linux-x64": "3.10.0", - "oh-my-opencode-linux-x64-baseline": "3.10.0", - "oh-my-opencode-linux-x64-musl": "3.10.0", - "oh-my-opencode-linux-x64-musl-baseline": "3.10.0", - "oh-my-opencode-windows-x64": "3.10.0", - "oh-my-opencode-windows-x64-baseline": "3.10.0", + "oh-my-opencode-darwin-arm64": "3.11.0", + "oh-my-opencode-darwin-x64": "3.11.0", + "oh-my-opencode-darwin-x64-baseline": "3.11.0", + "oh-my-opencode-linux-arm64": "3.11.0", + "oh-my-opencode-linux-arm64-musl": "3.11.0", + "oh-my-opencode-linux-x64": "3.11.0", + "oh-my-opencode-linux-x64-baseline": "3.11.0", + "oh-my-opencode-linux-x64-musl": "3.11.0", + "oh-my-opencode-linux-x64-musl-baseline": "3.11.0", + "oh-my-opencode-windows-x64": "3.11.0", + "oh-my-opencode-windows-x64-baseline": "3.11.0", }, }, }, @@ -238,27 +238,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.10.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KQ1Nva4eU03WIaQI8BiEgizYJAeddUIaC8dmks0Ug/2EkH6VyNj41+shI58HFGN9Jlg9Fd6MxpOW92S3JUHjOw=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.11.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-TLMCq1HXU1BOp3KWdcITQqT3TQcycAxvdYELMzY/17HUVHjvJiaLjyrbmw0VlgBjoRZOlmsedK+o59y7WRM40Q=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.10.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-PydZ6wKyLZzikSZA3Q89zKZwFyg0Ouqd/S6zDsf1zzpUWT1t5EcpBtYFwuscD7L4hdkIEFm8wxnnBkz5i6BEiA=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-szKfyAYbI3Mp6rqxHxcHhAE8noxIzBbpfvKX0acyMB/KRqUCtgTe13aic5tz/W/Agp9NU1PVasyqjJjAtE73JA=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.10.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-yOaVd0E1qspT2xP/BMJaJ/rpFTwkOh9U/SAk6uOuxHld6dZGI9e2Oq8F3pSD16xHnnpaz4VzadtT6HkvPdtBYg=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@3.11.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-QZ+2LCcXK6NPopYSxFCHrYAqLccN+jMQ0YrQI+QBlsajLSsnSqfv6W3Vaxv95iLWhGey3v2oGu5OUgdW9fjy9w=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.10.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-pLzcPMuzBb1tpVgqMilv7QdsE2xTMLCWT3b807mzjt0302fZTfm6emwymCG25RamHdq7+mI2B0rN7hjvbymFog=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-NZMbNG+kJ0FTS4u5xhuBUjJ2K2Tds8sETbdq1VPT52rd+mIbVVSbugfppagEh9wbNqXqJY1HwQ/+4Q+NoGGXhQ=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.10.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ca61zr+X8q0ipO2x72qU+4R6Dsr168OM9aXI6xDHbrr0l3XZlRO8xuwQidch1vE5QRv2/IJT10KjAFInCERDug=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.11.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-f0GO63uAwzBisotiMneA7Pi2xPXUxvdX5QRC6z4X2xoB8F7/jT+2+dY8J03eM+YJVAwQWR/74hm5HFSenqMeIA=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-m0Ys8Vnl8jUNRE5/aIseNOF1H57/W77xh3vkyBVfnjzHwQdEUWZz3IdoHaEWIFgIP2+fsNXRHqpx7Pbtuhxo6Q=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OzIgo26t1EbooHwzmli+4aemO6YqXEhJTBth8L688K1CI/xF567G3+uJemZ9U7NI+miHJRoKHcidNnaAi7bgGQ=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-a6OhfqMXhOTq1On8YHRRlVsNtMx84kgNAnStk/sY1Dw0kXU68QK4tWXVF+wNdiRG3egeM2SvjhJ5RhWlr3CCNQ=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ac7TfBli+gaHVu4aBtP2ADWzetrFZOs+h1K39KsR6MOhDZBl+B6B1S47U+BXGWtUKIRYm4uUo578XdnmsDanoA=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-lZkoEWwmrlVoZKewHNslUmQ2D6eWi1YqsoZMTd3qRj8V4XI6TDZHxg86hw4oxZ/EnKO4un+r83tb09JAAb1nNQ=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OvOsPNuvZQug4tGjbcpbvh67tud1K84A3Qskt9S7BHBIvMH129iV/2GGyr6aca8gwvd5T+X05H/s5mnPG6jkBQ=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UqArUpatMuen8+hZhMSbScaSmJlcwkEtf/IzDN1iYO0CttvhyYMUmm3el/1gWTAcaGNDFNkGmTli5WNYhnm2lA=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@3.11.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-fSsyVAFMoOljD+zqRO6lG3f9ka1YRLMp6rNSsPWkLEKKIyEdw1J0GcmA/48VI1NgtnEgKqS3Ft87tees1woyBw=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.10.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-BivOu1+Yty9N6VSmNzmxROZqjQKu3ImWjooKZDfczvYLDQmZV104QcOKV6bmdOCpHrqQ7cvdbygmeiJeRoYShg=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-k9F3/9r3pFnUVJW36+zF06znUdUzcnJp+BdvDcaJrcuuM516ECwCH0yY5WbDTFFydFBQBkPBJX9DwU8dmc4kHA=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.10.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-BBv+dNPuh9LEuqXUJLXNsvi3vL30zS1qcJuzlq/s8rYHry+VvEVXCRcMm5Vo0CVna8bUZf5U8MDkGDHOAiTeEw=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@3.11.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mRRcCHC43TLUuIkDs0ASAUGo3DpMIkSeIPDdtBrh1eJZyVulJRGBoniIk/+Y+RJwtsUoC+lUX/auQelzJsMpbQ=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/agents/atlas/default.ts b/src/agents/atlas/default.ts index 64e6a3348..380606752 100644 --- a/src/agents/atlas/default.ts +++ b/src/agents/atlas/default.ts @@ -184,7 +184,7 @@ task( After EVERY delegation, complete ALL of these steps — no shortcuts: #### A. Automated Verification -1. \`lsp_diagnostics(filePath=".")\` → ZERO errors at project level +1. 'lsp_diagnostics(filePath=".", extension=".ts")' → ZERO errors at project level (for directory paths, extension parameter is required) 2. \`bun run build\` or \`bun run typecheck\` → exit code 0 3. \`bun test\` → ALL tests pass @@ -346,7 +346,7 @@ You are the QA gate. Subagents lie. Verify EVERYTHING. **After each delegation — BOTH automated AND manual verification are MANDATORY:** -1. \`lsp_diagnostics\` at PROJECT level → ZERO errors +1. 'lsp_diagnostics(filePath=".", extension=".ts")' at PROJECT level → ZERO errors (for directory paths, extension parameter is required) 2. Run build command → exit 0 3. Run test suite → ALL pass 4. **\`Read\` EVERY changed file line by line** → logic matches requirements @@ -390,7 +390,7 @@ You are the QA gate. Subagents lie. Verify EVERYTHING. - Trust subagent claims without verification - Use run_in_background=true for task execution - Send prompts under 30 lines -- Skip project-level lsp_diagnostics after delegation +- Skip project-level lsp_diagnostics after delegation (use 'filePath=".", extension=".ts"' for TypeScript projects) - Batch multiple tasks in one delegation - Start fresh session for failures/follow-ups - use \`resume\` instead diff --git a/src/agents/atlas/gemini.ts b/src/agents/atlas/gemini.ts index 6ddfa94a0..73048975e 100644 --- a/src/agents/atlas/gemini.ts +++ b/src/agents/atlas/gemini.ts @@ -361,7 +361,7 @@ Subagents CLAIM "done" when: - Trust subagent claims without verification - Use run_in_background=true for task execution - Send prompts under 30 lines -- Skip project-level lsp_diagnostics +- Skip project-level lsp_diagnostics (use 'filePath=".", extension=".ts"' for TypeScript projects) - Batch multiple tasks in one delegation - Start fresh session for failures (use session_id) diff --git a/src/agents/atlas/gpt.ts b/src/agents/atlas/gpt.ts index 64b144b50..2a0fe8c80 100644 --- a/src/agents/atlas/gpt.ts +++ b/src/agents/atlas/gpt.ts @@ -55,7 +55,7 @@ Implementation tasks are the means. Final Wave approval is the goal. - Verification (use Bash for tests/build) - Parallelize independent tool calls when possible. - After ANY delegation, verify with your own tool calls: - 1. \`lsp_diagnostics\` at project level + 1. 'lsp_diagnostics(filePath=".", extension=".ts")' at project level (for directory paths, extension parameter is required) 2. \`Bash\` for build/test commands 3. \`Read\` for changed files @@ -364,7 +364,7 @@ Your job is to CATCH THEM. Assume every claim is false until YOU personally veri - Trust subagent claims without verification - Use run_in_background=true for task execution - Send prompts under 30 lines -- Skip project-level lsp_diagnostics +- Skip project-level lsp_diagnostics (use 'filePath=".", extension=".ts"' for TypeScript projects) - Batch multiple tasks in one delegation - Start fresh session for failures (use session_id) diff --git a/src/tools/lsp/constants.ts b/src/tools/lsp/constants.ts index 5997b01ba..758ff269c 100644 --- a/src/tools/lsp/constants.ts +++ b/src/tools/lsp/constants.ts @@ -1,6 +1,7 @@ export const DEFAULT_MAX_REFERENCES = 200 export const DEFAULT_MAX_SYMBOLS = 200 export const DEFAULT_MAX_DIAGNOSTICS = 200 +export const DEFAULT_MAX_DIRECTORY_FILES = 50 export { SYMBOL_KIND_MAP, SEVERITY_MAP, EXT_TO_LANG } from "./language-mappings" export { BUILTIN_SERVERS, LSP_INSTALL_HINTS } from "./server-definitions" diff --git a/src/tools/lsp/diagnostics-tool.ts b/src/tools/lsp/diagnostics-tool.ts index ffd4fb084..50e007cd7 100644 --- a/src/tools/lsp/diagnostics-tool.ts +++ b/src/tools/lsp/diagnostics-tool.ts @@ -1,21 +1,42 @@ +import { resolve } from "path" + import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { DEFAULT_MAX_DIAGNOSTICS } from "./constants" +import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics" import { filterDiagnosticsBySeverity, formatDiagnostic } from "./lsp-formatters" -import { withLspClient } from "./lsp-client-wrapper" +import { isDirectoryPath, withLspClient } from "./lsp-client-wrapper" import type { Diagnostic } from "./types" export const lsp_diagnostics: ToolDefinition = tool({ - description: "Get errors, warnings, hints from language server BEFORE running build.", + description: + 'Get errors, warnings, hints from language server BEFORE running build. For directories, provide \'extension\' parameter (e.g., extension=".ts").', args: { filePath: tool.schema.string(), severity: tool.schema .enum(["error", "warning", "information", "hint", "all"]) .optional() .describe("Filter by severity level"), + extension: tool.schema + .string() + .optional() + .describe("Required if filePath is a directory. E.g., '.ts', '.py', '.go'"), }, execute: async (args, _context) => { try { + const absPath = resolve(args.filePath) + + if (isDirectoryPath(absPath)) { + if (!args.extension) { + throw new Error( + `Directory path requires 'extension' parameter.\n\n` + + `Example: lsp_diagnostics(filePath="src", extension=".ts")\n\n` + + `Supported extensions: .ts, .tsx, .js, .py, .go, etc.` + ) + } + return await aggregateDiagnosticsForDirectory(absPath, args.extension, args.severity) + } + const result = await withLspClient(args.filePath, async (client) => { return (await client.diagnostics(args.filePath)) as { items?: Diagnostic[] } | Diagnostic[] | null }) diff --git a/src/tools/lsp/directory-diagnostics.test.ts b/src/tools/lsp/directory-diagnostics.test.ts new file mode 100644 index 000000000..4c932e036 --- /dev/null +++ b/src/tools/lsp/directory-diagnostics.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "bun:test" +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs" +import { join } from "path" +import os from "os" + +import { isDirectoryPath } from "./lsp-client-wrapper" +import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics" + +describe("directory diagnostics", () => { + describe("isDirectoryPath", () => { + it("returns true for existing directory", () => { + const tmp = mkdtempSync(join(os.tmpdir(), "omo-isdir-")) + try { + expect(isDirectoryPath(tmp)).toBe(true) + } finally { + rmSync(tmp, { recursive: true, force: true }) + } + }) + + it("returns false for existing file", () => { + const tmp = mkdtempSync(join(os.tmpdir(), "omo-isdir-file-")) + try { + const file = join(tmp, "test.txt") + writeFileSync(file, "content") + expect(isDirectoryPath(file)).toBe(false) + } finally { + rmSync(tmp, { recursive: true, force: true }) + } + }) + + it("returns false for non-existent path", () => { + const nonExistent = join(os.tmpdir(), "omo-nonexistent-" + Date.now()) + expect(isDirectoryPath(nonExistent)).toBe(false) + }) + }) + + describe("aggregateDiagnosticsForDirectory", () => { + it("throws error when extension does not start with dot", async () => { + const tmp = mkdtempSync(join(os.tmpdir(), "omo-aggr-ext-")) + try { + await expect(aggregateDiagnosticsForDirectory(tmp, "ts")).rejects.toThrow( + 'Extension must start with a dot (e.g., ".ts", not "ts")' + ) + } finally { + rmSync(tmp, { recursive: true, force: true }) + } + }) + + it("throws error when directory does not exist", async () => { + const nonExistent = join(os.tmpdir(), "omo-nonexistent-dir-" + Date.now()) + await expect(aggregateDiagnosticsForDirectory(nonExistent, ".ts")).rejects.toThrow( + "Directory does not exist" + ) + }) + }) +}) diff --git a/src/tools/lsp/directory-diagnostics.ts b/src/tools/lsp/directory-diagnostics.ts new file mode 100644 index 000000000..0ac3c754a --- /dev/null +++ b/src/tools/lsp/directory-diagnostics.ts @@ -0,0 +1,144 @@ +import { existsSync, lstatSync, readdirSync, type Stats } from "fs" +import { extname, join, resolve } from "path" + +import { findServerForExtension } from "./config" +import { findWorkspaceRoot, formatServerLookupError } from "./lsp-client-wrapper" +import { filterDiagnosticsBySeverity, formatDiagnostic } from "./lsp-formatters" +import { LSPClient } from "./lsp-client" +import { lspManager } from "./lsp-server" +import { DEFAULT_MAX_DIAGNOSTICS, DEFAULT_MAX_DIRECTORY_FILES } from "./constants" +import type { Diagnostic } from "./types" + +const SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]) + +function collectFilesWithExtension(dir: string, extension: string, maxFiles: number): string[] { + const files: string[] = [] + + function walk(currentDir: string): void { + if (files.length >= maxFiles) return + + let entries: string[] = [] + try { + entries = readdirSync(currentDir) + } catch { + return + } + + for (const entry of entries) { + if (files.length >= maxFiles) return + + const fullPath = join(currentDir, entry) + + let stat: Stats | undefined + try { + stat = lstatSync(fullPath) + } catch { + continue + } + + if (!stat || stat.isSymbolicLink()) { + continue + } + + if (stat.isDirectory()) { + if (!SKIP_DIRECTORIES.has(entry)) { + walk(fullPath) + } + } else if (stat.isFile()) { + if (extname(fullPath) === extension) { + files.push(fullPath) + } + } + } + } + + walk(dir) + return files +} + +export async function aggregateDiagnosticsForDirectory( + directory: string, + extension: string, + severity?: "error" | "warning" | "information" | "hint" | "all", + maxFiles: number = DEFAULT_MAX_DIRECTORY_FILES +): Promise { + if (!extension.startsWith(".")) { + throw new Error( + `Extension must start with a dot (e.g., ".ts", not "${extension}"). ` + + `Use ".${extension}" instead.` + ) + } + + const absDir = resolve(directory) + if (!existsSync(absDir)) { + throw new Error(`Directory does not exist: ${absDir}`) + } + + const serverResult = findServerForExtension(extension) + if (serverResult.status !== "found") { + throw new Error(formatServerLookupError(serverResult)) + } + + const server = serverResult.server + const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1) + const wasCapped = allFiles.length > maxFiles + const filesToProcess = allFiles.slice(0, maxFiles) + + const root = findWorkspaceRoot(absDir) + + const allDiagnostics: Diagnostic[] = [] + const fileErrors: { file: string; error: string }[] = [] + + let client: LSPClient + try { + client = await lspManager.getClient(root, server) + + for (const file of filesToProcess) { + try { + const result = await client.diagnostics(file) + const filtered = filterDiagnosticsBySeverity(result.items, severity) + allDiagnostics.push(...filtered) + } catch (e) { + fileErrors.push({ + file, + error: e instanceof Error ? e.message : String(e), + }) + } + } + } finally { + lspManager.releaseClient(root, server.id) + } + + const displayDiagnostics = allDiagnostics.slice(0, DEFAULT_MAX_DIAGNOSTICS) + const wasDiagCapped = allDiagnostics.length > DEFAULT_MAX_DIAGNOSTICS + + const lines: string[] = [ + `Directory: ${absDir}`, + `Extension: ${extension}`, + `Files scanned: ${filesToProcess.length}${wasCapped ? ` (capped at ${maxFiles})` : ""}`, + `Files with errors: ${fileErrors.length}`, + `Total diagnostics: ${allDiagnostics.length}`, + ] + + if (fileErrors.length > 0) { + lines.push("", "File processing errors:") + for (const { file, error } of fileErrors) { + lines.push(` ${file}: ${error}`) + } + } + + if (displayDiagnostics.length > 0) { + lines.push("") + for (const diag of displayDiagnostics) { + lines.push(formatDiagnostic(diag)) + } + if (wasDiagCapped) { + lines.push( + "", + `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)` + ) + } + } + + return lines.join("\n") +} diff --git a/src/tools/lsp/lsp-client-wrapper.ts b/src/tools/lsp/lsp-client-wrapper.ts index 7ec33847b..0b64e418e 100644 --- a/src/tools/lsp/lsp-client-wrapper.ts +++ b/src/tools/lsp/lsp-client-wrapper.ts @@ -1,6 +1,6 @@ import { extname, resolve } from "path" import { fileURLToPath } from "node:url" -import { existsSync } from "fs" +import { existsSync, statSync } from "fs" import { LSPClient, lspManager } from "./client" import { findServerForExtension } from "./config" @@ -33,6 +33,13 @@ export function uriToPath(uri: string): string { return fileURLToPath(uri) } +export function isDirectoryPath(filePath: string): boolean { + if (!existsSync(filePath)) { + return false + } + return statSync(filePath).isDirectory() +} + export function formatServerLookupError(result: Exclude): string { if (result.status === "not_installed") { const { server, installHint } = result @@ -70,6 +77,14 @@ export function formatServerLookupError(result: Exclude(filePath: string, fn: (client: LSPClient) => Promise): Promise { const absPath = resolve(filePath) + + if (isDirectoryPath(absPath)) { + throw new Error( + `Directory paths are not supported by this LSP tool. ` + + `Use lsp_diagnostics with the 'extension' parameter for directory diagnostics.` + ) + } + const ext = extname(absPath) const result = findServerForExtension(ext)