feat(lsp): add directory support to lsp_diagnostics via extension param
- Add isDirectoryPath helper to lsp-client-wrapper.ts - Create directory-diagnostics.ts with aggregateDiagnosticsForDirectory - Update diagnostics-tool.ts with extension parameter for directory paths - Update Atlas agent prompts to use extension param for directory diagnostics - Add unit tests for isDirectoryPath and aggregateDiagnosticsForDirectory Fixes #2362
This commit is contained in:
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
</tool_usage_rules>
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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<string> {
|
||||
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")
|
||||
}
|
||||
@@ -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<ServerLookupResult, { status: "found" }>): string {
|
||||
if (result.status === "not_installed") {
|
||||
const { server, installHint } = result
|
||||
@@ -70,6 +77,14 @@ export function formatServerLookupError(result: Exclude<ServerLookupResult, { st
|
||||
|
||||
export async function withLspClient<T>(filePath: string, fn: (client: LSPClient) => Promise<T>): Promise<T> {
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user