UNPKG

codn_ts

Version:

智能代码分析工具 - 支持语义搜索、调用链分析和代码结构可视化,对大模型/AI agent 友好

638 lines 22.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.LSPClient = exports.SymbolKind = exports.DEFAULT_CONFIG = void 0; const child_process_1 = require("child_process"); const events_1 = require("events"); const path = __importStar(require("path")); const url = __importStar(require("url")); var LSPClientState; (function (LSPClientState) { LSPClientState["STOPPED"] = "stopped"; LSPClientState["STARTING"] = "starting"; LSPClientState["RUNNING"] = "running"; LSPClientState["STOPPING"] = "stopping"; })(LSPClientState || (LSPClientState = {})); const DEFAULT_TIMEOUT = 30_000; exports.DEFAULT_CONFIG = { timeout: DEFAULT_TIMEOUT, enableFileWatcher: true, logLevel: "info", lspCommands: { cpp: ["clangd"], c: ["clangd", "--pch-storage=memory"], python: ["pyright-langserver", "--stdio"], typescript: ["typescript-language-server", "--stdio"], }, }; // 完整的LSP符号类型枚举 (来自Language Server Protocol规范) exports.SymbolKind = { File: 1, Module: 2, Namespace: 3, Package: 4, Class: 5, Method: 6, Property: 7, Field: 8, Constructor: 9, Enum: 10, Interface: 11, Function: 12, Variable: 13, Constant: 14, String: 15, Number: 16, Boolean: 17, Array: 18, Object: 19, Key: 20, Null: 21, EnumMember: 22, Struct: 23, Event: 24, Operator: 25, TypeParameter: 26, }; class LSPClient extends events_1.EventEmitter { _rootUri; _config; _state = LSPClientState.STOPPED; _msgId = 1; _pending = new Map(); _proc = null; _stdoutBuffer = Buffer.alloc(0); _openFiles = new Set(); _fileStates = new Map(); _fileVersions = new Map(); _shutdownSignal = new AbortController(); _tasks = new Set(); constructor(_rootUri, _config = exports.DEFAULT_CONFIG) { super(); this._rootUri = _rootUri; this._config = _config; this._setupMessageHandlers(); } get state() { return this._state; } get isClosing() { return [LSPClientState.STOPPING, LSPClientState.STOPPED].includes(this._state); } async start(lang) { if (this._state !== LSPClientState.STOPPED) { throw new Error(`Cannot start client in state: ${this._state}`); } this._state = LSPClientState.STARTING; try { await this._startSubprocess(lang); await this._initialize(); this._state = LSPClientState.RUNNING; this.emit("stateChanged", this._state); } catch (err) { this._state = LSPClientState.STOPPED; await this._cleanup(); throw new Error(`Failed to start LSP client: ${err}`); } } async _startSubprocess(lang) { const commands = this._config.lspCommands[lang]; if (!commands?.length) { throw new Error(`No LSP commands configured for language: ${lang}`); } return new Promise((resolve, reject) => { try { const proc = (0, child_process_1.spawn)(commands[0], commands.slice(1), { stdio: ["pipe", "pipe", "pipe"], }); proc.on("error", (err) => { if (this._state === LSPClientState.STARTING) { reject(new Error(`Failed to start subprocess: ${err}`)); } }); proc.stdout.on("data", (chunk) => this._handleData(chunk)); proc.stderr.on("data", (chunk) => { this._handleLogMessage({ type: 1, message: chunk.toString(), }); }); proc.on("exit", (code) => { if (!this.isClosing) { this.emit("error", new Error(`LSP process crashed with code ${code}`)); } }); this._proc = proc; resolve(); } catch (err) { reject(new Error(`Failed to spawn process: ${err}`)); } }); } async _initialize() { const initParams = { processId: null, rootUri: this._rootUri, capabilities: { textDocument: { synchronization: { dynamicRegistration: true, willSave: true, didSave: true, }, completion: { dynamicRegistration: true }, hover: { dynamicRegistration: true }, definition: { dynamicRegistration: true }, references: { dynamicRegistration: true }, documentSymbol: { dynamicRegistration: true }, }, workspace: { applyEdit: true, workspaceEdit: { documentChanges: true }, didChangeConfiguration: { dynamicRegistration: true }, didChangeWatchedFiles: { dynamicRegistration: true }, }, }, workspaceFolders: [{ uri: this._rootUri, name: "workspace" }], }; await this._request("initialize", initParams); await this._notify("initialized", {}); } _setupMessageHandlers() { this.on("textDocument/publishDiagnostics", (params) => this._handleDiagnostics(params)); this.on("window/logMessage", (params) => this._handleLogMessage(params)); this.on("window/showMessage", (params) => this._handleShowMessage(params)); } _handleData(chunk) { this._stdoutBuffer = Buffer.concat([this._stdoutBuffer, chunk]); while (true) { const headerEnd = this._stdoutBuffer.indexOf("\r\n\r\n"); if (headerEnd === -1) break; const headers = this._stdoutBuffer .subarray(0, headerEnd) .toString() .split("\r\n"); const contentLengthHeader = headers.find((h) => h.startsWith("Content-Length:")); if (!contentLengthHeader) break; const contentLength = parseInt(contentLengthHeader.split(":")[1].trim(), 10); const messageStart = headerEnd + 4; const messageEnd = messageStart + contentLength; if (this._stdoutBuffer.length < messageEnd) break; const messageData = this._stdoutBuffer.subarray(messageStart, messageEnd); this._stdoutBuffer = this._stdoutBuffer.subarray(messageEnd); try { const message = JSON.parse(messageData.toString()); this._handleMessage(message); } catch (err) { this.emit("error", new Error(`Failed to parse message: ${err}`)); } } } _handleMessage(message) { if (message.id !== undefined) { const pending = this._pending.get(Number(message.id)); if (pending) { clearTimeout(pending.timeout); this._pending.delete(Number(message.id)); if (message.error) { pending.reject(new Error(message.error.message)); } else { pending.resolve(message.result); } } } else if (message.method) { this.emit(message.method, message.params); } } _handleDiagnostics(params) { const uri = params.uri; const diagnostics = params.diagnostics || []; if (diagnostics.length > 0) { this.emit("diagnostics", { uri, diagnostics, }); } } _handleLogMessage(params) { const message = params.message || ""; const type = params.type || 3; // Default to INFO const levels = ["error", "warn", "info", "debug"]; const level = levels[Math.min(type - 1, levels.length - 1)] || "info"; this.emit("log", { level, message }); } _handleShowMessage(params) { const message = params.message || ""; const type = params.type || 3; // Default to INFO this.emit("showMessage", { type, message }); } async _send(message) { if (!this._proc || !this._proc.stdin) { throw new Error("LSP process not available"); } return new Promise((resolve, reject) => { const data = JSON.stringify(message); const content = `Content-Length: ${Buffer.byteLength(data)}\r\n\r\n${data}`; this._proc.stdin.write(content, (err) => { if (err) { reject(new Error(`Failed to send message: ${err}`)); } else { resolve(); } }); }); } async _request(method, params, timeout = this._config.timeout) { if (this._state !== LSPClientState.RUNNING && method !== "initialize") { throw new Error(`Cannot send request in state: ${this._state}`); } const id = this._msgId++; const message = { jsonrpc: "2.0", id, method, params, }; return new Promise((resolve, reject) => { const timer = setTimeout(() => { this._pending.delete(id); reject(new Error(`Request ${method} timed out`)); }, timeout); this._pending.set(id, { resolve, reject, timeout: timer, }); this._send(message).catch((err) => { clearTimeout(timer); this._pending.delete(id); reject(err); }); }); } async _notify(method, params) { if (this._state !== LSPClientState.RUNNING && !["initialized", "exit"].includes(method)) { throw new Error(`Cannot send notification in state: ${this._state}`); } await this._send({ jsonrpc: "2.0", method, params, }); } async _manageFileState(uri, action, content = "", languageId = "") { if (action === "open") { if (this._openFiles.has(uri)) { const version = this._fileVersions.get(uri) + 1; this._fileVersions.set(uri, version); this._fileStates.set(uri, { content, languageId, version, status: "open", }); await this._notify("textDocument/didChange", { textDocument: { uri, version, }, contentChanges: [{ text: content }], }); } else { this._openFiles.add(uri); this._fileVersions.set(uri, 1); this._fileStates.set(uri, { content, languageId, version: 1, status: "open", }); await this._notify("textDocument/didOpen", { textDocument: { uri, languageId, version: 1, text: content, }, }); } } else if (action === "change") { if (!this._openFiles.has(uri)) { await this._manageFileState(uri, "open", content, languageId); return; } const version = this._fileVersions.get(uri) + 1; this._fileVersions.set(uri, version); this._fileStates.set(uri, { content, languageId, version, status: "change", }); await this._notify("textDocument/didChange", { textDocument: { uri, version, }, contentChanges: [{ text: content }], }); } else if (action === "close") { if (this._openFiles.has(uri)) { this._openFiles.delete(uri); this._fileVersions.delete(uri); const state = this._fileStates.get(uri); if (state) { this._fileStates.set(uri, { ...state, status: "closed", }); } await this._notify("textDocument/didClose", { textDocument: { uri }, }); } } } async readFile(uri) { const state = this._fileStates.get(uri); return state?.content || ""; } async sendDidOpen(uri, content, languageId) { await this._manageFileState(uri, "open", content, languageId); } async sendDidChange(uri, content) { await this._manageFileState(uri, "change", content); } async sendDidClose(uri) { await this._manageFileState(uri, "close"); } async sendReferences(uri, line, character, name = "", timeout = this._config.timeout) { if (line < 0 || character < 0) { throw new Error("Line and character must be non-negative"); } const startTime = performance.now(); const result = await this._request("textDocument/references", { textDocument: { uri }, position: { line, character }, context: { includeDeclaration: false }, }, timeout); const duration = performance.now() - startTime; return { uri, line, character, name, result, duration, }; } async sendDefinition(uri, line, character) { if (line < 0 || character < 0) { throw new Error("Line and character must be non-negative"); } return this._request("textDocument/definition", { textDocument: { uri }, position: { line, character }, }); } async sendDocumentSymbol(uri, timeout = this._config.timeout) { if (!uri) { throw new Error("URI is required for documentSymbol"); } return this._request("textDocument/documentSymbol", { textDocument: { uri }, }, timeout); } async streamRequests(method, argsList, options = {}) { const { maxConcurrency = 10, showProgress = true, progressEvery = 10, progressInterval = 1000, } = options; const total = argsList.length; const results = new Array(total).fill(null); let completed = 0; let lastPrintTime = 0; const startTime = performance.now(); const semaphore = new Semaphore(maxConcurrency); const worker = async (index, args) => { await semaphore.acquire(); try { const result = await method(...args); results[index] = result; } catch (err) { this.emit("error", new Error(`Request failed at index ${index}: ${err}`)); results[index] = null; } finally { completed++; semaphore.release(); if (showProgress) { const now = performance.now(); if (completed % progressEvery === 0 || now - lastPrintTime >= progressInterval) { lastPrintTime = now; const elapsed = (now - startTime) / 1000; const speed = completed / elapsed; const percent = (completed / total) * 100; const eta = (total - completed) / speed; this.emit("progress", { completed, total, percent, elapsed, speed, eta, }); } } } }; const tasks = argsList.map((args, index) => worker(index, args)); await Promise.all(tasks); return results; } async shutdown() { // 如果已经在关闭或已停止,直接返回 if (this._state === LSPClientState.STOPPING || this._state === LSPClientState.STOPPED) { return; } const wasRunning = this._state === LSPClientState.RUNNING; // 先保存状态 this._state = LSPClientState.STOPPING; this.emit("stateChanged", this._state); try { // 只有在 RUNNING 状态才发送 shutdown 请求 if (wasRunning) { try { await this._request("shutdown", {}, 5000); } catch (err) { // 忽略 shutdown 错误,继续执行关闭流程 this.emit("log", { level: "warn", message: `Shutdown request failed: ${err}`, }); } } // 总是发送 exit 通知 try { await this._notify("exit", {}); } catch (err) { this.emit("log", { level: "warn", message: `Exit notification failed: ${err}`, }); } } finally { // 确保执行清理 await this._cancelTasks(); await this._cleanup(); this._state = LSPClientState.STOPPED; this.emit("stateChanged", this._state); } } async _cancelTasks() { const tasks = Array.from(this._tasks); this._tasks.clear(); for (const task of tasks) { try { await Promise.race([ task, new Promise((_, reject) => setTimeout(() => reject(new Error("Task cancellation timeout")), 5000)), ]); } catch (err) { this.emit("error", new Error(`Error during task cancellation: ${err}`)); } } } async _cleanup() { if (this._proc) { // Close stdin if (this._proc.stdin && !this._proc.stdin.destroyed) { this._proc.stdin.end(); } // Kill process if still running if (this._proc.exitCode === null) { this._proc.kill(); } this._proc = null; } // Clean up state this._openFiles.clear(); this._fileStates.clear(); this._fileVersions.clear(); this._pending.clear(); this._stdoutBuffer = Buffer.alloc(0); } getRelativePath(uri) { const localPath = url.fileURLToPath(uri); const rootPath = url.fileURLToPath(this._rootUri); return path.relative(rootPath, localPath); } async sendHover(uri, position, timeout = this._config.timeout) { if (position.line < 0 || position.character < 0) { throw new Error("Line and character must be non-negative"); } return this._request("textDocument/hover", { textDocument: { uri }, position, }, timeout); } /** * 对单个文件进行 LSP 诊断,返回 diagnostics 数组 */ async diagnoseFile(uri, content, languageId, waitMs = 800) { const diagnostics = []; const seen = new Set(); const handler = ({ uri: diagUri, diagnostics: diags }) => { if (diagUri !== uri) return; for (const d of diags) { const key = `${d.range?.start?.line}:${d.range?.start?.character}:${d.message}`; if (seen.has(key)) continue; seen.add(key); diagnostics.push(d); } }; this.on("diagnostics", handler); await this.sendDidOpen(uri, content, languageId); await new Promise((resolve) => setTimeout(resolve, waitMs)); await this.sendDidClose(uri); this.off("diagnostics", handler); return diagnostics; } } exports.LSPClient = LSPClient; // Helper class for concurrency control class Semaphore { _queue = []; _count; constructor(count) { this._count = count; } acquire() { return new Promise((resolve) => { if (this._count > 0) { this._count--; resolve(); } else { this._queue.push(resolve); } }); } release() { this._count++; const next = this._queue.shift(); if (next) next(); } } //# sourceMappingURL=lsp_core.js.map