pi-lens 3.6.6 → 3.6.7
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/CHANGELOG.md +54 -0
- package/README.md +39 -2
- package/clients/dispatch/runners/similarity.ts +100 -1
- package/clients/installer/index.ts +26 -20
- package/clients/lsp/client.ts +249 -110
- package/clients/native-rust-client.ts +531 -0
- package/commands/booboo.ts +2 -2
- package/package.json +14 -2
- package/rust/Cargo.toml +34 -0
- package/rust/src/cache.rs +127 -0
- package/rust/src/index.rs +407 -0
- package/rust/src/lib.rs +209 -0
- package/rust/src/main.rs +24 -0
- package/rust/src/scan.rs +116 -0
- package/rust/src/similarity.rs +387 -0
- package/skills/ast-grep/SKILL.md +16 -4
package/clients/lsp/client.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface LSPClientInfo {
|
|
|
83
83
|
serverId: string;
|
|
84
84
|
root: string;
|
|
85
85
|
connection: MessageConnection;
|
|
86
|
+
/** Check if the connection is still alive */
|
|
87
|
+
isAlive: () => boolean;
|
|
86
88
|
notify: {
|
|
87
89
|
open(filePath: string, content: string, languageId: string): Promise<void>;
|
|
88
90
|
change(filePath: string, content: string): Promise<void>;
|
|
@@ -127,9 +129,13 @@ export interface LSPClientInfo {
|
|
|
127
129
|
character: number,
|
|
128
130
|
): Promise<LSPCallHierarchyItem[]>;
|
|
129
131
|
/** Find incoming calls (callers) */
|
|
130
|
-
incomingCalls(
|
|
132
|
+
incomingCalls(
|
|
133
|
+
item: LSPCallHierarchyItem,
|
|
134
|
+
): Promise<LSPCallHierarchyIncomingCall[]>;
|
|
131
135
|
/** Find outgoing calls (callees) */
|
|
132
|
-
outgoingCalls(
|
|
136
|
+
outgoingCalls(
|
|
137
|
+
item: LSPCallHierarchyItem,
|
|
138
|
+
): Promise<LSPCallHierarchyOutgoingCall[]>;
|
|
133
139
|
shutdown(): Promise<void>;
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -148,6 +154,46 @@ export async function createLSPClient(options: {
|
|
|
148
154
|
}): Promise<LSPClientInfo> {
|
|
149
155
|
const { serverId, process: lspProcess, root, initialization } = options;
|
|
150
156
|
|
|
157
|
+
// Attach persistent 'error' listeners to all three stdio streams.
|
|
158
|
+
//
|
|
159
|
+
// Why: when the LSP process exits, Node.js destroys its stdio streams and
|
|
160
|
+
// may emit 'error' (ERR_STREAM_DESTROYED / EPIPE / ECONNRESET) on them.
|
|
161
|
+
// Without a listener that becomes an uncaught exception.
|
|
162
|
+
//
|
|
163
|
+
// vscode-jsonrpc attaches its own error listeners to stdin/stdout via
|
|
164
|
+
// WritableStreamWrapper / ReadableStreamWrapper, but those listeners are
|
|
165
|
+
// removed when connection.dispose() is called. Our listeners are permanent
|
|
166
|
+
// so they cover the window between dispose() and process.kill(), as well as
|
|
167
|
+
// any cases where the process dies before the connection is set up.
|
|
168
|
+
//
|
|
169
|
+
// stderr: nobody else ever attaches an error listener here.
|
|
170
|
+
// stdout: vscode-jsonrpc covers it during the connection lifetime, but not
|
|
171
|
+
// after dispose(). After dispose() the stream is about to be
|
|
172
|
+
// destroyed anyway, so we just swallow the error.
|
|
173
|
+
const streamErrorHandler =
|
|
174
|
+
(label: string) => (err: Error & { code?: string }) => {
|
|
175
|
+
if (
|
|
176
|
+
err.code === "ERR_STREAM_DESTROYED" ||
|
|
177
|
+
err.code === "EPIPE" ||
|
|
178
|
+
err.code === "ECONNRESET"
|
|
179
|
+
)
|
|
180
|
+
return;
|
|
181
|
+
console.error(`[lsp] ${serverId} ${label} stream error:`, err.message);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
(lspProcess.stdin as NodeJS.WritableStream).on(
|
|
185
|
+
"error",
|
|
186
|
+
streamErrorHandler("stdin"),
|
|
187
|
+
);
|
|
188
|
+
(lspProcess.stdout as NodeJS.ReadableStream).on(
|
|
189
|
+
"error",
|
|
190
|
+
streamErrorHandler("stdout"),
|
|
191
|
+
);
|
|
192
|
+
(lspProcess.stderr as NodeJS.ReadableStream).on(
|
|
193
|
+
"error",
|
|
194
|
+
streamErrorHandler("stderr"),
|
|
195
|
+
);
|
|
196
|
+
|
|
151
197
|
// Create JSON-RPC connection
|
|
152
198
|
const connection = createMessageConnection(
|
|
153
199
|
new StreamMessageReader(lspProcess.stdout),
|
|
@@ -205,9 +251,41 @@ export async function createLSPClient(options: {
|
|
|
205
251
|
// Start listening
|
|
206
252
|
connection.listen();
|
|
207
253
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
254
|
+
// Track connection state
|
|
255
|
+
let isConnected = true;
|
|
256
|
+
let lastError: Error | undefined;
|
|
257
|
+
let isDestroyed = false;
|
|
258
|
+
|
|
259
|
+
// Handle connection errors and close events
|
|
260
|
+
connection.onError((error) => {
|
|
261
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
262
|
+
isConnected = false;
|
|
263
|
+
isDestroyed = true;
|
|
264
|
+
console.error(`[lsp] ${serverId} connection error:`, lastError.message);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
connection.onClose(() => {
|
|
268
|
+
isConnected = false;
|
|
269
|
+
isDestroyed = true;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Also handle process exit to catch crashes immediately
|
|
273
|
+
lspProcess.process.on("exit", (code) => {
|
|
274
|
+
if (code !== 0 && code !== null) {
|
|
275
|
+
isConnected = false;
|
|
276
|
+
isDestroyed = true;
|
|
277
|
+
console.error(`[lsp] ${serverId} process exited with code ${code}`);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Helper to check if process is still alive before operations
|
|
282
|
+
function isProcessAlive(): boolean {
|
|
283
|
+
return isConnected && !isDestroyed && !lspProcess.process.killed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Send initialize request with error handling
|
|
287
|
+
const initResult = await withTimeout(
|
|
288
|
+
safeSendRequest(connection, "initialize", {
|
|
211
289
|
processId: process.pid,
|
|
212
290
|
rootUri: pathToFileURL(root).href,
|
|
213
291
|
workspaceFolders: [
|
|
@@ -242,12 +320,19 @@ export async function createLSPClient(options: {
|
|
|
242
320
|
INITIALIZE_TIMEOUT_MS,
|
|
243
321
|
);
|
|
244
322
|
|
|
323
|
+
if (initResult === undefined) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`[lsp] ${serverId} failed to initialize - stream may have been destroyed. ` +
|
|
326
|
+
`The server binary may be missing or crashed immediately. Try reinstalling: npm install -g ${serverId}-language-server`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
245
330
|
// Send initialized notification
|
|
246
|
-
await connection
|
|
331
|
+
await safeSendNotification(connection, "initialized", {});
|
|
247
332
|
|
|
248
333
|
// Send configuration if provided (helps pyright and other servers)
|
|
249
334
|
if (initialization) {
|
|
250
|
-
await connection
|
|
335
|
+
await safeSendNotification(connection, "workspace/didChangeConfiguration", {
|
|
251
336
|
settings: initialization,
|
|
252
337
|
});
|
|
253
338
|
}
|
|
@@ -259,9 +344,11 @@ export async function createLSPClient(options: {
|
|
|
259
344
|
serverId,
|
|
260
345
|
root,
|
|
261
346
|
connection,
|
|
347
|
+
isAlive: () => isProcessAlive(),
|
|
262
348
|
|
|
263
349
|
notify: {
|
|
264
350
|
async open(filePath, content, languageId) {
|
|
351
|
+
if (!isProcessAlive()) return;
|
|
265
352
|
const uri = pathToFileURL(filePath).href;
|
|
266
353
|
// Normalize path for Windows case-insensitive lookup
|
|
267
354
|
const normalizedPath = normalizeMapKey(filePath);
|
|
@@ -269,16 +356,22 @@ export async function createLSPClient(options: {
|
|
|
269
356
|
diagnostics.delete(normalizedPath); // Clear stale diagnostics
|
|
270
357
|
|
|
271
358
|
// Send workspace notification first (like opencode does)
|
|
272
|
-
await
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
359
|
+
await safeSendNotification(
|
|
360
|
+
connection,
|
|
361
|
+
"workspace/didChangeWatchedFiles",
|
|
362
|
+
{
|
|
363
|
+
changes: [
|
|
364
|
+
{
|
|
365
|
+
uri,
|
|
366
|
+
type: 1, // Created
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
);
|
|
280
371
|
|
|
281
|
-
|
|
372
|
+
if (!isProcessAlive()) return;
|
|
373
|
+
|
|
374
|
+
await safeSendNotification(connection, "textDocument/didOpen", {
|
|
282
375
|
textDocument: {
|
|
283
376
|
uri,
|
|
284
377
|
languageId,
|
|
@@ -289,13 +382,14 @@ export async function createLSPClient(options: {
|
|
|
289
382
|
},
|
|
290
383
|
|
|
291
384
|
async change(filePath, content) {
|
|
385
|
+
if (!isProcessAlive()) return;
|
|
292
386
|
const uri = pathToFileURL(filePath).href;
|
|
293
387
|
// Normalize path for Windows case-insensitive lookup
|
|
294
388
|
const normalizedPath = normalizeMapKey(filePath);
|
|
295
389
|
const version = (documentVersions.get(normalizedPath) ?? 0) + 1;
|
|
296
390
|
documentVersions.set(normalizedPath, version);
|
|
297
391
|
|
|
298
|
-
await connection
|
|
392
|
+
await safeSendNotification(connection, "textDocument/didChange", {
|
|
299
393
|
textDocument: { uri, version },
|
|
300
394
|
contentChanges: [{ text: content }],
|
|
301
395
|
});
|
|
@@ -350,138 +444,130 @@ export async function createLSPClient(options: {
|
|
|
350
444
|
},
|
|
351
445
|
|
|
352
446
|
async definition(filePath, line, character) {
|
|
447
|
+
if (!isProcessAlive()) return [];
|
|
353
448
|
const uri = pathToFileURL(filePath).href;
|
|
354
|
-
|
|
355
|
-
|
|
449
|
+
const result = await safeSendRequest<LSPLocation | LSPLocation[]>(
|
|
450
|
+
connection,
|
|
451
|
+
"textDocument/definition",
|
|
452
|
+
{
|
|
356
453
|
textDocument: { uri },
|
|
357
454
|
position: { line, character },
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
return [];
|
|
363
|
-
}
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
if (!result) return [];
|
|
458
|
+
return Array.isArray(result) ? result : [result];
|
|
364
459
|
},
|
|
365
460
|
|
|
366
461
|
async references(filePath, line, character, includeDeclaration = true) {
|
|
462
|
+
if (!isProcessAlive()) return [];
|
|
367
463
|
const uri = pathToFileURL(filePath).href;
|
|
368
|
-
|
|
369
|
-
|
|
464
|
+
const result = await safeSendRequest<LSPLocation[]>(
|
|
465
|
+
connection,
|
|
466
|
+
"textDocument/references",
|
|
467
|
+
{
|
|
370
468
|
textDocument: { uri },
|
|
371
469
|
position: { line, character },
|
|
372
470
|
context: { includeDeclaration },
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return [];
|
|
377
|
-
}
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
return result ?? [];
|
|
378
474
|
},
|
|
379
475
|
|
|
380
476
|
async hover(filePath, line, character) {
|
|
477
|
+
if (!isProcessAlive()) return null;
|
|
381
478
|
const uri = pathToFileURL(filePath).href;
|
|
382
|
-
|
|
383
|
-
|
|
479
|
+
const result = await safeSendRequest<LSPHover>(
|
|
480
|
+
connection,
|
|
481
|
+
"textDocument/hover",
|
|
482
|
+
{
|
|
384
483
|
textDocument: { uri },
|
|
385
484
|
position: { line, character },
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
return result ?? null;
|
|
390
488
|
},
|
|
391
489
|
|
|
392
490
|
async documentSymbol(filePath) {
|
|
491
|
+
if (!isProcessAlive()) return [];
|
|
393
492
|
const uri = pathToFileURL(filePath).href;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
} catch {
|
|
403
|
-
return [];
|
|
404
|
-
}
|
|
493
|
+
const result = await safeSendRequest<LSPSymbol[]>(
|
|
494
|
+
connection,
|
|
495
|
+
"textDocument/documentSymbol",
|
|
496
|
+
{
|
|
497
|
+
textDocument: { uri },
|
|
498
|
+
},
|
|
499
|
+
);
|
|
500
|
+
return result ?? [];
|
|
405
501
|
},
|
|
406
502
|
|
|
407
503
|
async workspaceSymbol(query) {
|
|
408
|
-
|
|
409
|
-
|
|
504
|
+
if (!isProcessAlive()) return [];
|
|
505
|
+
const result = await safeSendRequest<LSPSymbol[]>(
|
|
506
|
+
connection,
|
|
507
|
+
"workspace/symbol",
|
|
508
|
+
{
|
|
410
509
|
query,
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return [];
|
|
415
|
-
}
|
|
510
|
+
},
|
|
511
|
+
);
|
|
512
|
+
return result ?? [];
|
|
416
513
|
},
|
|
417
514
|
|
|
418
515
|
async implementation(filePath, line, character) {
|
|
516
|
+
if (!isProcessAlive()) return [];
|
|
419
517
|
const uri = pathToFileURL(filePath).href;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
} catch {
|
|
431
|
-
return [];
|
|
432
|
-
}
|
|
518
|
+
const result = await safeSendRequest<LSPLocation | LSPLocation[]>(
|
|
519
|
+
connection,
|
|
520
|
+
"textDocument/implementation",
|
|
521
|
+
{
|
|
522
|
+
textDocument: { uri },
|
|
523
|
+
position: { line, character },
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
if (!result) return [];
|
|
527
|
+
return Array.isArray(result) ? result : [result];
|
|
433
528
|
},
|
|
434
529
|
|
|
435
530
|
// --- Call Hierarchy Methods ---
|
|
436
531
|
|
|
437
532
|
async prepareCallHierarchy(filePath, line, character) {
|
|
533
|
+
if (!isProcessAlive()) return [];
|
|
438
534
|
const uri = pathToFileURL(filePath).href;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (!result) return [];
|
|
448
|
-
return Array.isArray(result) ? result : [result];
|
|
449
|
-
} catch {
|
|
450
|
-
return [];
|
|
451
|
-
}
|
|
535
|
+
const result = await safeSendRequest<
|
|
536
|
+
LSPCallHierarchyItem | LSPCallHierarchyItem[]
|
|
537
|
+
>(connection, "textDocument/prepareCallHierarchy", {
|
|
538
|
+
textDocument: { uri },
|
|
539
|
+
position: { line, character },
|
|
540
|
+
});
|
|
541
|
+
if (!result) return [];
|
|
542
|
+
return Array.isArray(result) ? result : [result];
|
|
452
543
|
},
|
|
453
544
|
|
|
454
545
|
async incomingCalls(item) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
} catch {
|
|
465
|
-
return [];
|
|
466
|
-
}
|
|
546
|
+
if (!isProcessAlive()) return [];
|
|
547
|
+
const result = await safeSendRequest<LSPCallHierarchyIncomingCall[]>(
|
|
548
|
+
connection,
|
|
549
|
+
"callHierarchy/incomingCalls",
|
|
550
|
+
{
|
|
551
|
+
item,
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
return result ?? [];
|
|
467
555
|
},
|
|
468
556
|
|
|
469
557
|
async outgoingCalls(item) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
} catch {
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
558
|
+
if (!isProcessAlive()) return [];
|
|
559
|
+
const result = await safeSendRequest<LSPCallHierarchyOutgoingCall[]>(
|
|
560
|
+
connection,
|
|
561
|
+
"callHierarchy/outgoingCalls",
|
|
562
|
+
{
|
|
563
|
+
item,
|
|
564
|
+
},
|
|
565
|
+
);
|
|
566
|
+
return result ?? [];
|
|
482
567
|
},
|
|
483
568
|
|
|
484
569
|
async shutdown() {
|
|
570
|
+
isConnected = false;
|
|
485
571
|
// Clear pending timers
|
|
486
572
|
for (const timer of pendingDiagnostics.values()) {
|
|
487
573
|
clearTimeout(timer);
|
|
@@ -491,21 +577,74 @@ export async function createLSPClient(options: {
|
|
|
491
577
|
// Remove all diagnostic listeners (cancels any in-flight waitForDiagnostics)
|
|
492
578
|
diagnosticEmitter.removeAllListeners();
|
|
493
579
|
|
|
494
|
-
// Graceful shutdown
|
|
580
|
+
// Graceful shutdown - ignore errors from destroyed streams
|
|
581
|
+
try {
|
|
582
|
+
await safeSendRequest(connection, "shutdown", {});
|
|
583
|
+
} catch {
|
|
584
|
+
/* ignore */
|
|
585
|
+
}
|
|
495
586
|
try {
|
|
496
|
-
await connection
|
|
497
|
-
await connection.sendNotification("exit");
|
|
587
|
+
await safeSendNotification(connection, "exit", {});
|
|
498
588
|
} catch {
|
|
499
589
|
/* ignore */
|
|
500
590
|
}
|
|
501
591
|
|
|
592
|
+
connection.end();
|
|
502
593
|
connection.dispose();
|
|
503
594
|
lspProcess.process.kill();
|
|
504
595
|
},
|
|
505
596
|
};
|
|
506
597
|
}
|
|
507
598
|
|
|
508
|
-
//
|
|
599
|
+
// Helper to safely send notifications - catches stream destruction
|
|
600
|
+
async function safeSendNotification(
|
|
601
|
+
connection: MessageConnection,
|
|
602
|
+
method: string,
|
|
603
|
+
params: unknown,
|
|
604
|
+
): Promise<void> {
|
|
605
|
+
try {
|
|
606
|
+
await connection.sendNotification(method as never, params as never);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
if (isStreamError(err)) {
|
|
609
|
+
// Silently ignore - stream was destroyed, connection error handlers will update state
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Helper to safely send requests - catches stream destruction
|
|
617
|
+
async function safeSendRequest<T>(
|
|
618
|
+
connection: MessageConnection,
|
|
619
|
+
method: string,
|
|
620
|
+
params: unknown,
|
|
621
|
+
): Promise<T | undefined> {
|
|
622
|
+
try {
|
|
623
|
+
return (await connection.sendRequest(
|
|
624
|
+
method as never,
|
|
625
|
+
params as never,
|
|
626
|
+
)) as T;
|
|
627
|
+
} catch (err) {
|
|
628
|
+
if (isStreamError(err)) {
|
|
629
|
+
// Silently ignore - stream was destroyed
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
throw err;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Helper to detect stream destruction errors
|
|
637
|
+
function isStreamError(err: unknown): boolean {
|
|
638
|
+
if (!(err instanceof Error)) return false;
|
|
639
|
+
const msg = err.message.toLowerCase();
|
|
640
|
+
return (
|
|
641
|
+
msg.includes("stream") ||
|
|
642
|
+
msg.includes("destroyed") ||
|
|
643
|
+
msg.includes("closed") ||
|
|
644
|
+
(err as { code?: string }).code === "ERR_STREAM_DESTROYED" ||
|
|
645
|
+
(err as { code?: string }).code === "EPIPE"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
509
648
|
|
|
510
649
|
// Using shared path utilities from path-utils.ts
|
|
511
650
|
|