pi-lens 3.8.25 → 3.8.26
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 +5 -0
- package/clients/dispatch/runners/architect.ts +5 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +12 -4
- package/clients/dispatch/runners/tree-sitter.ts +17 -3
- package/clients/lsp/client.ts +1 -0
- package/clients/lsp/index.ts +193 -21
- package/clients/lsp/server.ts +21 -9
- package/clients/tree-sitter-client.ts +6 -2
- package/index.ts +15 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.8.26] - 2026-04-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Silent crash on unhandled promise rejection** — the LSP crash guard's `unhandledRejection` handler was swallowing all non-ignorable rejections without rethrowing, causing silent process exits. The handler now rethrows so non-ignorable rejections surface as `uncaughtException` and are properly reported. Triggered most visibly when editing JSON files while Biome or another LSP server was active.
|
|
11
|
+
|
|
7
12
|
## [3.8.25] - 2026-04-13
|
|
8
13
|
|
|
9
14
|
### Changed
|
|
@@ -47,7 +47,11 @@ const architectRunner: RunnerDefinition = {
|
|
|
47
47
|
|
|
48
48
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
49
49
|
const relPath = path.relative(ctx.cwd, ctx.filePath).replace(/\\/g, "/");
|
|
50
|
-
const
|
|
50
|
+
const contentFromFacts = ctx.facts.getFileFact<string | null>(
|
|
51
|
+
ctx.filePath,
|
|
52
|
+
"file.content",
|
|
53
|
+
);
|
|
54
|
+
const content = contentFromFacts ?? readFileContent(ctx.filePath);
|
|
51
55
|
|
|
52
56
|
if (!content) {
|
|
53
57
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
@@ -423,10 +423,18 @@ const astGrepNapiRunner: RunnerDefinition = {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
let content: string;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
const contentFromFacts = ctx.facts.getFileFact<string | null>(
|
|
427
|
+
ctx.filePath,
|
|
428
|
+
"file.content",
|
|
429
|
+
);
|
|
430
|
+
if (contentFromFacts !== undefined && contentFromFacts !== null) {
|
|
431
|
+
content = contentFromFacts;
|
|
432
|
+
} else {
|
|
433
|
+
try {
|
|
434
|
+
content = fs.readFileSync(ctx.filePath, "utf-8");
|
|
435
|
+
} catch {
|
|
436
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
437
|
+
}
|
|
430
438
|
}
|
|
431
439
|
|
|
432
440
|
let root: import("@ast-grep/napi").SgRoot;
|
|
@@ -519,13 +519,27 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
const diagnostics: Diagnostic[] = [];
|
|
522
|
+
const contentFromFacts = ctx.facts.getFileFact<string | null>(
|
|
523
|
+
filePath,
|
|
524
|
+
"file.content",
|
|
525
|
+
);
|
|
526
|
+
const contentOverride =
|
|
527
|
+
contentFromFacts !== undefined && contentFromFacts !== null
|
|
528
|
+
? contentFromFacts
|
|
529
|
+
: undefined;
|
|
522
530
|
|
|
523
531
|
// Run each query against the file
|
|
524
532
|
for (const query of effectiveQueries) {
|
|
525
533
|
try {
|
|
526
|
-
const matches = await client.runQueryOnFile(
|
|
527
|
-
|
|
528
|
-
|
|
534
|
+
const matches = await client.runQueryOnFile(
|
|
535
|
+
query,
|
|
536
|
+
filePath,
|
|
537
|
+
languageId,
|
|
538
|
+
{
|
|
539
|
+
maxResults: 10,
|
|
540
|
+
},
|
|
541
|
+
contentOverride,
|
|
542
|
+
);
|
|
529
543
|
|
|
530
544
|
for (const match of matches) {
|
|
531
545
|
// Get line/column from match (already 0-indexed from tree-sitter)
|
package/clients/lsp/client.ts
CHANGED
package/clients/lsp/index.ts
CHANGED
|
@@ -31,6 +31,20 @@ export interface LSPState {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const BROKEN_RETRY_COOLDOWN_MS = 15_000;
|
|
34
|
+
const OPTIONAL_LSP_RETRY_COOLDOWN_MS = 5 * 60_000;
|
|
35
|
+
const OPTIONAL_LSP_SERVER_IDS = new Set(["biome-lsp"]);
|
|
36
|
+
const NAV_CLIENT_WAIT_TIMEOUT_MS = Math.max(
|
|
37
|
+
0,
|
|
38
|
+
Number.parseInt(
|
|
39
|
+
process.env.PI_LENS_LSP_NAV_CLIENT_WAIT_MS ?? "700",
|
|
40
|
+
10,
|
|
41
|
+
) || 700,
|
|
42
|
+
);
|
|
43
|
+
const TOUCH_DEBOUNCE_MS = Math.max(
|
|
44
|
+
0,
|
|
45
|
+
Number.parseInt(process.env.PI_LENS_LSP_TOUCH_DEBOUNCE_MS ?? "1500", 10) ||
|
|
46
|
+
1500,
|
|
47
|
+
);
|
|
34
48
|
const SESSIONSTART_LOG_DIR = path.join(os.homedir(), ".pi-lens");
|
|
35
49
|
const SESSIONSTART_LOG = path.join(SESSIONSTART_LOG_DIR, "sessionstart.log");
|
|
36
50
|
|
|
@@ -57,6 +71,12 @@ export class LSPService {
|
|
|
57
71
|
private workspaceProbeLogged = new Set<string>();
|
|
58
72
|
private warmStartLogged = new Set<string>();
|
|
59
73
|
private emitConsoleLspErrors = process.env.PI_LENS_CONSOLE_LSP === "1";
|
|
74
|
+
private optionalFailureLogged = new Set<string>();
|
|
75
|
+
private optionalDisabled = new Set<string>();
|
|
76
|
+
private recentTouches = new Map<
|
|
77
|
+
string,
|
|
78
|
+
{ fingerprint: string; touchedAt: number; clientScope: "primary" | "all" }
|
|
79
|
+
>();
|
|
60
80
|
|
|
61
81
|
constructor() {
|
|
62
82
|
this.state = {
|
|
@@ -67,11 +87,57 @@ export class LSPService {
|
|
|
67
87
|
};
|
|
68
88
|
}
|
|
69
89
|
|
|
90
|
+
private fingerprintContent(content: string): string {
|
|
91
|
+
if (content.length <= 96) {
|
|
92
|
+
return `${content.length}:${content}`;
|
|
93
|
+
}
|
|
94
|
+
return `${content.length}:${content.slice(0, 48)}:${content.slice(-48)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private shouldSkipTouch(
|
|
98
|
+
filePath: string,
|
|
99
|
+
content: string,
|
|
100
|
+
clientScope: "primary" | "all",
|
|
101
|
+
waitForDiagnostics: boolean,
|
|
102
|
+
): boolean {
|
|
103
|
+
if (waitForDiagnostics || TOUCH_DEBOUNCE_MS <= 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const key = `${normalizeMapKey(filePath)}:${clientScope}`;
|
|
108
|
+
const previous = this.recentTouches.get(key);
|
|
109
|
+
if (!previous) return false;
|
|
110
|
+
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
if (now - previous.touchedAt > TOUCH_DEBOUNCE_MS) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return previous.fingerprint === this.fingerprintContent(content);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private markTouched(
|
|
120
|
+
filePath: string,
|
|
121
|
+
content: string,
|
|
122
|
+
clientScope: "primary" | "all",
|
|
123
|
+
): void {
|
|
124
|
+
const key = `${normalizeMapKey(filePath)}:${clientScope}`;
|
|
125
|
+
this.recentTouches.set(key, {
|
|
126
|
+
fingerprint: this.fingerprintContent(content),
|
|
127
|
+
touchedAt: Date.now(),
|
|
128
|
+
clientScope,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
70
132
|
/**
|
|
71
133
|
* Get or create LSP client for a file
|
|
72
134
|
* Prevents duplicate client creation via in-flight promise tracking
|
|
73
135
|
*/
|
|
74
|
-
async getClientForFile(
|
|
136
|
+
async getClientForFile(
|
|
137
|
+
filePath: string,
|
|
138
|
+
maxWaitMs?: number,
|
|
139
|
+
): Promise<SpawnedServer | undefined> {
|
|
140
|
+
const withBudget = async (): Promise<SpawnedServer | undefined> => {
|
|
75
141
|
const servers = getServersForFileWithConfig(filePath);
|
|
76
142
|
if (servers.length === 0) return undefined;
|
|
77
143
|
|
|
@@ -105,6 +171,32 @@ export class LSPService {
|
|
|
105
171
|
});
|
|
106
172
|
|
|
107
173
|
return undefined;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!maxWaitMs || maxWaitMs <= 0) {
|
|
177
|
+
return withBudget();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const timeoutResult = await Promise.race<SpawnedServer | undefined>([
|
|
181
|
+
withBudget(),
|
|
182
|
+
new Promise<undefined>((resolve) =>
|
|
183
|
+
setTimeout(() => resolve(undefined), maxWaitMs),
|
|
184
|
+
),
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
if (!timeoutResult) {
|
|
188
|
+
logLatency({
|
|
189
|
+
type: "phase",
|
|
190
|
+
phase: "lsp_client_wait_timeout",
|
|
191
|
+
filePath,
|
|
192
|
+
durationMs: maxWaitMs,
|
|
193
|
+
metadata: {
|
|
194
|
+
maxWaitMs,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return timeoutResult;
|
|
108
200
|
}
|
|
109
201
|
|
|
110
202
|
/**
|
|
@@ -131,6 +223,11 @@ export class LSPService {
|
|
|
131
223
|
|
|
132
224
|
const normalizedRoot = normalizeMapKey(root);
|
|
133
225
|
const key = `${server.id}:${normalizedRoot}`;
|
|
226
|
+
const isOptionalServer = OPTIONAL_LSP_SERVER_IDS.has(server.id);
|
|
227
|
+
|
|
228
|
+
if (isOptionalServer && this.optionalDisabled.has(key)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
134
231
|
|
|
135
232
|
const existing = this.state.clients.get(key);
|
|
136
233
|
if (existing) {
|
|
@@ -228,6 +325,7 @@ export class LSPService {
|
|
|
228
325
|
filePath: string,
|
|
229
326
|
allowInstall: boolean,
|
|
230
327
|
): Promise<SpawnedServer | undefined> {
|
|
328
|
+
const isOptionalServer = OPTIONAL_LSP_SERVER_IDS.has(server.id);
|
|
231
329
|
const startedAt = Date.now();
|
|
232
330
|
logSessionStart(
|
|
233
331
|
`lsp spawn ${server.id}: start root=${root} policy=${server.installPolicy ?? "unknown"} install=${allowInstall ? "enabled" : "disabled"} file=${filePath}`,
|
|
@@ -258,6 +356,10 @@ export class LSPService {
|
|
|
258
356
|
};
|
|
259
357
|
|
|
260
358
|
this.state.clients.set(key, client);
|
|
359
|
+
if (isOptionalServer) {
|
|
360
|
+
this.optionalDisabled.delete(key);
|
|
361
|
+
this.optionalFailureLogged.delete(key);
|
|
362
|
+
}
|
|
261
363
|
logSessionStart(
|
|
262
364
|
`lsp spawn ${server.id}: success source=${spawned.source ?? server.installPolicy ?? "unknown"} (${Date.now() - startedAt}ms)`,
|
|
263
365
|
);
|
|
@@ -269,11 +371,16 @@ export class LSPService {
|
|
|
269
371
|
}
|
|
270
372
|
return { client, info: server };
|
|
271
373
|
} catch (err) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
374
|
+
if (!isOptionalServer || !this.optionalFailureLogged.has(key)) {
|
|
375
|
+
logSessionStart(
|
|
376
|
+
`lsp spawn ${server.id}: failed (${Date.now() - startedAt}ms) error=${err instanceof Error ? err.message : String(err)}`,
|
|
377
|
+
);
|
|
378
|
+
if (isOptionalServer) {
|
|
379
|
+
this.optionalFailureLogged.add(key);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
275
382
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
276
|
-
if (this.emitConsoleLspErrors) {
|
|
383
|
+
if (this.emitConsoleLspErrors && !isOptionalServer) {
|
|
277
384
|
if (errorMsg.includes("Timeout")) {
|
|
278
385
|
console.error(
|
|
279
386
|
`[lsp] ${server.id} timed out during initialization (${errorMsg}). The server may be downloading or the project is large. Skipping.`,
|
|
@@ -290,7 +397,13 @@ export class LSPService {
|
|
|
290
397
|
console.error(`[lsp] Failed to spawn ${server.id}:`, err);
|
|
291
398
|
}
|
|
292
399
|
}
|
|
293
|
-
this.state.broken.set(
|
|
400
|
+
this.state.broken.set(
|
|
401
|
+
key,
|
|
402
|
+
Date.now() + (isOptionalServer ? OPTIONAL_LSP_RETRY_COOLDOWN_MS : BROKEN_RETRY_COOLDOWN_MS),
|
|
403
|
+
);
|
|
404
|
+
if (isOptionalServer) {
|
|
405
|
+
this.optionalDisabled.add(key);
|
|
406
|
+
}
|
|
294
407
|
return undefined;
|
|
295
408
|
}
|
|
296
409
|
}
|
|
@@ -326,12 +439,14 @@ export class LSPService {
|
|
|
326
439
|
waitForDiagnostics = false,
|
|
327
440
|
source = "unknown",
|
|
328
441
|
useAllClients = false,
|
|
442
|
+
maxClientWaitMs?: number,
|
|
329
443
|
): Promise<void> {
|
|
330
444
|
const startedAt = Date.now();
|
|
331
445
|
const normalizedPath = normalizeMapKey(filePath);
|
|
446
|
+
const clientScope = useAllClients ? "all" : "primary";
|
|
332
447
|
const spawned = useAllClients
|
|
333
448
|
? await this.getClientsForFile(filePath)
|
|
334
|
-
: await this.getClientForFile(filePath).then((entry) =>
|
|
449
|
+
: await this.getClientForFile(filePath, maxClientWaitMs).then((entry) =>
|
|
335
450
|
entry ? [entry] : [],
|
|
336
451
|
);
|
|
337
452
|
if (spawned.length === 0) {
|
|
@@ -342,13 +457,32 @@ export class LSPService {
|
|
|
342
457
|
durationMs: Date.now() - startedAt,
|
|
343
458
|
metadata: {
|
|
344
459
|
serverCountReady: 0,
|
|
345
|
-
clientScope
|
|
460
|
+
clientScope,
|
|
346
461
|
failureKind: "no_clients",
|
|
347
462
|
},
|
|
348
463
|
});
|
|
349
464
|
return;
|
|
350
465
|
}
|
|
351
466
|
|
|
467
|
+
if (this.shouldSkipTouch(filePath, content, clientScope, waitForDiagnostics)) {
|
|
468
|
+
logLatency({
|
|
469
|
+
type: "phase",
|
|
470
|
+
phase: "lsp_touch_file",
|
|
471
|
+
filePath: normalizedPath,
|
|
472
|
+
durationMs: Date.now() - startedAt,
|
|
473
|
+
metadata: {
|
|
474
|
+
serverCountReady: spawned.length,
|
|
475
|
+
clientScope,
|
|
476
|
+
waitForDiagnostics,
|
|
477
|
+
source,
|
|
478
|
+
failureKind: "success",
|
|
479
|
+
skipped: true,
|
|
480
|
+
reason: "debounced_unchanged_content",
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
352
486
|
const languageId = getLanguageId(filePath) ?? "plaintext";
|
|
353
487
|
await Promise.all(
|
|
354
488
|
spawned.map((entry) => entry.client.notify.open(filePath, content, languageId)),
|
|
@@ -362,6 +496,8 @@ export class LSPService {
|
|
|
362
496
|
);
|
|
363
497
|
}
|
|
364
498
|
|
|
499
|
+
this.markTouched(filePath, content, clientScope);
|
|
500
|
+
|
|
365
501
|
logLatency({
|
|
366
502
|
type: "phase",
|
|
367
503
|
phase: "lsp_touch_file",
|
|
@@ -369,7 +505,7 @@ export class LSPService {
|
|
|
369
505
|
durationMs: Date.now() - startedAt,
|
|
370
506
|
metadata: {
|
|
371
507
|
serverCountReady: spawned.length,
|
|
372
|
-
clientScope
|
|
508
|
+
clientScope,
|
|
373
509
|
waitForDiagnostics,
|
|
374
510
|
source,
|
|
375
511
|
failureKind: "success",
|
|
@@ -471,7 +607,10 @@ export class LSPService {
|
|
|
471
607
|
* Navigation: go to definition
|
|
472
608
|
*/
|
|
473
609
|
async definition(filePath: string, line: number, character: number) {
|
|
474
|
-
const spawned = await this.getClientForFile(
|
|
610
|
+
const spawned = await this.getClientForFile(
|
|
611
|
+
filePath,
|
|
612
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
613
|
+
);
|
|
475
614
|
if (!spawned) return [];
|
|
476
615
|
return spawned.client.definition(filePath, line, character);
|
|
477
616
|
}
|
|
@@ -485,7 +624,10 @@ export class LSPService {
|
|
|
485
624
|
character: number,
|
|
486
625
|
includeDeclaration = true,
|
|
487
626
|
) {
|
|
488
|
-
const spawned = await this.getClientForFile(
|
|
627
|
+
const spawned = await this.getClientForFile(
|
|
628
|
+
filePath,
|
|
629
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
630
|
+
);
|
|
489
631
|
if (!spawned) return [];
|
|
490
632
|
return spawned.client.references(
|
|
491
633
|
filePath,
|
|
@@ -499,7 +641,10 @@ export class LSPService {
|
|
|
499
641
|
* Navigation: hover info
|
|
500
642
|
*/
|
|
501
643
|
async hover(filePath: string, line: number, character: number) {
|
|
502
|
-
const spawned = await this.getClientForFile(
|
|
644
|
+
const spawned = await this.getClientForFile(
|
|
645
|
+
filePath,
|
|
646
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
647
|
+
);
|
|
503
648
|
if (!spawned) return null;
|
|
504
649
|
return spawned.client.hover(filePath, line, character);
|
|
505
650
|
}
|
|
@@ -508,7 +653,10 @@ export class LSPService {
|
|
|
508
653
|
* Navigation: signature help at cursor position
|
|
509
654
|
*/
|
|
510
655
|
async signatureHelp(filePath: string, line: number, character: number) {
|
|
511
|
-
const spawned = await this.getClientForFile(
|
|
656
|
+
const spawned = await this.getClientForFile(
|
|
657
|
+
filePath,
|
|
658
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
659
|
+
);
|
|
512
660
|
if (!spawned) return null;
|
|
513
661
|
return spawned.client.signatureHelp(filePath, line, character);
|
|
514
662
|
}
|
|
@@ -517,7 +665,10 @@ export class LSPService {
|
|
|
517
665
|
* Navigation: symbols in document
|
|
518
666
|
*/
|
|
519
667
|
async documentSymbol(filePath: string) {
|
|
520
|
-
const spawned = await this.getClientForFile(
|
|
668
|
+
const spawned = await this.getClientForFile(
|
|
669
|
+
filePath,
|
|
670
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
671
|
+
);
|
|
521
672
|
if (!spawned) return [];
|
|
522
673
|
return spawned.client.documentSymbol(filePath);
|
|
523
674
|
}
|
|
@@ -527,7 +678,10 @@ export class LSPService {
|
|
|
527
678
|
*/
|
|
528
679
|
async workspaceSymbol(query: string, filePath?: string) {
|
|
529
680
|
if (filePath) {
|
|
530
|
-
const spawned = await this.getClientForFile(
|
|
681
|
+
const spawned = await this.getClientForFile(
|
|
682
|
+
filePath,
|
|
683
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
684
|
+
);
|
|
531
685
|
if (!spawned) return [];
|
|
532
686
|
return spawned.client.workspaceSymbol(query);
|
|
533
687
|
}
|
|
@@ -592,7 +746,10 @@ export class LSPService {
|
|
|
592
746
|
endLine: number,
|
|
593
747
|
endCharacter: number,
|
|
594
748
|
) {
|
|
595
|
-
const spawned = await this.getClientForFile(
|
|
749
|
+
const spawned = await this.getClientForFile(
|
|
750
|
+
filePath,
|
|
751
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
752
|
+
);
|
|
596
753
|
if (!spawned) return [];
|
|
597
754
|
return spawned.client.codeAction(
|
|
598
755
|
filePath,
|
|
@@ -612,7 +769,10 @@ export class LSPService {
|
|
|
612
769
|
character: number,
|
|
613
770
|
newName: string,
|
|
614
771
|
) {
|
|
615
|
-
const spawned = await this.getClientForFile(
|
|
772
|
+
const spawned = await this.getClientForFile(
|
|
773
|
+
filePath,
|
|
774
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
775
|
+
);
|
|
616
776
|
if (!spawned) return null;
|
|
617
777
|
return spawned.client.rename(filePath, line, character, newName);
|
|
618
778
|
}
|
|
@@ -621,7 +781,10 @@ export class LSPService {
|
|
|
621
781
|
* Navigation: go to implementation
|
|
622
782
|
*/
|
|
623
783
|
async implementation(filePath: string, line: number, character: number) {
|
|
624
|
-
const spawned = await this.getClientForFile(
|
|
784
|
+
const spawned = await this.getClientForFile(
|
|
785
|
+
filePath,
|
|
786
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
787
|
+
);
|
|
625
788
|
if (!spawned) return [];
|
|
626
789
|
return spawned.client.implementation(filePath, line, character);
|
|
627
790
|
}
|
|
@@ -634,7 +797,10 @@ export class LSPService {
|
|
|
634
797
|
line: number,
|
|
635
798
|
character: number,
|
|
636
799
|
) {
|
|
637
|
-
const spawned = await this.getClientForFile(
|
|
800
|
+
const spawned = await this.getClientForFile(
|
|
801
|
+
filePath,
|
|
802
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
803
|
+
);
|
|
638
804
|
if (!spawned) return [];
|
|
639
805
|
return spawned.client.prepareCallHierarchy(filePath, line, character);
|
|
640
806
|
}
|
|
@@ -643,7 +809,10 @@ export class LSPService {
|
|
|
643
809
|
* Navigation: find incoming calls (callers)
|
|
644
810
|
*/
|
|
645
811
|
async incomingCalls(item: import("./client.js").LSPCallHierarchyItem) {
|
|
646
|
-
const spawned = await this.getClientForFile(
|
|
812
|
+
const spawned = await this.getClientForFile(
|
|
813
|
+
uriToPath(item.uri),
|
|
814
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
815
|
+
);
|
|
647
816
|
if (!spawned) return [];
|
|
648
817
|
return spawned.client.incomingCalls(item);
|
|
649
818
|
}
|
|
@@ -652,7 +821,10 @@ export class LSPService {
|
|
|
652
821
|
* Navigation: find outgoing calls (callees)
|
|
653
822
|
*/
|
|
654
823
|
async outgoingCalls(item: import("./client.js").LSPCallHierarchyItem) {
|
|
655
|
-
const spawned = await this.getClientForFile(
|
|
824
|
+
const spawned = await this.getClientForFile(
|
|
825
|
+
uriToPath(item.uri),
|
|
826
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
827
|
+
);
|
|
656
828
|
if (!spawned) return [];
|
|
657
829
|
return spawned.client.outgoingCalls(item);
|
|
658
830
|
}
|
package/clients/lsp/server.ts
CHANGED
|
@@ -124,23 +124,35 @@ async function launchWithDirectOrPackageManager(
|
|
|
124
124
|
args: string[],
|
|
125
125
|
options: { cwd: string; env?: NodeJS.ProcessEnv; allowInstall?: boolean },
|
|
126
126
|
): Promise<{ process: LSPProcess; source: "direct" | "package-manager" } | undefined> {
|
|
127
|
+
let lastDirectError: unknown;
|
|
128
|
+
|
|
127
129
|
for (const command of directCommands) {
|
|
128
130
|
try {
|
|
129
131
|
const process = await launchLSP(command, args, options);
|
|
130
132
|
return { process, source: "direct" };
|
|
131
133
|
} catch (error) {
|
|
132
|
-
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
134
|
+
lastDirectError = error;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
try {
|
|
139
|
+
const process = await launchViaPackageManagerWithPolicy(packageName, args, {
|
|
140
|
+
cwd: options.cwd,
|
|
141
|
+
allowInstall: options.allowInstall,
|
|
142
|
+
});
|
|
143
|
+
if (!process) {
|
|
144
|
+
if (lastDirectError && !isCommandNotFoundError(lastDirectError)) {
|
|
145
|
+
throw lastDirectError;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return { process, source: "package-manager" };
|
|
150
|
+
} catch (packageManagerError) {
|
|
151
|
+
if (lastDirectError && !isCommandNotFoundError(lastDirectError)) {
|
|
152
|
+
throw lastDirectError;
|
|
153
|
+
}
|
|
154
|
+
throw packageManagerError;
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
type InitializationConfig = Record<string, unknown>;
|
|
@@ -283,6 +283,7 @@ export class TreeSitterClient {
|
|
|
283
283
|
async parseFile(
|
|
284
284
|
filePath: string,
|
|
285
285
|
languageId: string,
|
|
286
|
+
contentOverride?: string,
|
|
286
287
|
): Promise<TreeSitterTree | null> {
|
|
287
288
|
this.dbg(`Parsing ${filePath} with language ${languageId}`);
|
|
288
289
|
const parser = await this.getParser(languageId);
|
|
@@ -292,7 +293,7 @@ export class TreeSitterClient {
|
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
try {
|
|
295
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
296
|
+
const content = contentOverride ?? fs.readFileSync(filePath, "utf-8");
|
|
296
297
|
this.dbg(`File content length: ${content.length}`);
|
|
297
298
|
|
|
298
299
|
// Check cache first
|
|
@@ -469,6 +470,7 @@ export class TreeSitterClient {
|
|
|
469
470
|
filePath: string,
|
|
470
471
|
languageId: string,
|
|
471
472
|
options: { maxResults?: number } = {},
|
|
473
|
+
contentOverride?: string,
|
|
472
474
|
): Promise<StructuralMatch[]> {
|
|
473
475
|
if (!this.initialized) {
|
|
474
476
|
const ok = await this.init();
|
|
@@ -493,6 +495,7 @@ export class TreeSitterClient {
|
|
|
493
495
|
queryDef.id,
|
|
494
496
|
compiled.postFilter,
|
|
495
497
|
compiled.postFilterParams,
|
|
498
|
+
contentOverride,
|
|
496
499
|
);
|
|
497
500
|
|
|
498
501
|
const maxResults = options.maxResults ?? 50;
|
|
@@ -727,8 +730,9 @@ export class TreeSitterClient {
|
|
|
727
730
|
postFilter?: string,
|
|
728
731
|
// biome-ignore lint/suspicious/noExplicitAny: Post filter params
|
|
729
732
|
postFilterParams?: any,
|
|
733
|
+
contentOverride?: string,
|
|
730
734
|
): Promise<StructuralMatch[]> {
|
|
731
|
-
const tree = await this.parseFile(filePath, languageId);
|
|
735
|
+
const tree = await this.parseFile(filePath, languageId, contentOverride);
|
|
732
736
|
if (!tree) return [];
|
|
733
737
|
|
|
734
738
|
const matches: StructuralMatch[] = [];
|
package/index.ts
CHANGED
|
@@ -59,6 +59,11 @@ function dbg(msg: string) {
|
|
|
59
59
|
let _verbose = false;
|
|
60
60
|
const runtime = new RuntimeCoordinator();
|
|
61
61
|
const _lspConfigInitializedCwds = new Set<string>();
|
|
62
|
+
const LSP_TOOLCALL_NAV_TOUCH_BUDGET_MS = Math.max(
|
|
63
|
+
0,
|
|
64
|
+
Number.parseInt(process.env.PI_LENS_TOOLCALL_NAV_TOUCH_MS ?? "700", 10) ||
|
|
65
|
+
700,
|
|
66
|
+
);
|
|
62
67
|
|
|
63
68
|
function log(msg: string) {
|
|
64
69
|
if (_verbose) console.error(`[pi-lens] ${msg}`);
|
|
@@ -576,8 +581,17 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
576
581
|
if (shouldAutoTouch) {
|
|
577
582
|
try {
|
|
578
583
|
const fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
584
|
+
const maxClientWaitMs =
|
|
585
|
+
toolName === "lsp_navigation" ? LSP_TOOLCALL_NAV_TOUCH_BUDGET_MS : undefined;
|
|
579
586
|
void getLSPService()
|
|
580
|
-
.touchFile(
|
|
587
|
+
.touchFile(
|
|
588
|
+
filePath,
|
|
589
|
+
fileContent,
|
|
590
|
+
false,
|
|
591
|
+
`tool_call:${toolName}`,
|
|
592
|
+
false,
|
|
593
|
+
maxClientWaitMs,
|
|
594
|
+
)
|
|
581
595
|
.catch((err) => dbg(`lsp auto-touch failed for ${filePath}: ${err}`));
|
|
582
596
|
} catch {
|
|
583
597
|
// Best effort only; never block tool calls.
|