@mrclrchtr/supi-code-intelligence 4.9.0 → 4.10.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/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +155 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +70 -34
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +104 -5
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +10 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +4 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +106 -2
- package/package.json +5 -5
- package/src/analysis/refactor/apply.ts +14 -19
- package/src/analysis/refactor/position.ts +47 -0
- package/src/analysis/refactor/safety.ts +18 -10
- package/src/analysis/search/ast-scan-timing.ts +0 -2
|
@@ -46,13 +46,21 @@ export interface DebugTimer {
|
|
|
46
46
|
* names are accumulated. Event data reserves the `timing` field. When Debug is
|
|
47
47
|
* disabled at start, this returns a no-op timer and does not read the clock.
|
|
48
48
|
* Pass a factory to `finish()` to avoid event-data construction when disabled.
|
|
49
|
+
* Clock, event-construction, and registry failures are isolated from the
|
|
50
|
+
* measured operation and make the timer a no-op.
|
|
49
51
|
*/
|
|
50
52
|
export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
51
53
|
if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
|
|
52
54
|
const now = options.now ?? performance.now.bind(performance);
|
|
53
|
-
|
|
55
|
+
let startedAt: number;
|
|
56
|
+
try {
|
|
57
|
+
startedAt = now();
|
|
58
|
+
} catch {
|
|
59
|
+
return DISABLED_DEBUG_TIMER;
|
|
60
|
+
}
|
|
54
61
|
let previousAt = startedAt;
|
|
55
62
|
let finished = false;
|
|
63
|
+
let failed = false;
|
|
56
64
|
const phases = new Map<string, number>();
|
|
57
65
|
|
|
58
66
|
const markAt = (phase: string, current: number): void => {
|
|
@@ -65,30 +73,35 @@ export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
|
65
73
|
return {
|
|
66
74
|
enabled: true,
|
|
67
75
|
mark(phase) {
|
|
68
|
-
if (finished) return;
|
|
69
|
-
|
|
76
|
+
if (finished || failed) return;
|
|
77
|
+
try {
|
|
78
|
+
markAt(phase, now());
|
|
79
|
+
} catch {
|
|
80
|
+
failed = true;
|
|
81
|
+
}
|
|
70
82
|
},
|
|
71
83
|
finish(input, finalPhase) {
|
|
72
|
-
if (finished) return null;
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
if (finished || failed) return null;
|
|
85
|
+
finished = true;
|
|
86
|
+
try {
|
|
87
|
+
if (!isDebugRegistryEnabled()) return null;
|
|
88
|
+
const completedAt = now();
|
|
89
|
+
if (finalPhase) markAt(finalPhase, completedAt);
|
|
90
|
+
const phasesMs = Object.fromEntries(
|
|
91
|
+
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
92
|
+
);
|
|
93
|
+
const timing: DebugTiming = {
|
|
94
|
+
durationMs: duration(completedAt - startedAt),
|
|
95
|
+
phasesMs,
|
|
96
|
+
};
|
|
97
|
+
const eventInput = typeof input === "function" ? input() : input;
|
|
98
|
+
return recordDebugEvent({
|
|
99
|
+
...eventInput,
|
|
100
|
+
data: { ...eventInput.data, timing },
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
75
103
|
return null;
|
|
76
104
|
}
|
|
77
|
-
const completedAt = now();
|
|
78
|
-
if (finalPhase) markAt(finalPhase, completedAt);
|
|
79
|
-
finished = true;
|
|
80
|
-
const phasesMs = Object.fromEntries(
|
|
81
|
-
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
82
|
-
);
|
|
83
|
-
const timing: DebugTiming = {
|
|
84
|
-
durationMs: duration(completedAt - startedAt),
|
|
85
|
-
phasesMs,
|
|
86
|
-
};
|
|
87
|
-
const eventInput = typeof input === "function" ? input() : input;
|
|
88
|
-
return recordDebugEvent({
|
|
89
|
-
...eventInput,
|
|
90
|
-
data: { ...eventInput.data, timing },
|
|
91
|
-
});
|
|
92
105
|
},
|
|
93
106
|
};
|
|
94
107
|
}
|
package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts
CHANGED
|
@@ -46,13 +46,21 @@ export interface DebugTimer {
|
|
|
46
46
|
* names are accumulated. Event data reserves the `timing` field. When Debug is
|
|
47
47
|
* disabled at start, this returns a no-op timer and does not read the clock.
|
|
48
48
|
* Pass a factory to `finish()` to avoid event-data construction when disabled.
|
|
49
|
+
* Clock, event-construction, and registry failures are isolated from the
|
|
50
|
+
* measured operation and make the timer a no-op.
|
|
49
51
|
*/
|
|
50
52
|
export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
51
53
|
if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
|
|
52
54
|
const now = options.now ?? performance.now.bind(performance);
|
|
53
|
-
|
|
55
|
+
let startedAt: number;
|
|
56
|
+
try {
|
|
57
|
+
startedAt = now();
|
|
58
|
+
} catch {
|
|
59
|
+
return DISABLED_DEBUG_TIMER;
|
|
60
|
+
}
|
|
54
61
|
let previousAt = startedAt;
|
|
55
62
|
let finished = false;
|
|
63
|
+
let failed = false;
|
|
56
64
|
const phases = new Map<string, number>();
|
|
57
65
|
|
|
58
66
|
const markAt = (phase: string, current: number): void => {
|
|
@@ -65,30 +73,35 @@ export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
|
|
|
65
73
|
return {
|
|
66
74
|
enabled: true,
|
|
67
75
|
mark(phase) {
|
|
68
|
-
if (finished) return;
|
|
69
|
-
|
|
76
|
+
if (finished || failed) return;
|
|
77
|
+
try {
|
|
78
|
+
markAt(phase, now());
|
|
79
|
+
} catch {
|
|
80
|
+
failed = true;
|
|
81
|
+
}
|
|
70
82
|
},
|
|
71
83
|
finish(input, finalPhase) {
|
|
72
|
-
if (finished) return null;
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
if (finished || failed) return null;
|
|
85
|
+
finished = true;
|
|
86
|
+
try {
|
|
87
|
+
if (!isDebugRegistryEnabled()) return null;
|
|
88
|
+
const completedAt = now();
|
|
89
|
+
if (finalPhase) markAt(finalPhase, completedAt);
|
|
90
|
+
const phasesMs = Object.fromEntries(
|
|
91
|
+
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
92
|
+
);
|
|
93
|
+
const timing: DebugTiming = {
|
|
94
|
+
durationMs: duration(completedAt - startedAt),
|
|
95
|
+
phasesMs,
|
|
96
|
+
};
|
|
97
|
+
const eventInput = typeof input === "function" ? input() : input;
|
|
98
|
+
return recordDebugEvent({
|
|
99
|
+
...eventInput,
|
|
100
|
+
data: { ...eventInput.data, timing },
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
75
103
|
return null;
|
|
76
104
|
}
|
|
77
|
-
const completedAt = now();
|
|
78
|
-
if (finalPhase) markAt(finalPhase, completedAt);
|
|
79
|
-
finished = true;
|
|
80
|
-
const phasesMs = Object.fromEntries(
|
|
81
|
-
[...phases.entries()].map(([name, value]) => [name, duration(value)]),
|
|
82
|
-
);
|
|
83
|
-
const timing: DebugTiming = {
|
|
84
|
-
durationMs: duration(completedAt - startedAt),
|
|
85
|
-
phasesMs,
|
|
86
|
-
};
|
|
87
|
-
const eventInput = typeof input === "function" ? input() : input;
|
|
88
|
-
return recordDebugEvent({
|
|
89
|
-
...eventInput,
|
|
90
|
-
data: { ...eventInput.data, timing },
|
|
91
|
-
});
|
|
92
105
|
},
|
|
93
106
|
};
|
|
94
107
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-lsp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"description": "Language Server Protocol runtime for SuPi code intelligence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"vscode-jsonrpc": "^9.0.0",
|
|
38
38
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
39
39
|
"vscode-languageserver-types": "^3.17.5",
|
|
40
|
-
"@mrclrchtr/supi-
|
|
41
|
-
"@mrclrchtr/supi-
|
|
40
|
+
"@mrclrchtr/supi-code-runtime": "4.10.0",
|
|
41
|
+
"@mrclrchtr/supi-core": "4.10.0"
|
|
42
42
|
},
|
|
43
43
|
"bundledDependencies": [
|
|
44
44
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
|
|
2
|
+
|
|
3
|
+
type DiagnosticCollection = "fallback" | "none" | "pull" | "push";
|
|
4
|
+
type DiagnosticFreshness = "not-observed" | "observed";
|
|
5
|
+
type DiagnosticOutcome = "completed" | "skipped" | "timed-out";
|
|
6
|
+
type DiagnosticPullOutcome = "completed" | "failed" | "not-supported" | "not-used" | "timed-out";
|
|
7
|
+
type DiagnosticPushOutcome = "not-used" | "published" | "released" | "settled" | "timed-out";
|
|
8
|
+
type DiagnosticSettleOutcome = "not-used" | "published" | "quiet" | "released" | "timed-out";
|
|
9
|
+
type DiagnosticTimingOperation = "refresh-open" | "sync-file";
|
|
10
|
+
|
|
11
|
+
/** Result of waiting for a quiet push-diagnostic window. */
|
|
12
|
+
export interface DiagnosticSettleResult {
|
|
13
|
+
readonly outcome: "quiet" | "timed-out";
|
|
14
|
+
readonly freshness: DiagnosticFreshness;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Result of waiting for one file's push diagnostics. */
|
|
18
|
+
export type DiagnosticPushWaitOutcome = "published" | "released" | "timed-out";
|
|
19
|
+
|
|
20
|
+
interface DiagnosticTimingData {
|
|
21
|
+
readonly collection: DiagnosticCollection;
|
|
22
|
+
readonly documentCount: number;
|
|
23
|
+
readonly fallback: boolean;
|
|
24
|
+
readonly freshness: DiagnosticFreshness;
|
|
25
|
+
readonly outcome: DiagnosticOutcome;
|
|
26
|
+
readonly pull: DiagnosticPullOutcome;
|
|
27
|
+
readonly push: DiagnosticPushOutcome;
|
|
28
|
+
readonly settle: DiagnosticSettleOutcome;
|
|
29
|
+
readonly timedOut: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Internal pull failure that retains only whether a timeout occurred. */
|
|
33
|
+
export class DiagnosticPullError extends Error {
|
|
34
|
+
constructor(readonly timedOut: boolean) {
|
|
35
|
+
super("pull diagnostics incomplete");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Record one diagnostic operation without document identifiers or diagnostic text.
|
|
41
|
+
*
|
|
42
|
+
* The observer owns result classification so diagnostic control flow does not
|
|
43
|
+
* duplicate the event shape.
|
|
44
|
+
*/
|
|
45
|
+
export class DiagnosticObserver {
|
|
46
|
+
readonly #timer = startDebugTimer();
|
|
47
|
+
#pull: "failed" | "not-supported" | "timed-out";
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
readonly operation: DiagnosticTimingOperation,
|
|
51
|
+
readonly supportsPull: boolean,
|
|
52
|
+
) {
|
|
53
|
+
this.#pull = supportsPull ? "failed" : "not-supported";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
synchronized(): void {
|
|
57
|
+
this.#timer.mark("synchronize");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
skipped(documentCount: number): void {
|
|
61
|
+
this.#finish({
|
|
62
|
+
collection: "none",
|
|
63
|
+
documentCount,
|
|
64
|
+
fallback: false,
|
|
65
|
+
freshness: "not-observed",
|
|
66
|
+
outcome: "skipped",
|
|
67
|
+
pull: "not-used",
|
|
68
|
+
push: "not-used",
|
|
69
|
+
settle: "not-used",
|
|
70
|
+
timedOut: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pullCompleted(documentCount: number): void {
|
|
75
|
+
this.#finish(
|
|
76
|
+
{
|
|
77
|
+
collection: "pull",
|
|
78
|
+
documentCount,
|
|
79
|
+
fallback: false,
|
|
80
|
+
freshness: "observed",
|
|
81
|
+
outcome: "completed",
|
|
82
|
+
pull: "completed",
|
|
83
|
+
push: "not-used",
|
|
84
|
+
settle: "not-used",
|
|
85
|
+
timedOut: false,
|
|
86
|
+
},
|
|
87
|
+
"pull",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pullFailed(error: unknown): void {
|
|
92
|
+
this.#pull = isDiagnosticTimeout(error) ? "timed-out" : "failed";
|
|
93
|
+
this.#timer.mark("pull");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pullTimedOut(): void {
|
|
97
|
+
this.#pull = "timed-out";
|
|
98
|
+
this.#timer.mark("pull");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pushSettled(documentCount: number, settle: DiagnosticSettleResult): void {
|
|
102
|
+
const timedOut = settle.outcome === "timed-out";
|
|
103
|
+
this.#finish(
|
|
104
|
+
{
|
|
105
|
+
collection: this.supportsPull ? "fallback" : "push",
|
|
106
|
+
documentCount,
|
|
107
|
+
fallback: this.supportsPull,
|
|
108
|
+
freshness: settle.freshness,
|
|
109
|
+
outcome: timedOut ? "timed-out" : "completed",
|
|
110
|
+
pull: this.#pull,
|
|
111
|
+
push: timedOut ? "timed-out" : "settled",
|
|
112
|
+
settle: settle.outcome,
|
|
113
|
+
timedOut: timedOut || this.#pull === "timed-out",
|
|
114
|
+
},
|
|
115
|
+
"push-settle",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pushWaitCompleted(documentCount: number, push: DiagnosticPushWaitOutcome): void {
|
|
120
|
+
const timedOut = push === "timed-out";
|
|
121
|
+
this.#finish(
|
|
122
|
+
{
|
|
123
|
+
collection: this.supportsPull ? "fallback" : "push",
|
|
124
|
+
documentCount,
|
|
125
|
+
fallback: this.supportsPull,
|
|
126
|
+
freshness: push === "published" ? "observed" : "not-observed",
|
|
127
|
+
outcome: timedOut ? "timed-out" : "completed",
|
|
128
|
+
pull: this.#pull,
|
|
129
|
+
push,
|
|
130
|
+
settle: push,
|
|
131
|
+
timedOut: timedOut || this.#pull === "timed-out",
|
|
132
|
+
},
|
|
133
|
+
"push-settle",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#finish(data: DiagnosticTimingData, finalPhase?: "pull" | "push-settle" | "synchronize"): void {
|
|
138
|
+
this.#timer.finish(
|
|
139
|
+
() => ({
|
|
140
|
+
source: "lsp",
|
|
141
|
+
level: "debug",
|
|
142
|
+
category: "diagnostics.timing",
|
|
143
|
+
message: `LSP diagnostic ${this.operation} ${data.outcome}`,
|
|
144
|
+
data: { operation: this.operation, ...data },
|
|
145
|
+
}),
|
|
146
|
+
finalPhase,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Return whether a diagnostic failure represents a timeout without retaining its message. */
|
|
152
|
+
export function isDiagnosticTimeout(error: unknown): boolean {
|
|
153
|
+
if (error instanceof DiagnosticPullError) return error.timedOut;
|
|
154
|
+
return error instanceof Error && /\btimed? ?out\b|\btimeout\b/i.test(error.message);
|
|
155
|
+
}
|
|
@@ -10,10 +10,18 @@ import type {
|
|
|
10
10
|
VersionedTextDocumentIdentifier,
|
|
11
11
|
} from "../config/types.ts";
|
|
12
12
|
import { detectLanguageId, fileToUri, uriToFile } from "../utils.ts";
|
|
13
|
+
import {
|
|
14
|
+
DiagnosticObserver,
|
|
15
|
+
DiagnosticPullError,
|
|
16
|
+
type DiagnosticPushWaitOutcome,
|
|
17
|
+
type DiagnosticSettleResult,
|
|
18
|
+
isDiagnosticTimeout,
|
|
19
|
+
} from "./client-diagnostic-timing.ts";
|
|
13
20
|
|
|
14
21
|
const DIAGNOSTIC_WAIT_MS = 3_000;
|
|
15
22
|
|
|
16
23
|
type OpenDocument = { version: number };
|
|
24
|
+
type DiagnosticWaiter = (outcome: "published" | "released") => void;
|
|
17
25
|
|
|
18
26
|
type DiagnosticCacheEntry = {
|
|
19
27
|
diagnostics: Diagnostic[];
|
|
@@ -48,7 +56,7 @@ interface ClientDiagnosticsHost {
|
|
|
48
56
|
export class ClientDiagnostics {
|
|
49
57
|
readonly #openDocs = new Map<string, OpenDocument>();
|
|
50
58
|
readonly #diagnosticStore = new Map<string, DiagnosticCacheEntry>();
|
|
51
|
-
readonly #diagnosticWaiters = new Map<string,
|
|
59
|
+
readonly #diagnosticWaiters = new Map<string, DiagnosticWaiter[]>();
|
|
52
60
|
|
|
53
61
|
constructor(private readonly host: ClientDiagnosticsHost) {}
|
|
54
62
|
|
|
@@ -160,53 +168,72 @@ export class ClientDiagnostics {
|
|
|
160
168
|
receivedAt: Date.now(),
|
|
161
169
|
version: params.version ?? undefined,
|
|
162
170
|
});
|
|
163
|
-
this.#releaseDiagnosticWaiters(params.uri);
|
|
171
|
+
this.#releaseDiagnosticWaiters(params.uri, "published");
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
/** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
|
|
167
175
|
async refreshOpenDiagnostics(
|
|
168
176
|
options: { maxWaitMs?: number; quietMs?: number } = {},
|
|
169
177
|
): Promise<void> {
|
|
170
|
-
|
|
178
|
+
const supportsPull = this.host.supportsPullDiagnostics();
|
|
179
|
+
const observer = new DiagnosticObserver("refresh-open", supportsPull);
|
|
180
|
+
if (!this.host.isOperational()) {
|
|
181
|
+
observer.skipped(0);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
171
184
|
|
|
172
185
|
const maxWaitMs = options.maxWaitMs ?? 3_000;
|
|
173
186
|
const quietMs = options.quietMs ?? 200;
|
|
174
187
|
const syncStart = Date.now();
|
|
175
|
-
|
|
176
188
|
this.#resyncOpenDocuments();
|
|
177
|
-
|
|
189
|
+
observer.synchronized();
|
|
190
|
+
const documentCount = this.#openDocs.size;
|
|
191
|
+
if (documentCount === 0) {
|
|
192
|
+
observer.skipped(0);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
178
195
|
|
|
179
|
-
if (
|
|
196
|
+
if (supportsPull) {
|
|
180
197
|
try {
|
|
181
198
|
await this.#pullDiagnosticsForOpenDocuments(syncStart, maxWaitMs);
|
|
199
|
+
observer.pullCompleted(documentCount);
|
|
182
200
|
return;
|
|
183
|
-
} catch {
|
|
184
|
-
|
|
201
|
+
} catch (error) {
|
|
202
|
+
observer.pullFailed(error);
|
|
185
203
|
}
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
await this.#waitForDiagnosticSettle(syncStart, maxWaitMs, quietMs);
|
|
206
|
+
const settle = await this.#waitForDiagnosticSettle(syncStart, maxWaitMs, quietMs);
|
|
207
|
+
observer.pushSettled(documentCount, settle);
|
|
189
208
|
}
|
|
190
209
|
|
|
191
210
|
/** Sync one file and return its diagnostics after pull or push collection. */
|
|
192
211
|
async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
|
|
212
|
+
const supportsPull = this.host.supportsPullDiagnostics();
|
|
213
|
+
const observer = new DiagnosticObserver("sync-file", supportsPull);
|
|
193
214
|
const uri = fileToUri(filePath);
|
|
194
215
|
const syncStart = Date.now();
|
|
195
216
|
this.didChange(filePath, content);
|
|
217
|
+
observer.synchronized();
|
|
196
218
|
|
|
197
|
-
if (
|
|
219
|
+
if (supportsPull) {
|
|
198
220
|
const remaining = DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
try {
|
|
222
|
+
if (remaining <= 0) observer.pullTimedOut();
|
|
223
|
+
else if (await this.#pullDiagnosticsForUri(uri, remaining)) {
|
|
224
|
+
observer.pullCompleted(1);
|
|
225
|
+
return this.getDiagnostics(filePath);
|
|
226
|
+
} else observer.pullFailed(undefined);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
observer.pullFailed(error);
|
|
206
229
|
}
|
|
207
230
|
}
|
|
208
231
|
|
|
209
|
-
await this.#waitForDiagnostics(
|
|
232
|
+
const push = await this.#waitForDiagnostics(
|
|
233
|
+
uri,
|
|
234
|
+
Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)),
|
|
235
|
+
);
|
|
236
|
+
observer.pushWaitCompleted(1, push);
|
|
210
237
|
return this.getDiagnostics(filePath);
|
|
211
238
|
}
|
|
212
239
|
|
|
@@ -240,9 +267,9 @@ export class ClientDiagnostics {
|
|
|
240
267
|
);
|
|
241
268
|
|
|
242
269
|
const anySuccess = results.some((result) => result.status === "fulfilled" && result.value);
|
|
243
|
-
const
|
|
244
|
-
if ((
|
|
245
|
-
throw new
|
|
270
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
271
|
+
if ((failures.length > 0 || !anySuccess) && uris.length > 0) {
|
|
272
|
+
throw new DiagnosticPullError(failures.some((result) => isDiagnosticTimeout(result.reason)));
|
|
246
273
|
}
|
|
247
274
|
}
|
|
248
275
|
|
|
@@ -280,16 +307,25 @@ export class ClientDiagnostics {
|
|
|
280
307
|
syncStart: number,
|
|
281
308
|
maxWaitMs: number,
|
|
282
309
|
quietMs: number,
|
|
283
|
-
): Promise<
|
|
310
|
+
): Promise<DiagnosticSettleResult> {
|
|
284
311
|
const deadline = syncStart + maxWaitMs;
|
|
285
312
|
while (Date.now() < deadline) {
|
|
286
|
-
const
|
|
287
|
-
const elapsed = Date.now() -
|
|
288
|
-
if (elapsed >= quietMs)
|
|
313
|
+
const observedAt = this.#lastDiagnosticReceivedAfter(syncStart);
|
|
314
|
+
const elapsed = Date.now() - (observedAt || syncStart);
|
|
315
|
+
if (elapsed >= quietMs) {
|
|
316
|
+
return {
|
|
317
|
+
outcome: "quiet",
|
|
318
|
+
freshness: observedAt > 0 ? "observed" : "not-observed",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
289
321
|
await new Promise((resolve) =>
|
|
290
322
|
setTimeout(resolve, Math.min(quietMs - elapsed, deadline - Date.now(), 50)),
|
|
291
323
|
);
|
|
292
324
|
}
|
|
325
|
+
return {
|
|
326
|
+
outcome: "timed-out",
|
|
327
|
+
freshness: this.#lastDiagnosticReceivedAfter(syncStart) > 0 ? "observed" : "not-observed",
|
|
328
|
+
};
|
|
293
329
|
}
|
|
294
330
|
|
|
295
331
|
#lastDiagnosticReceivedAfter(afterTime: number): number {
|
|
@@ -319,18 +355,18 @@ export class ClientDiagnostics {
|
|
|
319
355
|
this.#releaseDiagnosticWaiters(uri);
|
|
320
356
|
}
|
|
321
357
|
|
|
322
|
-
#waitForDiagnostics(uri: string, timeoutMs: number): Promise<
|
|
323
|
-
if (timeoutMs <= 0) return Promise.resolve();
|
|
358
|
+
#waitForDiagnostics(uri: string, timeoutMs: number): Promise<DiagnosticPushWaitOutcome> {
|
|
359
|
+
if (timeoutMs <= 0) return Promise.resolve("timed-out");
|
|
324
360
|
|
|
325
|
-
return new Promise<
|
|
326
|
-
const waiter = () => {
|
|
361
|
+
return new Promise<DiagnosticPushWaitOutcome>((resolve) => {
|
|
362
|
+
const waiter: DiagnosticWaiter = (outcome) => {
|
|
327
363
|
clearTimeout(timer);
|
|
328
364
|
this.#removeDiagnosticWaiter(uri, waiter);
|
|
329
|
-
resolve();
|
|
365
|
+
resolve(outcome);
|
|
330
366
|
};
|
|
331
367
|
const timer = setTimeout(() => {
|
|
332
368
|
this.#removeDiagnosticWaiter(uri, waiter);
|
|
333
|
-
resolve();
|
|
369
|
+
resolve("timed-out");
|
|
334
370
|
}, timeoutMs);
|
|
335
371
|
const waiters = this.#diagnosticWaiters.get(uri) ?? [];
|
|
336
372
|
waiters.push(waiter);
|
|
@@ -338,7 +374,7 @@ export class ClientDiagnostics {
|
|
|
338
374
|
});
|
|
339
375
|
}
|
|
340
376
|
|
|
341
|
-
#removeDiagnosticWaiter(uri: string, waiter:
|
|
377
|
+
#removeDiagnosticWaiter(uri: string, waiter: DiagnosticWaiter): void {
|
|
342
378
|
const waiters = this.#diagnosticWaiters.get(uri);
|
|
343
379
|
if (!waiters) return;
|
|
344
380
|
const next = waiters.filter((entry) => entry !== waiter);
|
|
@@ -352,10 +388,10 @@ export class ClientDiagnostics {
|
|
|
352
388
|
}
|
|
353
389
|
}
|
|
354
390
|
|
|
355
|
-
#releaseDiagnosticWaiters(uri: string): void {
|
|
391
|
+
#releaseDiagnosticWaiters(uri: string, outcome: "published" | "released" = "released"): void {
|
|
356
392
|
const waiters = this.#diagnosticWaiters.get(uri);
|
|
357
393
|
if (!waiters) return;
|
|
358
394
|
this.#diagnosticWaiters.delete(uri);
|
|
359
|
-
for (const waiter of waiters) waiter();
|
|
395
|
+
for (const waiter of waiters) waiter(outcome);
|
|
360
396
|
}
|
|
361
397
|
}
|