codn_ts
Version:
智能代码分析工具 - 支持语义搜索、调用链分析和代码结构可视化,对大模型/AI agent 友好
638 lines • 22.6 kB
JavaScript
"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