pi-ui-extend 0.1.27 → 0.1.29
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/dist/app/screen/file-link-opener.js +5 -0
- package/dist/app/screen/file-links.js +13 -1
- package/dist/app/screen/mouse-controller.d.ts +1 -0
- package/dist/app/screen/mouse-controller.js +20 -6
- package/extensions/session-title/index.ts +8 -19
- package/external/pi-tools-suite/src/lsp/constants.ts +1 -0
- package/external/pi-tools-suite/src/lsp/manager.ts +120 -71
- package/package.json +1 -1
|
@@ -11,6 +11,8 @@ export function setFileLinkOpenerTestDeps(overrides) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
export function openFileLink(link) {
|
|
14
|
+
if (isWebUrl(link.url))
|
|
15
|
+
return openPathWithSystemViewer(link.url);
|
|
14
16
|
const filePath = link.filePath ?? filePathFromUrl(link.url);
|
|
15
17
|
if (!filePath)
|
|
16
18
|
return false;
|
|
@@ -19,6 +21,9 @@ export function openFileLink(link) {
|
|
|
19
21
|
return true;
|
|
20
22
|
return openPathWithSystemViewer(filePath);
|
|
21
23
|
}
|
|
24
|
+
function isWebUrl(url) {
|
|
25
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
26
|
+
}
|
|
22
27
|
function filePathFromUrl(url) {
|
|
23
28
|
if (!url.startsWith("file://"))
|
|
24
29
|
return undefined;
|
|
@@ -3,10 +3,11 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
const FILE_PATH_CANDIDATE = /(?<![\p{L}\p{N}_:])((?:file:\/\/\/|~[\\/]|\.{1,2}[\\/]|[A-Za-z]:[\\/]|[\\/]|[A-Za-z0-9_.@-]+[\\/])[^\s"'`<>]*)/gu;
|
|
6
|
+
const WEB_URL_CANDIDATE = /https?:\/\/[^\s"'`<>]+/gu;
|
|
6
7
|
const TRAILING_PUNCTUATION = new Set([".", ",", ";", ")", "]", "}"]);
|
|
7
8
|
export function detectFileLinks(text, cwd) {
|
|
8
9
|
const links = [];
|
|
9
|
-
if (!text.includes("/") && !text.includes("\\"))
|
|
10
|
+
if (!text.includes("/") && !text.includes("\\") && !text.includes("http://") && !text.includes("https://"))
|
|
10
11
|
return links;
|
|
11
12
|
for (const match of text.matchAll(FILE_PATH_CANDIDATE)) {
|
|
12
13
|
const raw = match[1];
|
|
@@ -28,6 +29,17 @@ export function detectFileLinks(text, cwd) {
|
|
|
28
29
|
column: location.column,
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
for (const match of text.matchAll(WEB_URL_CANDIDATE)) {
|
|
33
|
+
const raw = match[0];
|
|
34
|
+
const candidate = trimTrailingPunctuation(raw);
|
|
35
|
+
if (!candidate)
|
|
36
|
+
continue;
|
|
37
|
+
links.push({
|
|
38
|
+
start: match.index,
|
|
39
|
+
end: match.index + candidate.length,
|
|
40
|
+
url: candidate,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
31
43
|
return mergeOverlappingLinks(links);
|
|
32
44
|
}
|
|
33
45
|
export function hyperlink(text, url) {
|
|
@@ -139,6 +139,7 @@ export declare class AppMouseController {
|
|
|
139
139
|
private inputClickFlashRegionForEvent;
|
|
140
140
|
private imageTargetAt;
|
|
141
141
|
private fileLinkAt;
|
|
142
|
+
private fileLinkTargetAt;
|
|
142
143
|
private statusTargetAt;
|
|
143
144
|
private handleImageClick;
|
|
144
145
|
private handleFileLinkClick;
|
|
@@ -60,6 +60,8 @@ export class AppMouseController {
|
|
|
60
60
|
this.showClickFlashOnPress(event);
|
|
61
61
|
if (event.button === 0 && !event.released && this.handleInputBorderStatusClick(event))
|
|
62
62
|
return;
|
|
63
|
+
if (event.button === 0 && !event.released && this.fileLinkAt(event))
|
|
64
|
+
return;
|
|
63
65
|
if (this.handleMouseSelection(event))
|
|
64
66
|
return;
|
|
65
67
|
if (this.withClickFlash(event, () => this.handleImageClick(event)))
|
|
@@ -234,9 +236,9 @@ export class AppMouseController {
|
|
|
234
236
|
const imageTarget = this.imageTargetAt(event);
|
|
235
237
|
if (imageTarget)
|
|
236
238
|
return { y: event.y, startColumn: imageTarget.start + 1, endColumn: imageTarget.end + 1 };
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
return { y: event.y, startColumn:
|
|
239
|
+
const linkTarget = this.fileLinkTargetAt(event);
|
|
240
|
+
if (linkTarget)
|
|
241
|
+
return { y: event.y, startColumn: linkTarget.startColumn, endColumn: linkTarget.endColumn };
|
|
240
242
|
const tabTarget = this.tabLineTargetAt(event);
|
|
241
243
|
if (tabTarget)
|
|
242
244
|
return { y: tabTarget.row, startColumn: tabTarget.startColumn, endColumn: tabTarget.endColumn };
|
|
@@ -290,10 +292,19 @@ export class AppMouseController {
|
|
|
290
292
|
return targets?.find((candidate) => event.x >= candidate.start + 1 && event.x <= candidate.end);
|
|
291
293
|
}
|
|
292
294
|
fileLinkAt(event) {
|
|
295
|
+
return this.fileLinkTargetAt(event)?.link;
|
|
296
|
+
}
|
|
297
|
+
fileLinkTargetAt(event) {
|
|
293
298
|
const text = this.renderedRowTexts.get(event.y);
|
|
294
299
|
if (!text)
|
|
295
300
|
return undefined;
|
|
296
|
-
|
|
301
|
+
for (const link of detectFileLinks(text, this.host.cwd())) {
|
|
302
|
+
const startColumn = stringDisplayWidth(text.slice(0, link.start)) + 1;
|
|
303
|
+
const endColumn = startColumn + stringDisplayWidth(text.slice(link.start, link.end));
|
|
304
|
+
if (event.x >= startColumn && event.x < endColumn)
|
|
305
|
+
return { link, startColumn, endColumn };
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
297
308
|
}
|
|
298
309
|
statusTargetAt(event) {
|
|
299
310
|
const target = [
|
|
@@ -323,7 +334,7 @@ export class AppMouseController {
|
|
|
323
334
|
};
|
|
324
335
|
}
|
|
325
336
|
handleImageClick(event) {
|
|
326
|
-
if (
|
|
337
|
+
if (!isPrimaryButtonRelease(event))
|
|
327
338
|
return false;
|
|
328
339
|
const imageTarget = this.imageTargetAt(event);
|
|
329
340
|
if (!imageTarget)
|
|
@@ -341,7 +352,7 @@ export class AppMouseController {
|
|
|
341
352
|
}
|
|
342
353
|
handleFileLinkClick(event) {
|
|
343
354
|
const modifiedPress = isModifiedPrimaryButton(event.button) && !event.released;
|
|
344
|
-
const plainRelease = event
|
|
355
|
+
const plainRelease = isPrimaryButtonRelease(event);
|
|
345
356
|
if (!modifiedPress && !plainRelease)
|
|
346
357
|
return false;
|
|
347
358
|
const link = this.fileLinkAt(event);
|
|
@@ -1053,6 +1064,9 @@ function isModifiedPrimaryButton(button) {
|
|
|
1053
1064
|
const modifierBits = button & (8 | 16);
|
|
1054
1065
|
return primaryButton && modifierBits !== 0;
|
|
1055
1066
|
}
|
|
1067
|
+
function isPrimaryButtonRelease(event) {
|
|
1068
|
+
return event.released && (event.button === 0 || (event.button & 3) === 3);
|
|
1069
|
+
}
|
|
1056
1070
|
function editorLayoutRows(terminalRows, tabPanelRows) {
|
|
1057
1071
|
return Math.max(1, terminalRows - tabPanelRows);
|
|
1058
1072
|
}
|
|
@@ -70,6 +70,10 @@ export function firstUserMessageText(ctx: ExtensionContext): string | undefined
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function hasExistingUserMessage(ctx: ExtensionContext): boolean {
|
|
74
|
+
return firstUserMessageText(ctx) !== undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
export function fallbackSessionTitleFromInput(input: string, maxTitleChars: number): string | undefined {
|
|
74
78
|
const normalized = input
|
|
75
79
|
.replace(/[\t\r\n]+/gu, " ")
|
|
@@ -371,24 +375,6 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
371
375
|
})();
|
|
372
376
|
}
|
|
373
377
|
|
|
374
|
-
function primeTitleGenerationFromExistingSession(ctx: ExtensionContext, currentConfig: SessionTitleConfig): void {
|
|
375
|
-
if (currentSessionName(ctx)) return;
|
|
376
|
-
|
|
377
|
-
const input = firstUserMessageText(ctx);
|
|
378
|
-
if (!input) return;
|
|
379
|
-
if (!currentConfig.enabled) {
|
|
380
|
-
applyFallbackSessionTitle(ctx, currentConfig, input);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
pendingGeneration = {
|
|
385
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
386
|
-
input: truncateInput(input, currentConfig.maxInputChars),
|
|
387
|
-
attempts: 0,
|
|
388
|
-
};
|
|
389
|
-
startTitleGeneration(ctx, currentConfig);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
378
|
function isSameSessionPath(left: string | undefined, right: string | undefined): boolean {
|
|
393
379
|
if (!left || !right) return false;
|
|
394
380
|
if (left === right) return true;
|
|
@@ -447,7 +433,6 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
447
433
|
await prepareForkTitleState(event, ctx);
|
|
448
434
|
refreshSessionUi(ctx, { force: true });
|
|
449
435
|
scheduleSessionUiRefresh(ctx);
|
|
450
|
-
if (!forkTitleState) primeTitleGenerationFromExistingSession(ctx, config);
|
|
451
436
|
});
|
|
452
437
|
|
|
453
438
|
pi.on("session_shutdown", async () => {
|
|
@@ -480,6 +465,10 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
480
465
|
sessionId = currentSessionId;
|
|
481
466
|
const currentName = currentSessionName(ctx);
|
|
482
467
|
const activeForkTitleState = forkTitleState?.sessionId === currentSessionId ? forkTitleState : undefined;
|
|
468
|
+
if (!activeForkTitleState && hasExistingUserMessage(ctx)) {
|
|
469
|
+
forkTitleState = undefined;
|
|
470
|
+
return { action: "continue" as const };
|
|
471
|
+
}
|
|
483
472
|
if (currentName && (!activeForkTitleState || currentName !== activeForkTitleState.inheritedSessionName)) {
|
|
484
473
|
forkTitleState = undefined;
|
|
485
474
|
return { action: "continue" as const };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const DEFAULT_STARTUP_TIMEOUT_MS = 45_000;
|
|
2
2
|
export const DEFAULT_DIAGNOSTICS_WAIT_MS = 10_000;
|
|
3
3
|
export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
export const DEFAULT_IDLE_SHUTDOWN_MS = 30_000;
|
|
4
5
|
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
6
|
export const SHUTDOWN_WRITE_TIMEOUT_MS = 100;
|
|
6
7
|
export const SHUTDOWN_TERM_TIMEOUT_MS = 2_000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { DEFAULT_DIAGNOSTICS_WAIT_MS, DEFAULT_MAX_FILE_SIZE_BYTES, LSP_MANAGER_GLOBAL_KEY } from "./constants";
|
|
3
|
+
import { DEFAULT_DIAGNOSTICS_WAIT_MS, DEFAULT_IDLE_SHUTDOWN_MS, DEFAULT_MAX_FILE_SIZE_BYTES, LSP_MANAGER_GLOBAL_KEY } from "./constants";
|
|
4
4
|
import { DiagnosticsStore } from "./diagnostics-store";
|
|
5
5
|
import { LspClient } from "./client";
|
|
6
6
|
import { loadLspConfig } from "./_shared/config";
|
|
@@ -43,12 +43,51 @@ export class LspManager {
|
|
|
43
43
|
private readonly diagnostics = new DiagnosticsStore();
|
|
44
44
|
private readonly clients = new Map<string, LspClient>();
|
|
45
45
|
private readonly backoff = new Map<string, { retryAt: number; attempts: number; reason: string }>();
|
|
46
|
+
private idleShutdownTimer: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
private activeOperations = 0;
|
|
48
|
+
private handlingSignal = false;
|
|
46
49
|
private readonly handleProcessExit = () => {
|
|
47
50
|
this.shutdownAllSync();
|
|
48
51
|
};
|
|
52
|
+
private readonly handleProcessSignal = (signal: NodeJS.Signals) => {
|
|
53
|
+
this.shutdownAllSync();
|
|
54
|
+
|
|
55
|
+
// Restore the platform default for the terminating signal. LSP servers are
|
|
56
|
+
// spawned detached so they can otherwise outlive Pi when the process is
|
|
57
|
+
// killed by a terminal/editor without a session_shutdown event.
|
|
58
|
+
if (this.handlingSignal) return;
|
|
59
|
+
this.handlingSignal = true;
|
|
60
|
+
process.kill(process.pid, signal);
|
|
61
|
+
};
|
|
49
62
|
|
|
50
63
|
constructor() {
|
|
51
64
|
process.once("exit", this.handleProcessExit);
|
|
65
|
+
process.once("SIGINT", this.handleProcessSignal);
|
|
66
|
+
process.once("SIGTERM", this.handleProcessSignal);
|
|
67
|
+
process.once("SIGHUP", this.handleProcessSignal);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private clearIdleShutdownTimer(): void {
|
|
71
|
+
if (!this.idleShutdownTimer) return;
|
|
72
|
+
clearTimeout(this.idleShutdownTimer);
|
|
73
|
+
this.idleShutdownTimer = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private beginOperation(): void {
|
|
77
|
+
this.activeOperations += 1;
|
|
78
|
+
this.clearIdleShutdownTimer();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private endOperation(): void {
|
|
82
|
+
this.activeOperations = Math.max(0, this.activeOperations - 1);
|
|
83
|
+
if (this.activeOperations > 0 || this.clients.size === 0) return;
|
|
84
|
+
|
|
85
|
+
this.clearIdleShutdownTimer();
|
|
86
|
+
this.idleShutdownTimer = setTimeout(() => {
|
|
87
|
+
if (this.activeOperations > 0 || this.clients.size === 0) return;
|
|
88
|
+
void this.shutdownAll();
|
|
89
|
+
}, DEFAULT_IDLE_SHUTDOWN_MS);
|
|
90
|
+
this.idleShutdownTimer.unref?.();
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
async matchingServers(ctx: ExtensionContext, file: string): Promise<{ matches: MatchedServer[]; warnings: string[]; workspace: string }> {
|
|
@@ -104,93 +143,98 @@ export class LspManager {
|
|
|
104
143
|
}
|
|
105
144
|
|
|
106
145
|
async updateDiagnosticsForFile(ctx: ExtensionContext, file: string): Promise<string> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
for (const match of matches) {
|
|
112
|
-
try {
|
|
113
|
-
const maxFileSizeBytes = match.server.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
114
|
-
if (!(await fileSizeAllowed(file, maxFileSizeBytes))) {
|
|
115
|
-
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: skipped ${match.relFile}; file exceeds maxFileSizeBytes (${maxFileSizeBytes})`);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
146
|
+
this.beginOperation();
|
|
147
|
+
try {
|
|
148
|
+
const { matches, warnings, workspace } = await this.matchingServers(ctx, file);
|
|
149
|
+
if (matches.length === 0) return formatWarnings("LSP diagnostics", warnings);
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// old error from a previous document version. Empty diagnostics published
|
|
122
|
-
// by the server are stored, but this local clear is not.
|
|
123
|
-
this.diagnostics.clear(match.server.id, match.root, filePathToUri(file));
|
|
124
|
-
|
|
125
|
-
const text = await readTextFile(file);
|
|
126
|
-
const client = await this.getClient(match.server, match.root, file, workspace, ctx.signal);
|
|
127
|
-
const languageId = languageIdForFile(match.server, file);
|
|
128
|
-
const startedAt = Date.now();
|
|
129
|
-
const doc = await client.openOrChange(file, languageId, text, ctx.signal);
|
|
130
|
-
await client.didSave(file);
|
|
131
|
-
const diagnosticsWaitMs = match.server.diagnosticsWaitMs ?? DEFAULT_DIAGNOSTICS_WAIT_MS;
|
|
132
|
-
|
|
133
|
-
// typescript-language-server sometimes does not emit a fresh
|
|
134
|
-
// textDocument/publishDiagnostics notification after didChange/didSave,
|
|
135
|
-
// even though tsserver can answer diagnostics synchronously. Prefer the
|
|
136
|
-
// explicit tsserver request when the server exposes it, so post-edit
|
|
137
|
-
// diagnostics don't degrade into a misleading publishDiagnostics timeout.
|
|
138
|
-
let tsserverFallbackError: string | undefined;
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
for (const match of matches) {
|
|
139
153
|
try {
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
144
|
-
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
154
|
+
const maxFileSizeBytes = match.server.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
155
|
+
if (!(await fileSizeAllowed(file, maxFileSizeBytes))) {
|
|
156
|
+
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: skipped ${match.relFile}; file exceeds maxFileSizeBytes (${maxFileSizeBytes})`);
|
|
145
157
|
continue;
|
|
146
158
|
}
|
|
147
|
-
} catch (error) {
|
|
148
|
-
tsserverFallbackError = (error as Error).message;
|
|
149
|
-
}
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
// Clear stale diagnostics before refreshing this file. The synchronous
|
|
161
|
+
// wait below must observe a fresh publishDiagnostics notification, not an
|
|
162
|
+
// old error from a previous document version. Empty diagnostics published
|
|
163
|
+
// by the server are stored, but this local clear is not.
|
|
164
|
+
this.diagnostics.clear(match.server.id, match.root, filePathToUri(file));
|
|
165
|
+
|
|
166
|
+
const text = await readTextFile(file);
|
|
167
|
+
const client = await this.getClient(match.server, match.root, file, workspace, ctx.signal);
|
|
168
|
+
const languageId = languageIdForFile(match.server, file);
|
|
169
|
+
const startedAt = Date.now();
|
|
170
|
+
const doc = await client.openOrChange(file, languageId, text, ctx.signal);
|
|
171
|
+
await client.didSave(file);
|
|
172
|
+
const diagnosticsWaitMs = match.server.diagnosticsWaitMs ?? DEFAULT_DIAGNOSTICS_WAIT_MS;
|
|
173
|
+
|
|
174
|
+
// typescript-language-server sometimes does not emit a fresh
|
|
175
|
+
// textDocument/publishDiagnostics notification after didChange/didSave,
|
|
176
|
+
// even though tsserver can answer diagnostics synchronously. Prefer the
|
|
177
|
+
// explicit tsserver request when the server exposes it, so post-edit
|
|
178
|
+
// diagnostics don't degrade into a misleading publishDiagnostics timeout.
|
|
179
|
+
let tsserverFallbackError: string | undefined;
|
|
153
180
|
try {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text,
|
|
181
|
+
const tsserverDiagnostics = await client.tsserverDiagnostics(file, text, diagnosticsWaitMs, ctx.signal);
|
|
182
|
+
if (tsserverDiagnostics !== undefined) {
|
|
183
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, tsserverDiagnostics);
|
|
157
184
|
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
158
185
|
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
159
186
|
continue;
|
|
160
187
|
}
|
|
161
188
|
} catch (error) {
|
|
162
|
-
|
|
189
|
+
tsserverFallbackError = (error as Error).message;
|
|
163
190
|
}
|
|
164
|
-
}
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
let pullDiagnosticsError: string | undefined;
|
|
193
|
+
if (match.server.pullDiagnostics !== false) {
|
|
194
|
+
try {
|
|
195
|
+
const pulledDiagnostics = await client.pullDiagnostics(file, diagnosticsWaitMs, ctx.signal);
|
|
196
|
+
if (pulledDiagnostics !== undefined) {
|
|
197
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, pulledDiagnostics);
|
|
198
|
+
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
199
|
+
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
pullDiagnosticsError = (error as Error).message;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (match.server.waitForPublishDiagnostics === false || diagnosticsWaitMs <= 0) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
169
210
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
211
|
+
const entry = await this.diagnostics.waitForFile(
|
|
212
|
+
match.server.id,
|
|
213
|
+
match.root,
|
|
214
|
+
file,
|
|
215
|
+
startedAt,
|
|
216
|
+
doc.version,
|
|
217
|
+
diagnosticsWaitMs,
|
|
218
|
+
ctx.signal,
|
|
219
|
+
);
|
|
220
|
+
if (!isFreshDiagnosticsEntry(entry, startedAt, doc.version)) {
|
|
221
|
+
const fallbackSuffix = tsserverFallbackError ? `; tsserver fallback failed: ${tsserverFallbackError}` : "";
|
|
222
|
+
const pullSuffix = pullDiagnosticsError ? `; pull diagnostics failed: ${pullDiagnosticsError}` : "";
|
|
223
|
+
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: timed out after ${diagnosticsWaitMs}ms waiting for fresh diagnostics for ${match.relFile}${fallbackSuffix}${pullSuffix}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, entry.diagnostics);
|
|
227
|
+
if (diagnostics !== entry.diagnostics) this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
228
|
+
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
229
|
+
} catch (error) {
|
|
230
|
+
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: ${(error as Error).message}`);
|
|
184
231
|
}
|
|
185
|
-
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, entry.diagnostics);
|
|
186
|
-
if (diagnostics !== entry.diagnostics) this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
187
|
-
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
188
|
-
} catch (error) {
|
|
189
|
-
lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: ${(error as Error).message}`);
|
|
190
232
|
}
|
|
191
|
-
}
|
|
192
233
|
|
|
193
|
-
|
|
234
|
+
return [formatWarnings("LSP diagnostics", warnings), joinSections("LSP diagnostics", lines)].filter(Boolean).join("\n\n");
|
|
235
|
+
} finally {
|
|
236
|
+
this.endOperation();
|
|
237
|
+
}
|
|
194
238
|
}
|
|
195
239
|
|
|
196
240
|
async ensureDocumentForTool(ctx: ExtensionContext, inputPath: string): Promise<{ file: string; match: MatchedServer; client: LspClient; workspace: string } | undefined> {
|
|
@@ -215,15 +259,20 @@ export class LspManager {
|
|
|
215
259
|
}
|
|
216
260
|
|
|
217
261
|
async shutdownAll(): Promise<void> {
|
|
262
|
+
this.clearIdleShutdownTimer();
|
|
218
263
|
const clients = [...this.clients.values()];
|
|
219
264
|
this.clients.clear();
|
|
220
265
|
await Promise.allSettled(clients.map((client) => client.shutdown()));
|
|
221
266
|
}
|
|
222
267
|
|
|
223
268
|
shutdownAllSync(): void {
|
|
269
|
+
this.clearIdleShutdownTimer();
|
|
224
270
|
const clients = [...this.clients.values()];
|
|
225
271
|
this.clients.clear();
|
|
226
272
|
process.off("exit", this.handleProcessExit);
|
|
273
|
+
process.off("SIGINT", this.handleProcessSignal);
|
|
274
|
+
process.off("SIGTERM", this.handleProcessSignal);
|
|
275
|
+
process.off("SIGHUP", this.handleProcessSignal);
|
|
227
276
|
for (const client of clients) client.shutdownSync();
|
|
228
277
|
}
|
|
229
278
|
}
|