@kernel.chat/kbot 2.5.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -42
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +140 -21
- package/dist/agent.js.map +1 -1
- package/dist/architect.d.ts +44 -0
- package/dist/architect.d.ts.map +1 -0
- package/dist/architect.js +403 -0
- package/dist/architect.js.map +1 -0
- package/dist/auth.d.ts +11 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +165 -7
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +251 -22
- package/dist/cli.js.map +1 -1
- package/dist/graph-memory.d.ts +98 -0
- package/dist/graph-memory.d.ts.map +1 -0
- package/dist/graph-memory.js +926 -0
- package/dist/graph-memory.js.map +1 -0
- package/dist/ide/acp-server.js +2 -2
- package/dist/ide/mcp-server.js +1 -1
- package/dist/lsp-client.d.ts +167 -0
- package/dist/lsp-client.d.ts.map +1 -0
- package/dist/lsp-client.js +679 -0
- package/dist/lsp-client.js.map +1 -0
- package/dist/matrix.d.ts +6 -0
- package/dist/matrix.d.ts.map +1 -1
- package/dist/matrix.js +98 -0
- package/dist/matrix.js.map +1 -1
- package/dist/mcp-plugins.d.ts +62 -0
- package/dist/mcp-plugins.d.ts.map +1 -0
- package/dist/mcp-plugins.js +551 -0
- package/dist/mcp-plugins.js.map +1 -0
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +29 -0
- package/dist/planner.js.map +1 -1
- package/dist/provider-fallback.d.ts +51 -0
- package/dist/provider-fallback.d.ts.map +1 -0
- package/dist/provider-fallback.js +237 -0
- package/dist/provider-fallback.js.map +1 -0
- package/dist/repo-map.d.ts +9 -0
- package/dist/repo-map.d.ts.map +1 -0
- package/dist/repo-map.js +280 -0
- package/dist/repo-map.js.map +1 -0
- package/dist/self-eval.d.ts +45 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +232 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +22 -2
- package/dist/streaming.js.map +1 -1
- package/dist/task-ledger.d.ts +71 -0
- package/dist/task-ledger.d.ts.map +1 -0
- package/dist/task-ledger.js +282 -0
- package/dist/task-ledger.js.map +1 -0
- package/dist/tools/computer.js +3 -3
- package/dist/tools/computer.js.map +1 -1
- package/dist/tools/e2b-sandbox.d.ts +2 -0
- package/dist/tools/e2b-sandbox.d.ts.map +1 -0
- package/dist/tools/e2b-sandbox.js +460 -0
- package/dist/tools/e2b-sandbox.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +15 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lsp-tools.d.ts +2 -0
- package/dist/tools/lsp-tools.d.ts.map +1 -0
- package/dist/tools/lsp-tools.js +268 -0
- package/dist/tools/lsp-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +2 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +228 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/quality.d.ts +2 -0
- package/dist/tools/quality.d.ts.map +1 -0
- package/dist/tools/quality.js +313 -0
- package/dist/tools/quality.js.map +1 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +33 -18
- package/dist/ui.js.map +1 -1
- package/package.json +27 -3
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
// K:BOT LSP Client — Full Language Server Protocol integration
|
|
2
|
+
//
|
|
3
|
+
// Persistent LSP client connections for language intelligence:
|
|
4
|
+
// - Go-to-definition, find-references, hover, completions
|
|
5
|
+
// - Rename refactoring, document symbols, diagnostics
|
|
6
|
+
// - Auto-detects and spawns the right LSP server per language
|
|
7
|
+
// - JSON-RPC over stdio with Content-Length framing
|
|
8
|
+
//
|
|
9
|
+
// Unlike the one-shot lsp-bridge.ts (diagnostics only), this module
|
|
10
|
+
// maintains persistent connections for interactive queries.
|
|
11
|
+
import { spawn, execSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { extname, resolve, join } from 'node:path';
|
|
14
|
+
// ── Constants ──
|
|
15
|
+
const REQUEST_TIMEOUT = 10_000; // 10 seconds
|
|
16
|
+
/** LSP server commands by language identifier */
|
|
17
|
+
const SERVER_COMMANDS = {
|
|
18
|
+
typescript: [
|
|
19
|
+
['typescript-language-server', '--stdio'],
|
|
20
|
+
],
|
|
21
|
+
typescriptreact: [
|
|
22
|
+
['typescript-language-server', '--stdio'],
|
|
23
|
+
],
|
|
24
|
+
javascript: [
|
|
25
|
+
['typescript-language-server', '--stdio'],
|
|
26
|
+
],
|
|
27
|
+
javascriptreact: [
|
|
28
|
+
['typescript-language-server', '--stdio'],
|
|
29
|
+
],
|
|
30
|
+
python: [
|
|
31
|
+
['pyright-langserver', '--stdio'],
|
|
32
|
+
['pylsp'],
|
|
33
|
+
],
|
|
34
|
+
go: [
|
|
35
|
+
['gopls', 'serve'],
|
|
36
|
+
],
|
|
37
|
+
rust: [
|
|
38
|
+
['rust-analyzer'],
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
/** Map file extensions to language IDs */
|
|
42
|
+
const EXT_TO_LANGUAGE = {
|
|
43
|
+
'.ts': 'typescript',
|
|
44
|
+
'.tsx': 'typescriptreact',
|
|
45
|
+
'.js': 'javascript',
|
|
46
|
+
'.jsx': 'javascriptreact',
|
|
47
|
+
'.mjs': 'javascript',
|
|
48
|
+
'.cjs': 'javascript',
|
|
49
|
+
'.py': 'python',
|
|
50
|
+
'.pyi': 'python',
|
|
51
|
+
'.go': 'go',
|
|
52
|
+
'.rs': 'rust',
|
|
53
|
+
'.json': 'json',
|
|
54
|
+
'.css': 'css',
|
|
55
|
+
'.html': 'html',
|
|
56
|
+
'.md': 'markdown',
|
|
57
|
+
'.yaml': 'yaml',
|
|
58
|
+
'.yml': 'yaml',
|
|
59
|
+
'.toml': 'toml',
|
|
60
|
+
};
|
|
61
|
+
/** Symbol kind number → human-readable name */
|
|
62
|
+
const SYMBOL_KINDS = {
|
|
63
|
+
1: 'File', 2: 'Module', 3: 'Namespace', 4: 'Package',
|
|
64
|
+
5: 'Class', 6: 'Method', 7: 'Property', 8: 'Field',
|
|
65
|
+
9: 'Constructor', 10: 'Enum', 11: 'Interface', 12: 'Function',
|
|
66
|
+
13: 'Variable', 14: 'Constant', 15: 'String', 16: 'Number',
|
|
67
|
+
17: 'Boolean', 18: 'Array', 19: 'Object', 20: 'Key',
|
|
68
|
+
21: 'Null', 22: 'EnumMember', 23: 'Struct', 24: 'Event',
|
|
69
|
+
25: 'Operator', 26: 'TypeParameter',
|
|
70
|
+
};
|
|
71
|
+
/** Completion item kind number → human-readable name */
|
|
72
|
+
const COMPLETION_KINDS = {
|
|
73
|
+
1: 'Text', 2: 'Method', 3: 'Function', 4: 'Constructor',
|
|
74
|
+
5: 'Field', 6: 'Variable', 7: 'Class', 8: 'Interface',
|
|
75
|
+
9: 'Module', 10: 'Property', 11: 'Unit', 12: 'Value',
|
|
76
|
+
13: 'Enum', 14: 'Keyword', 15: 'Snippet', 16: 'Color',
|
|
77
|
+
17: 'File', 18: 'Reference', 19: 'Folder', 20: 'EnumMember',
|
|
78
|
+
21: 'Constant', 22: 'Struct', 23: 'Event', 24: 'Operator',
|
|
79
|
+
25: 'TypeParameter',
|
|
80
|
+
};
|
|
81
|
+
// ── LSP Connection ──
|
|
82
|
+
class LspConnection {
|
|
83
|
+
language;
|
|
84
|
+
command;
|
|
85
|
+
workspaceRoot;
|
|
86
|
+
process;
|
|
87
|
+
buffer = '';
|
|
88
|
+
nextId = 1;
|
|
89
|
+
pending = new Map();
|
|
90
|
+
notificationHandlers = new Map();
|
|
91
|
+
diagnosticsStore = new Map();
|
|
92
|
+
initialized = false;
|
|
93
|
+
rootUri;
|
|
94
|
+
constructor(language, command, workspaceRoot) {
|
|
95
|
+
this.language = language;
|
|
96
|
+
this.command = command;
|
|
97
|
+
this.workspaceRoot = workspaceRoot;
|
|
98
|
+
this.rootUri = pathToUri(workspaceRoot);
|
|
99
|
+
this.process = spawn(command[0], command.slice(1), {
|
|
100
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
101
|
+
cwd: workspaceRoot,
|
|
102
|
+
});
|
|
103
|
+
this.process.stdout?.on('data', (chunk) => {
|
|
104
|
+
this.buffer += chunk.toString();
|
|
105
|
+
this.drainBuffer();
|
|
106
|
+
});
|
|
107
|
+
this.process.stderr?.on('data', () => {
|
|
108
|
+
// Discard stderr — some LSP servers are noisy
|
|
109
|
+
});
|
|
110
|
+
this.process.on('error', () => {
|
|
111
|
+
// Reject all pending requests
|
|
112
|
+
for (const [id, req] of this.pending) {
|
|
113
|
+
clearTimeout(req.timer);
|
|
114
|
+
req.reject(new Error(`LSP server process error (${this.language})`));
|
|
115
|
+
this.pending.delete(id);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
this.process.on('exit', () => {
|
|
119
|
+
for (const [id, req] of this.pending) {
|
|
120
|
+
clearTimeout(req.timer);
|
|
121
|
+
req.reject(new Error(`LSP server exited unexpectedly (${this.language})`));
|
|
122
|
+
this.pending.delete(id);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Store incoming diagnostics
|
|
126
|
+
this.onNotification('textDocument/publishDiagnostics', (params) => {
|
|
127
|
+
const p = params;
|
|
128
|
+
this.diagnosticsStore.set(p.uri, p.diagnostics);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Parse complete JSON-RPC messages from the buffer */
|
|
132
|
+
drainBuffer() {
|
|
133
|
+
while (true) {
|
|
134
|
+
const headerEnd = this.buffer.indexOf('\r\n\r\n');
|
|
135
|
+
if (headerEnd === -1)
|
|
136
|
+
break;
|
|
137
|
+
const header = this.buffer.slice(0, headerEnd);
|
|
138
|
+
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
139
|
+
if (!lengthMatch) {
|
|
140
|
+
// Malformed header — skip past it
|
|
141
|
+
this.buffer = this.buffer.slice(headerEnd + 4);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const contentLength = parseInt(lengthMatch[1], 10);
|
|
145
|
+
const bodyStart = headerEnd + 4;
|
|
146
|
+
const bodyEnd = bodyStart + contentLength;
|
|
147
|
+
if (this.buffer.length < bodyEnd)
|
|
148
|
+
break; // Incomplete body
|
|
149
|
+
const body = this.buffer.slice(bodyStart, bodyEnd);
|
|
150
|
+
this.buffer = this.buffer.slice(bodyEnd);
|
|
151
|
+
try {
|
|
152
|
+
const msg = JSON.parse(body);
|
|
153
|
+
this.handleMessage(msg);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Skip malformed JSON
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Dispatch an incoming message to the right handler */
|
|
161
|
+
handleMessage(msg) {
|
|
162
|
+
// Response to a request we sent
|
|
163
|
+
if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
164
|
+
const pending = this.pending.get(msg.id);
|
|
165
|
+
if (pending) {
|
|
166
|
+
clearTimeout(pending.timer);
|
|
167
|
+
this.pending.delete(msg.id);
|
|
168
|
+
if (msg.error) {
|
|
169
|
+
pending.reject(new Error(`LSP error ${msg.error.code}: ${msg.error.message}`));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
pending.resolve(msg.result);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Notification from the server
|
|
178
|
+
if (msg.method) {
|
|
179
|
+
const handler = this.notificationHandlers.get(msg.method);
|
|
180
|
+
if (handler)
|
|
181
|
+
handler(msg.params);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Register a handler for a server notification */
|
|
185
|
+
onNotification(method, handler) {
|
|
186
|
+
this.notificationHandlers.set(method, handler);
|
|
187
|
+
}
|
|
188
|
+
/** Send a JSON-RPC request and wait for the response */
|
|
189
|
+
request(method, params) {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
if (!this.process.stdin?.writable) {
|
|
192
|
+
reject(new Error(`LSP server stdin not writable (${this.language})`));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const id = this.nextId++;
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
this.pending.delete(id);
|
|
198
|
+
reject(new Error(`LSP request '${method}' timed out after ${REQUEST_TIMEOUT / 1000}s`));
|
|
199
|
+
}, REQUEST_TIMEOUT);
|
|
200
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
201
|
+
const msg = { jsonrpc: '2.0', id, method, params };
|
|
202
|
+
const body = JSON.stringify(msg);
|
|
203
|
+
const frame = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
|
|
204
|
+
this.process.stdin.write(frame);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Send a JSON-RPC notification (no response expected) */
|
|
208
|
+
notify(method, params) {
|
|
209
|
+
if (!this.process.stdin?.writable)
|
|
210
|
+
return;
|
|
211
|
+
const msg = { jsonrpc: '2.0', method, params };
|
|
212
|
+
const body = JSON.stringify(msg);
|
|
213
|
+
const frame = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
|
|
214
|
+
this.process.stdin.write(frame);
|
|
215
|
+
}
|
|
216
|
+
/** Perform the LSP initialize handshake */
|
|
217
|
+
async initialize() {
|
|
218
|
+
if (this.initialized)
|
|
219
|
+
return;
|
|
220
|
+
await this.request('initialize', {
|
|
221
|
+
processId: process.pid,
|
|
222
|
+
clientInfo: { name: 'kbot', version: '2.7.0' },
|
|
223
|
+
rootUri: this.rootUri,
|
|
224
|
+
workspaceFolders: [{ uri: this.rootUri, name: 'workspace' }],
|
|
225
|
+
capabilities: {
|
|
226
|
+
textDocument: {
|
|
227
|
+
synchronization: {
|
|
228
|
+
didOpen: true,
|
|
229
|
+
didChange: true,
|
|
230
|
+
didClose: true,
|
|
231
|
+
},
|
|
232
|
+
publishDiagnostics: {
|
|
233
|
+
relatedInformation: true,
|
|
234
|
+
tagSupport: { valueSet: [1, 2] },
|
|
235
|
+
},
|
|
236
|
+
completion: {
|
|
237
|
+
completionItem: {
|
|
238
|
+
snippetSupport: false,
|
|
239
|
+
documentationFormat: ['markdown', 'plaintext'],
|
|
240
|
+
resolveSupport: { properties: ['documentation', 'detail'] },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
hover: {
|
|
244
|
+
contentFormat: ['markdown', 'plaintext'],
|
|
245
|
+
},
|
|
246
|
+
definition: { linkSupport: false },
|
|
247
|
+
references: {},
|
|
248
|
+
rename: { prepareSupport: true },
|
|
249
|
+
documentSymbol: {
|
|
250
|
+
hierarchicalDocumentSymbolSupport: true,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
workspace: {
|
|
254
|
+
workspaceFolders: true,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
this.notify('initialized', {});
|
|
259
|
+
this.initialized = true;
|
|
260
|
+
}
|
|
261
|
+
/** Tell the server about an opened file */
|
|
262
|
+
didOpen(uri, languageId, text) {
|
|
263
|
+
this.notify('textDocument/didOpen', {
|
|
264
|
+
textDocument: { uri, languageId, version: 1, text },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** Get stored diagnostics for a URI */
|
|
268
|
+
getDiagnostics(uri) {
|
|
269
|
+
return this.diagnosticsStore.get(uri) || [];
|
|
270
|
+
}
|
|
271
|
+
/** Graceful shutdown */
|
|
272
|
+
async shutdown() {
|
|
273
|
+
try {
|
|
274
|
+
await this.request('shutdown', null);
|
|
275
|
+
this.notify('exit', null);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Force kill if shutdown fails
|
|
279
|
+
}
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
try {
|
|
282
|
+
this.process.kill();
|
|
283
|
+
}
|
|
284
|
+
catch { /* already dead */ }
|
|
285
|
+
}, 500);
|
|
286
|
+
}
|
|
287
|
+
get isAlive() {
|
|
288
|
+
return !this.process.killed && this.process.exitCode === null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ── Server Manager ──
|
|
292
|
+
/** Active LSP connections keyed by language */
|
|
293
|
+
const connections = new Map();
|
|
294
|
+
/** Files already opened on each connection, keyed by language → Set<uri> */
|
|
295
|
+
const openedFiles = new Map();
|
|
296
|
+
/** Check if a binary exists in PATH */
|
|
297
|
+
function binaryExists(name) {
|
|
298
|
+
try {
|
|
299
|
+
execSync(`which ${name}`, { stdio: 'ignore', timeout: 3000 });
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Convert a file path to a file:// URI */
|
|
307
|
+
function pathToUri(filePath) {
|
|
308
|
+
return `file://${resolve(filePath)}`;
|
|
309
|
+
}
|
|
310
|
+
/** Convert a file:// URI to a file path */
|
|
311
|
+
function uriToPath(uri) {
|
|
312
|
+
if (uri.startsWith('file://'))
|
|
313
|
+
return uri.slice(7);
|
|
314
|
+
return uri;
|
|
315
|
+
}
|
|
316
|
+
/** Detect language from file extension */
|
|
317
|
+
export function detectLanguage(filePath) {
|
|
318
|
+
const ext = extname(filePath).toLowerCase();
|
|
319
|
+
return EXT_TO_LANGUAGE[ext] || null;
|
|
320
|
+
}
|
|
321
|
+
/** Find the nearest project root (package.json, Cargo.toml, go.mod, etc.) */
|
|
322
|
+
function findProjectRoot(filePath) {
|
|
323
|
+
const markers = ['package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml', 'setup.py', '.git'];
|
|
324
|
+
let dir = resolve(filePath, '..');
|
|
325
|
+
for (let i = 0; i < 20; i++) {
|
|
326
|
+
for (const marker of markers) {
|
|
327
|
+
if (existsSync(join(dir, marker)))
|
|
328
|
+
return dir;
|
|
329
|
+
}
|
|
330
|
+
const parent = resolve(dir, '..');
|
|
331
|
+
if (parent === dir)
|
|
332
|
+
break;
|
|
333
|
+
dir = parent;
|
|
334
|
+
}
|
|
335
|
+
return process.cwd();
|
|
336
|
+
}
|
|
337
|
+
/** Find a working server command for a language, trying fallbacks */
|
|
338
|
+
function findServerCommand(language) {
|
|
339
|
+
const candidates = SERVER_COMMANDS[language];
|
|
340
|
+
if (candidates) {
|
|
341
|
+
for (const cmd of candidates) {
|
|
342
|
+
if (binaryExists(cmd[0]))
|
|
343
|
+
return cmd;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Fallback: check for a generic `<language>-language-server` binary
|
|
347
|
+
const genericName = `${language}-language-server`;
|
|
348
|
+
if (binaryExists(genericName))
|
|
349
|
+
return [genericName, '--stdio'];
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Start or retrieve an LSP server for a given language.
|
|
354
|
+
* Returns null if no server is available.
|
|
355
|
+
*/
|
|
356
|
+
export async function startLspServer(language) {
|
|
357
|
+
// Return existing connection if alive
|
|
358
|
+
const existing = connections.get(language);
|
|
359
|
+
if (existing?.isAlive)
|
|
360
|
+
return existing;
|
|
361
|
+
// Clean up dead connection
|
|
362
|
+
if (existing) {
|
|
363
|
+
connections.delete(language);
|
|
364
|
+
openedFiles.delete(language);
|
|
365
|
+
}
|
|
366
|
+
const cmd = findServerCommand(language);
|
|
367
|
+
if (!cmd)
|
|
368
|
+
return null;
|
|
369
|
+
const workspaceRoot = process.cwd();
|
|
370
|
+
const conn = new LspConnection(language, cmd, workspaceRoot);
|
|
371
|
+
try {
|
|
372
|
+
await conn.initialize();
|
|
373
|
+
connections.set(language, conn);
|
|
374
|
+
openedFiles.set(language, new Set());
|
|
375
|
+
return conn;
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
try {
|
|
379
|
+
await conn.shutdown();
|
|
380
|
+
}
|
|
381
|
+
catch { /* ignore */ }
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get an LSP connection for a file, auto-detecting the language.
|
|
387
|
+
* Opens the file on the server if not already opened.
|
|
388
|
+
*/
|
|
389
|
+
export async function getConnectionForFile(filePath) {
|
|
390
|
+
const absPath = resolve(filePath);
|
|
391
|
+
const language = detectLanguage(absPath);
|
|
392
|
+
if (!language)
|
|
393
|
+
return null;
|
|
394
|
+
const conn = await startLspServer(language);
|
|
395
|
+
if (!conn)
|
|
396
|
+
return null;
|
|
397
|
+
const uri = pathToUri(absPath);
|
|
398
|
+
const opened = openedFiles.get(language);
|
|
399
|
+
if (!opened.has(uri)) {
|
|
400
|
+
// Read the file and send didOpen
|
|
401
|
+
try {
|
|
402
|
+
const text = readFileSync(absPath, 'utf-8');
|
|
403
|
+
conn.didOpen(uri, language, text);
|
|
404
|
+
opened.add(uri);
|
|
405
|
+
// Give the server a moment to process the file
|
|
406
|
+
await new Promise(r => setTimeout(r, 200));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { conn, uri, language };
|
|
413
|
+
}
|
|
414
|
+
// ── Public API ──
|
|
415
|
+
/**
|
|
416
|
+
* Go to definition at position.
|
|
417
|
+
* Returns location(s) where the symbol is defined.
|
|
418
|
+
*/
|
|
419
|
+
export async function gotoDefinition(filePath, line, character) {
|
|
420
|
+
const ctx = await getConnectionForFile(filePath);
|
|
421
|
+
if (!ctx)
|
|
422
|
+
return [];
|
|
423
|
+
const result = await ctx.conn.request('textDocument/definition', {
|
|
424
|
+
textDocument: { uri: ctx.uri },
|
|
425
|
+
position: { line, character },
|
|
426
|
+
});
|
|
427
|
+
return normalizeLocations(result);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Find all references to the symbol at position.
|
|
431
|
+
*/
|
|
432
|
+
export async function findReferences(filePath, line, character, includeDeclaration = true) {
|
|
433
|
+
const ctx = await getConnectionForFile(filePath);
|
|
434
|
+
if (!ctx)
|
|
435
|
+
return [];
|
|
436
|
+
const result = await ctx.conn.request('textDocument/references', {
|
|
437
|
+
textDocument: { uri: ctx.uri },
|
|
438
|
+
position: { line, character },
|
|
439
|
+
context: { includeDeclaration },
|
|
440
|
+
});
|
|
441
|
+
return normalizeLocations(result);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Get hover information (type, docs) at position.
|
|
445
|
+
*/
|
|
446
|
+
export async function hover(filePath, line, character) {
|
|
447
|
+
const ctx = await getConnectionForFile(filePath);
|
|
448
|
+
if (!ctx)
|
|
449
|
+
return null;
|
|
450
|
+
const result = await ctx.conn.request('textDocument/hover', {
|
|
451
|
+
textDocument: { uri: ctx.uri },
|
|
452
|
+
position: { line, character },
|
|
453
|
+
});
|
|
454
|
+
if (!result?.contents)
|
|
455
|
+
return null;
|
|
456
|
+
return formatHoverContents(result.contents);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Get completions at position.
|
|
460
|
+
*/
|
|
461
|
+
export async function completions(filePath, line, character) {
|
|
462
|
+
const ctx = await getConnectionForFile(filePath);
|
|
463
|
+
if (!ctx)
|
|
464
|
+
return [];
|
|
465
|
+
const result = await ctx.conn.request('textDocument/completion', {
|
|
466
|
+
textDocument: { uri: ctx.uri },
|
|
467
|
+
position: { line, character },
|
|
468
|
+
});
|
|
469
|
+
if (!result)
|
|
470
|
+
return [];
|
|
471
|
+
if (Array.isArray(result))
|
|
472
|
+
return result;
|
|
473
|
+
return result.items || [];
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Rename a symbol across the project.
|
|
477
|
+
* Returns a workspace edit describing all changes.
|
|
478
|
+
*/
|
|
479
|
+
export async function rename(filePath, line, character, newName) {
|
|
480
|
+
const ctx = await getConnectionForFile(filePath);
|
|
481
|
+
if (!ctx)
|
|
482
|
+
return null;
|
|
483
|
+
const result = await ctx.conn.request('textDocument/rename', {
|
|
484
|
+
textDocument: { uri: ctx.uri },
|
|
485
|
+
position: { line, character },
|
|
486
|
+
newName,
|
|
487
|
+
});
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Get diagnostics for a file.
|
|
492
|
+
* Note: diagnostics arrive asynchronously via notifications.
|
|
493
|
+
* This opens the file and waits briefly for diagnostics to arrive.
|
|
494
|
+
*/
|
|
495
|
+
export async function getDiagnostics(filePath) {
|
|
496
|
+
const ctx = await getConnectionForFile(filePath);
|
|
497
|
+
if (!ctx)
|
|
498
|
+
return [];
|
|
499
|
+
// Wait a bit for diagnostics to arrive (they come asynchronously)
|
|
500
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
501
|
+
return ctx.conn.getDiagnostics(ctx.uri);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get document symbols (functions, classes, variables, etc.).
|
|
505
|
+
*/
|
|
506
|
+
export async function documentSymbols(filePath) {
|
|
507
|
+
const ctx = await getConnectionForFile(filePath);
|
|
508
|
+
if (!ctx)
|
|
509
|
+
return [];
|
|
510
|
+
const result = await ctx.conn.request('textDocument/documentSymbol', {
|
|
511
|
+
textDocument: { uri: ctx.uri },
|
|
512
|
+
});
|
|
513
|
+
return result || [];
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Shutdown all active LSP servers.
|
|
517
|
+
*/
|
|
518
|
+
export async function shutdownAll() {
|
|
519
|
+
const shutdowns = [];
|
|
520
|
+
for (const [language, conn] of connections) {
|
|
521
|
+
shutdowns.push(conn.shutdown());
|
|
522
|
+
connections.delete(language);
|
|
523
|
+
openedFiles.delete(language);
|
|
524
|
+
}
|
|
525
|
+
await Promise.allSettled(shutdowns);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Get the list of active LSP server languages.
|
|
529
|
+
*/
|
|
530
|
+
export function getActiveServers() {
|
|
531
|
+
return Array.from(connections.keys()).filter(lang => connections.get(lang)?.isAlive);
|
|
532
|
+
}
|
|
533
|
+
// Register cleanup on process exit
|
|
534
|
+
process.on('exit', () => {
|
|
535
|
+
for (const conn of connections.values()) {
|
|
536
|
+
try {
|
|
537
|
+
conn.shutdown();
|
|
538
|
+
}
|
|
539
|
+
catch { /* best-effort */ }
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
process.on('SIGINT', () => {
|
|
543
|
+
shutdownAll().finally(() => process.exit(0));
|
|
544
|
+
});
|
|
545
|
+
process.on('SIGTERM', () => {
|
|
546
|
+
shutdownAll().finally(() => process.exit(0));
|
|
547
|
+
});
|
|
548
|
+
// ── Formatting Helpers ──
|
|
549
|
+
/** Normalize definition/reference results into a consistent array of locations */
|
|
550
|
+
function normalizeLocations(result) {
|
|
551
|
+
if (!result)
|
|
552
|
+
return [];
|
|
553
|
+
// Single location: { uri, range }
|
|
554
|
+
if (typeof result === 'object' && 'uri' in result) {
|
|
555
|
+
return [result];
|
|
556
|
+
}
|
|
557
|
+
// Array of locations
|
|
558
|
+
if (Array.isArray(result)) {
|
|
559
|
+
return result.filter((item) => item && typeof item === 'object' && 'uri' in item);
|
|
560
|
+
}
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
/** Format hover contents into a readable string */
|
|
564
|
+
function formatHoverContents(contents) {
|
|
565
|
+
if (typeof contents === 'string')
|
|
566
|
+
return contents;
|
|
567
|
+
if (Array.isArray(contents)) {
|
|
568
|
+
return contents
|
|
569
|
+
.map(c => (typeof c === 'string' ? c : c.value))
|
|
570
|
+
.filter(Boolean)
|
|
571
|
+
.join('\n\n');
|
|
572
|
+
}
|
|
573
|
+
if ('value' in contents)
|
|
574
|
+
return contents.value;
|
|
575
|
+
return String(contents);
|
|
576
|
+
}
|
|
577
|
+
/** Format an LspLocation for display */
|
|
578
|
+
export function formatLocation(loc) {
|
|
579
|
+
const path = uriToPath(loc.uri);
|
|
580
|
+
const line = loc.range.start.line + 1;
|
|
581
|
+
const col = loc.range.start.character + 1;
|
|
582
|
+
return `${path}:${line}:${col}`;
|
|
583
|
+
}
|
|
584
|
+
/** Format locations array for display */
|
|
585
|
+
export function formatLocations(locations) {
|
|
586
|
+
if (locations.length === 0)
|
|
587
|
+
return 'No results found.';
|
|
588
|
+
return locations.map(formatLocation).join('\n');
|
|
589
|
+
}
|
|
590
|
+
/** Format a diagnostic severity number to a string */
|
|
591
|
+
export function formatSeverity(severity) {
|
|
592
|
+
switch (severity) {
|
|
593
|
+
case 1: return 'error';
|
|
594
|
+
case 2: return 'warning';
|
|
595
|
+
case 3: return 'info';
|
|
596
|
+
case 4: return 'hint';
|
|
597
|
+
default: return 'unknown';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/** Format diagnostics for display */
|
|
601
|
+
export function formatDiagnosticsList(filePath, diagnostics) {
|
|
602
|
+
if (diagnostics.length === 0)
|
|
603
|
+
return 'No diagnostics.';
|
|
604
|
+
const errors = diagnostics.filter(d => d.severity === 1);
|
|
605
|
+
const warnings = diagnostics.filter(d => d.severity === 2);
|
|
606
|
+
const others = diagnostics.filter(d => !d.severity || d.severity > 2);
|
|
607
|
+
const lines = [];
|
|
608
|
+
if (errors.length > 0) {
|
|
609
|
+
lines.push(`${errors.length} error(s):`);
|
|
610
|
+
for (const d of errors) {
|
|
611
|
+
const loc = `${filePath}:${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
|
612
|
+
lines.push(` ${loc} — ${d.message}${d.source ? ` [${d.source}]` : ''}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (warnings.length > 0) {
|
|
616
|
+
lines.push(`${warnings.length} warning(s):`);
|
|
617
|
+
for (const d of warnings) {
|
|
618
|
+
const loc = `${filePath}:${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
|
619
|
+
lines.push(` ${loc} — ${d.message}${d.source ? ` [${d.source}]` : ''}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (others.length > 0) {
|
|
623
|
+
lines.push(`${others.length} info/hint(s):`);
|
|
624
|
+
for (const d of others) {
|
|
625
|
+
const loc = `${filePath}:${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
|
626
|
+
lines.push(` ${loc} — ${d.message}${d.source ? ` [${d.source}]` : ''}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return lines.join('\n');
|
|
630
|
+
}
|
|
631
|
+
/** Format a symbol for display, with indentation for hierarchy */
|
|
632
|
+
export function formatSymbol(sym, indent = 0) {
|
|
633
|
+
const kind = SYMBOL_KINDS[sym.kind] || `kind(${sym.kind})`;
|
|
634
|
+
const line = sym.range.start.line + 1;
|
|
635
|
+
const prefix = ' '.repeat(indent);
|
|
636
|
+
let result = `${prefix}${kind} ${sym.name} (line ${line})`;
|
|
637
|
+
if (sym.children?.length) {
|
|
638
|
+
for (const child of sym.children) {
|
|
639
|
+
result += '\n' + formatSymbol(child, indent + 1);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
/** Format a completion item for display */
|
|
645
|
+
export function formatCompletion(item) {
|
|
646
|
+
const kind = item.kind ? COMPLETION_KINDS[item.kind] || `kind(${item.kind})` : '';
|
|
647
|
+
const detail = item.detail ? ` — ${item.detail}` : '';
|
|
648
|
+
return `${item.label}${kind ? ` (${kind})` : ''}${detail}`;
|
|
649
|
+
}
|
|
650
|
+
/** Format workspace edits into a human-readable summary */
|
|
651
|
+
export function formatWorkspaceEdit(edit) {
|
|
652
|
+
const lines = [];
|
|
653
|
+
if (edit.changes) {
|
|
654
|
+
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
655
|
+
const path = uriToPath(uri);
|
|
656
|
+
lines.push(`${path}: ${edits.length} edit(s)`);
|
|
657
|
+
for (const e of edits) {
|
|
658
|
+
const loc = ` line ${e.range.start.line + 1}:${e.range.start.character + 1}`;
|
|
659
|
+
const preview = e.newText.length > 60 ? e.newText.slice(0, 60) + '...' : e.newText;
|
|
660
|
+
lines.push(`${loc} → "${preview}"`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (edit.documentChanges) {
|
|
665
|
+
for (const change of edit.documentChanges) {
|
|
666
|
+
const path = uriToPath(change.textDocument.uri);
|
|
667
|
+
lines.push(`${path}: ${change.edits.length} edit(s)`);
|
|
668
|
+
for (const e of change.edits) {
|
|
669
|
+
const loc = ` line ${e.range.start.line + 1}:${e.range.start.character + 1}`;
|
|
670
|
+
const preview = e.newText.length > 60 ? e.newText.slice(0, 60) + '...' : e.newText;
|
|
671
|
+
lines.push(`${loc} → "${preview}"`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (lines.length === 0)
|
|
676
|
+
return 'No edits.';
|
|
677
|
+
return lines.join('\n');
|
|
678
|
+
}
|
|
679
|
+
//# sourceMappingURL=lsp-client.js.map
|