@mrclrchtr/supi-lsp 10.0.0 → 11.0.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 +88 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/LICENSE +21 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/README.md +743 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/index.js +17 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/constants.js +184 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/parse.js +1425 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/picomatch.js +361 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/scan.js +403 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/utils.js +72 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/package.json +83 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/posix.js +3 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +4 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +11 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/file-associations.ts +190 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +22 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/package.json +3 -3
- package/src/api.ts +1 -0
- package/src/client/client-diagnostic-host.ts +3 -0
- package/src/client/client-diagnostics.ts +6 -4
- package/src/client/client-request-enrollment.ts +66 -22
- package/src/client/client-semantic-input-barrier.ts +37 -5
- package/src/client/client.ts +136 -43
- package/src/manager/manager.ts +261 -22
- package/src/provider/refactor-planning.ts +13 -0
- package/src/provider/semantic-edit-discovery.ts +151 -0
- package/src/provider/semantic-edit-normalizer.ts +97 -129
- package/src/provider/semantic-edit-protocol.ts +132 -0
- package/src/session/runtime-controller.ts +8 -1
- package/src/session/runtime-registry.ts +7 -20
- package/src/session/runtime-rename.ts +172 -0
- package/src/session/workspace-lsp-runtime.ts +14 -0
- package/src/utils.ts +12 -11
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
|
|
4
|
+
/** Language selected for a C-family source file. */
|
|
5
|
+
export type CFamilyLanguage = "c" | "cpp";
|
|
6
|
+
|
|
7
|
+
/** One ordered, project-relative language rule for an existing C-family file. */
|
|
8
|
+
export interface CFamilyFileAssociationRule {
|
|
9
|
+
readonly pattern: string;
|
|
10
|
+
readonly language: CFamilyLanguage;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** C-family suffixes that already have Tree-sitter and LSP support. */
|
|
14
|
+
export const C_FAMILY_EXTENSIONS = Object.freeze([
|
|
15
|
+
".c",
|
|
16
|
+
".h",
|
|
17
|
+
".cpp",
|
|
18
|
+
".hpp",
|
|
19
|
+
".cc",
|
|
20
|
+
".cxx",
|
|
21
|
+
".hxx",
|
|
22
|
+
".c++",
|
|
23
|
+
".h++",
|
|
24
|
+
] as const);
|
|
25
|
+
|
|
26
|
+
export type CFamilyExtension = (typeof C_FAMILY_EXTENSIONS)[number];
|
|
27
|
+
|
|
28
|
+
const DEFAULT_C_FAMILY_LANGUAGES: Readonly<Record<CFamilyExtension, CFamilyLanguage>> =
|
|
29
|
+
Object.freeze({
|
|
30
|
+
".c": "c",
|
|
31
|
+
".h": "c",
|
|
32
|
+
".cpp": "cpp",
|
|
33
|
+
".hpp": "cpp",
|
|
34
|
+
".cc": "cpp",
|
|
35
|
+
".cxx": "cpp",
|
|
36
|
+
".hxx": "cpp",
|
|
37
|
+
".c++": "cpp",
|
|
38
|
+
".h++": "cpp",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** Immutable C-family language selection for one workspace. */
|
|
42
|
+
export interface CFamilyFileAssociationResolver {
|
|
43
|
+
/** Absolute root used to make project-relative paths. */
|
|
44
|
+
readonly workspaceRoot: string;
|
|
45
|
+
/** Validated ordered rules captured when the workspace host starts. */
|
|
46
|
+
readonly rules: readonly CFamilyFileAssociationRule[];
|
|
47
|
+
/** Select the configured language or the existing suffix default. */
|
|
48
|
+
languageForFile(filePath: string): CFamilyLanguage | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Return the current default language for one supported C-family suffix. */
|
|
52
|
+
export function defaultCFamilyLanguage(extension: string): CFamilyLanguage | undefined {
|
|
53
|
+
return DEFAULT_C_FAMILY_LANGUAGES[extension.toLowerCase() as CFamilyExtension];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate and compile ordered C-family file associations for one workspace.
|
|
58
|
+
* Patterns are case-sensitive globs over POSIX-style, workspace-relative paths.
|
|
59
|
+
* Supported patterns use `*`, `?`, `**`, character classes, and brace alternatives.
|
|
60
|
+
* The first matching rule wins. Rules never expand the eligible suffix set.
|
|
61
|
+
*
|
|
62
|
+
* @throws If the rule list, a rule, a language, or a pattern is invalid.
|
|
63
|
+
*/
|
|
64
|
+
export function createCFamilyFileAssociationResolver(
|
|
65
|
+
workspaceRoot: string,
|
|
66
|
+
input: unknown = [],
|
|
67
|
+
): CFamilyFileAssociationResolver {
|
|
68
|
+
const rules = validateRules(input);
|
|
69
|
+
const matchers = rules.map((rule, index) => ({
|
|
70
|
+
language: rule.language,
|
|
71
|
+
matches: compilePattern(rule.pattern, `code-intelligence.fileAssociations[${index}]`),
|
|
72
|
+
}));
|
|
73
|
+
const root = path.resolve(workspaceRoot);
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
workspaceRoot: root,
|
|
76
|
+
rules,
|
|
77
|
+
languageForFile(filePath: string): CFamilyLanguage | undefined {
|
|
78
|
+
const extension = normalizeExtension(filePath);
|
|
79
|
+
const fallback = defaultCFamilyLanguage(extension);
|
|
80
|
+
if (!fallback) return undefined;
|
|
81
|
+
|
|
82
|
+
const absolutePath = path.resolve(root, filePath);
|
|
83
|
+
const relativePath = toPosixPath(path.relative(root, absolutePath));
|
|
84
|
+
if (
|
|
85
|
+
relativePath === ".." ||
|
|
86
|
+
relativePath.startsWith("../") ||
|
|
87
|
+
path.posix.isAbsolute(relativePath)
|
|
88
|
+
) {
|
|
89
|
+
return fallback;
|
|
90
|
+
}
|
|
91
|
+
for (const rule of matchers) {
|
|
92
|
+
if (rule.matches(relativePath)) return rule.language;
|
|
93
|
+
}
|
|
94
|
+
return fallback;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateRules(input: unknown): readonly CFamilyFileAssociationRule[] {
|
|
100
|
+
if (!Array.isArray(input)) {
|
|
101
|
+
throw new Error('Invalid "code-intelligence.fileAssociations": expected an ordered array.');
|
|
102
|
+
}
|
|
103
|
+
if (input.length > 64) {
|
|
104
|
+
throw new Error('Invalid "code-intelligence.fileAssociations": use no more than 64 rules.');
|
|
105
|
+
}
|
|
106
|
+
const rules: CFamilyFileAssociationRule[] = [];
|
|
107
|
+
const seenPatterns = new Set<string>();
|
|
108
|
+
for (const [index, value] of input.entries()) {
|
|
109
|
+
const key = `code-intelligence.fileAssociations[${index}]`;
|
|
110
|
+
if (!isRecord(value) || !hasExactKeys(value, ["pattern", "language"])) {
|
|
111
|
+
throw new Error(`${key} must contain only "pattern" and "language" fields.`);
|
|
112
|
+
}
|
|
113
|
+
if (typeof value.pattern !== "string") {
|
|
114
|
+
throw new Error(`${key}.pattern must be a string.`);
|
|
115
|
+
}
|
|
116
|
+
if (value.language !== "c" && value.language !== "cpp") {
|
|
117
|
+
throw new Error(`${key}.language must be "c" or "cpp".`);
|
|
118
|
+
}
|
|
119
|
+
if (seenPatterns.has(value.pattern)) {
|
|
120
|
+
throw new Error(`${key}.pattern duplicates an earlier rule.`);
|
|
121
|
+
}
|
|
122
|
+
seenPatterns.add(value.pattern);
|
|
123
|
+
rules.push(Object.freeze({ pattern: value.pattern, language: value.language }));
|
|
124
|
+
}
|
|
125
|
+
return Object.freeze(rules);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function compilePattern(pattern: string, key: string): (file: string) => boolean {
|
|
129
|
+
if (
|
|
130
|
+
pattern.length === 0 ||
|
|
131
|
+
pattern.length > 512 ||
|
|
132
|
+
pattern !== pattern.trim() ||
|
|
133
|
+
pattern.includes("\\") ||
|
|
134
|
+
hasControlCharacter(pattern) ||
|
|
135
|
+
pattern.startsWith("/") ||
|
|
136
|
+
/^[A-Za-z]:/.test(pattern) ||
|
|
137
|
+
pattern.startsWith("!")
|
|
138
|
+
) {
|
|
139
|
+
throw new Error(`${key}.pattern must be a safe workspace-relative glob using "/" separators.`);
|
|
140
|
+
}
|
|
141
|
+
const segments = pattern.split("/");
|
|
142
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
143
|
+
throw new Error(`${key}.pattern contains an unsafe or unsupported path pattern.`);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return picomatch(pattern, {
|
|
147
|
+
nocase: false,
|
|
148
|
+
windows: false,
|
|
149
|
+
dot: true,
|
|
150
|
+
strictBrackets: true,
|
|
151
|
+
nonegate: true,
|
|
152
|
+
noextglob: true,
|
|
153
|
+
maxLength: 512,
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new Error(`${key}.pattern is not a valid glob: ${errorMessage(error)}`, { cause: error });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function hasControlCharacter(value: string): boolean {
|
|
161
|
+
for (const character of value) {
|
|
162
|
+
const code = character.charCodeAt(0);
|
|
163
|
+
if (code < 32 || code === 127) return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeExtension(filePath: string): string {
|
|
169
|
+
return path.posix.extname(toPosixPath(filePath)).toLowerCase();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toPosixPath(value: string): string {
|
|
173
|
+
return value.replaceAll("\\", "/");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
177
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
181
|
+
const allowed = new Set(keys);
|
|
182
|
+
return (
|
|
183
|
+
keys.every((key) => Object.hasOwn(value, key)) &&
|
|
184
|
+
Object.keys(value).every((key) => allowed.has(key))
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function errorMessage(error: unknown): string {
|
|
189
|
+
return error instanceof Error ? error.message : String(error);
|
|
190
|
+
}
|
|
@@ -80,6 +80,14 @@ export interface DocumentCodeSymbol extends CodeSymbol {
|
|
|
80
80
|
*/
|
|
81
81
|
export type CodeResult<T> =
|
|
82
82
|
| { kind: "success"; data: T }
|
|
83
|
+
| {
|
|
84
|
+
kind: "partial";
|
|
85
|
+
data: T;
|
|
86
|
+
file: string;
|
|
87
|
+
reason: string;
|
|
88
|
+
/** Syntax error and missing-node ranges in 0-based UTF-16 coordinates. */
|
|
89
|
+
errorRanges: SourceRange[];
|
|
90
|
+
}
|
|
83
91
|
| { kind: "unsupported-language"; file: string; message: string }
|
|
84
92
|
| { kind: "file-access-error"; file: string; message: string }
|
|
85
93
|
| { kind: "validation-error"; message: string }
|
|
@@ -93,7 +101,10 @@ export type ConfidenceMode = "semantic" | "structural" | "heuristic" | "unavaila
|
|
|
93
101
|
|
|
94
102
|
export interface OutlineData {
|
|
95
103
|
name: string;
|
|
104
|
+
/** Common structural search category. */
|
|
96
105
|
kind: string;
|
|
106
|
+
/** Source-language declaration kind, separate from common search category. */
|
|
107
|
+
nativeKind?: string;
|
|
97
108
|
/** Exact 1-based start of the declaration name token, when known. */
|
|
98
109
|
nameAnchor?: SymbolAnchor;
|
|
99
110
|
/** Exclusive 1-based end of a verified literal name span, when known. */
|
|
@@ -156,6 +167,12 @@ export interface FileEdit {
|
|
|
156
167
|
/** A document-state precondition established when semantic edits are normalized. */
|
|
157
168
|
export type DocumentEditPrecondition =
|
|
158
169
|
| { file: string; kind: "open-document-version"; version: number }
|
|
170
|
+
| {
|
|
171
|
+
file: string;
|
|
172
|
+
kind: "open-document-content";
|
|
173
|
+
version: number;
|
|
174
|
+
fingerprint: string;
|
|
175
|
+
}
|
|
159
176
|
| { file: string; kind: "disk-content" };
|
|
160
177
|
|
|
161
178
|
/**
|
|
@@ -257,5 +274,10 @@ export interface CalleesData {
|
|
|
257
274
|
/** A single call-site match with name and start line. */
|
|
258
275
|
export interface CallSite {
|
|
259
276
|
name: string;
|
|
277
|
+
/** 1-based UTF-16 start position of the written callee expression. */
|
|
260
278
|
startLine: number;
|
|
279
|
+
startCharacter: number;
|
|
280
|
+
/** Exclusive 1-based UTF-16 end position of the captured source expression. */
|
|
281
|
+
endLine: number;
|
|
282
|
+
endCharacter: number;
|
|
261
283
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-lsp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.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.18.0",
|
|
39
39
|
"vscode-languageserver-types": "^3.18.0",
|
|
40
|
-
"@mrclrchtr/supi-code-runtime": "
|
|
41
|
-
"@mrclrchtr/supi-core": "
|
|
40
|
+
"@mrclrchtr/supi-code-runtime": "11.0.0",
|
|
41
|
+
"@mrclrchtr/supi-core": "11.0.0"
|
|
42
42
|
},
|
|
43
43
|
"bundledDependencies": [
|
|
44
44
|
"@mrclrchtr/supi-code-runtime",
|
package/src/api.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CFamilyFileAssociationResolver } from "@mrclrchtr/supi-code-runtime/api";
|
|
1
2
|
import type { DiagnosticRequestAdapter } from "./client-diagnostic-request.ts";
|
|
2
3
|
import type { NormalizedDocumentSync } from "./client-document-sync.ts";
|
|
3
4
|
|
|
@@ -7,6 +8,8 @@ export interface ClientDiagnosticsHost {
|
|
|
7
8
|
server: string;
|
|
8
9
|
/** Absolute workspace root, for debug-telemetry identity. */
|
|
9
10
|
cwd?: string;
|
|
11
|
+
/** Immutable workspace association snapshot used for didOpen document IDs. */
|
|
12
|
+
fileAssociations?: CFamilyFileAssociationResolver;
|
|
10
13
|
isOperational(): boolean;
|
|
11
14
|
/** Native pull and server-specific request evidence share this priority adapter. */
|
|
12
15
|
diagnosticRequestAdapter: DiagnosticRequestAdapter;
|
|
@@ -51,6 +51,7 @@ import { SemanticInputRequestRetryGuard } from "./client-request-enrollment.ts";
|
|
|
51
51
|
import {
|
|
52
52
|
SemanticInputBarrier,
|
|
53
53
|
type SemanticInputDocument,
|
|
54
|
+
type SemanticInputRevision,
|
|
54
55
|
type SemanticInputSnapshot,
|
|
55
56
|
type SemanticInputUpdate,
|
|
56
57
|
} from "./client-semantic-input-barrier.ts";
|
|
@@ -156,10 +157,10 @@ export class ClientDiagnostics {
|
|
|
156
157
|
|
|
157
158
|
/** Fail closed when a semantic request observes a changed input generation. */
|
|
158
159
|
assertSemanticInputsCurrent(
|
|
159
|
-
snapshot:
|
|
160
|
+
snapshot: SemanticInputRevision,
|
|
160
161
|
control?: CodeRequestControl,
|
|
161
162
|
contentOverrides?: ReadonlyMap<string, string>,
|
|
162
|
-
): Promise<
|
|
163
|
+
): Promise<SemanticInputSnapshot> {
|
|
163
164
|
throwIfCodeRequestInterrupted(control);
|
|
164
165
|
return this.#inputBarrier.assertCurrent(snapshot, control, contentOverrides);
|
|
165
166
|
}
|
|
@@ -168,6 +169,7 @@ export class ClientDiagnostics {
|
|
|
168
169
|
return Array.from(this.#openDocs, ([uri, document]) => ({
|
|
169
170
|
uri,
|
|
170
171
|
filePath: uriToFile(uri),
|
|
172
|
+
version: document.version,
|
|
171
173
|
content: document.content,
|
|
172
174
|
contentFingerprint: document.contentFingerprint,
|
|
173
175
|
}));
|
|
@@ -293,7 +295,7 @@ export class ClientDiagnostics {
|
|
|
293
295
|
this.#cancelDiagnosticRequest(uri);
|
|
294
296
|
this.#advanceInputRevision(false, false, "enrollment");
|
|
295
297
|
|
|
296
|
-
const languageId = detectLanguageId(filePath);
|
|
298
|
+
const languageId = detectLanguageId(filePath, this.host.fileAssociations);
|
|
297
299
|
this.#waiters.cancelSettle();
|
|
298
300
|
// A didOpen is a synchronization: pushes the server sends after it are
|
|
299
301
|
// responses to the open and can become fresh evidence (ADR 0020).
|
|
@@ -797,7 +799,7 @@ export class ClientDiagnostics {
|
|
|
797
799
|
|
|
798
800
|
async #verifyDiagnosticInputs(options: {
|
|
799
801
|
result: CodeQueryResult<Diagnostic[]>;
|
|
800
|
-
snapshot:
|
|
802
|
+
snapshot: SemanticInputRevision;
|
|
801
803
|
control: CodeRequestControl | undefined;
|
|
802
804
|
contentOverrides: ReadonlyMap<string, string> | undefined;
|
|
803
805
|
uri: string;
|
|
@@ -3,30 +3,61 @@ import {
|
|
|
3
3
|
type SemanticInputSynchronizationError,
|
|
4
4
|
} from "./client-semantic-input-errors.ts";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const MAX_REQUEST_RETRIES = 1;
|
|
7
|
+
const CONTENT_MODIFIED_ERROR_CODE = -32_801;
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
export type
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/** Reason that permits one request retry. */
|
|
10
|
+
export type SemanticInputRequestRetryReason = "content-modified" | "enrollment";
|
|
11
|
+
|
|
12
|
+
/** A request-level retry decision owned by one attempt. */
|
|
13
|
+
export type SemanticInputRequestRetryDecision =
|
|
14
|
+
| { readonly kind: "retry"; readonly reason: SemanticInputRequestRetryReason }
|
|
15
|
+
| {
|
|
16
|
+
readonly kind: "stop";
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
readonly reason?: SemanticInputRequestRetryReason;
|
|
19
|
+
};
|
|
12
20
|
|
|
13
21
|
/** Look up the current input change after a failed request revision. */
|
|
14
22
|
export type SemanticInputChangeLookup = (
|
|
15
23
|
revision: number,
|
|
16
24
|
) => SemanticInputSynchronizationError | undefined;
|
|
17
25
|
|
|
26
|
+
/** Optional request retry policies. ContentModified retries stay off by default. */
|
|
27
|
+
export interface SemanticInputRequestRetryPolicy {
|
|
28
|
+
readonly retryContentModified?: boolean;
|
|
29
|
+
/** Retain a verified revision across document enrollment and a follow-up request. */
|
|
30
|
+
readonly inputRevision?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isContentModifiedError(error: unknown): boolean {
|
|
34
|
+
return (
|
|
35
|
+
typeof error === "object" &&
|
|
36
|
+
error !== null &&
|
|
37
|
+
"code" in error &&
|
|
38
|
+
(error as { code?: unknown }).code === CONTENT_MODIFIED_ERROR_CODE
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
/**
|
|
19
|
-
* Keep one
|
|
43
|
+
* Keep one LSP request's input retry policy in one place.
|
|
20
44
|
*
|
|
21
|
-
* Synchronization passes have their own rejoin budget. This guard
|
|
22
|
-
*
|
|
23
|
-
*
|
|
45
|
+
* Synchronization passes have their own rejoin budget. This guard owns one
|
|
46
|
+
* shared request retry budget and a protected revision used to classify later
|
|
47
|
+
* changes. A caller can retain an earlier verified revision across enrollment.
|
|
48
|
+
* Protocol retries are disabled unless the caller opts in.
|
|
24
49
|
*/
|
|
25
50
|
export class SemanticInputRequestRetryGuard {
|
|
26
51
|
#requestRetries = 0;
|
|
27
|
-
#
|
|
52
|
+
#protectedRevision: number | undefined;
|
|
53
|
+
#retryReason: SemanticInputRequestRetryReason | undefined;
|
|
28
54
|
|
|
29
|
-
constructor(
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly currentChangeSince: SemanticInputChangeLookup,
|
|
57
|
+
private readonly policy: SemanticInputRequestRetryPolicy = {},
|
|
58
|
+
) {
|
|
59
|
+
this.#protectedRevision = policy.inputRevision;
|
|
60
|
+
}
|
|
30
61
|
|
|
31
62
|
/** Number of request-level retries permitted for this request so far. */
|
|
32
63
|
get retryCount(): number {
|
|
@@ -38,6 +69,11 @@ export class SemanticInputRequestRetryGuard {
|
|
|
38
69
|
return this.#requestRetries > 0;
|
|
39
70
|
}
|
|
40
71
|
|
|
72
|
+
/** Cause of the retry already used by this request, if any. */
|
|
73
|
+
get retryReason(): SemanticInputRequestRetryReason | undefined {
|
|
74
|
+
return this.#retryReason;
|
|
75
|
+
}
|
|
76
|
+
|
|
41
77
|
/** Throw the current non-enrollment cause when it blocks a retry. */
|
|
42
78
|
assertCurrent(): void {
|
|
43
79
|
const blockingChange = this.#getBlockingChange();
|
|
@@ -45,31 +81,39 @@ export class SemanticInputRequestRetryGuard {
|
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
/** Classify one failed attempt and decide whether it may be retried. */
|
|
48
|
-
decide(error: unknown, inputRevision: number | undefined):
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
84
|
+
decide(error: unknown, inputRevision: number | undefined): SemanticInputRequestRetryDecision {
|
|
85
|
+
const reason = this.#getRetryReason(error);
|
|
86
|
+
if (!reason) return { kind: "stop", error };
|
|
52
87
|
|
|
53
|
-
if (this.#
|
|
88
|
+
if (this.#protectedRevision !== undefined) {
|
|
54
89
|
const blockingChange = this.#getBlockingChange();
|
|
55
90
|
if (blockingChange) return { kind: "stop", error: blockingChange };
|
|
56
91
|
}
|
|
57
92
|
if (inputRevision === undefined) return { kind: "stop", error };
|
|
58
93
|
|
|
59
|
-
this.#
|
|
94
|
+
this.#protectedRevision ??= inputRevision;
|
|
60
95
|
const blockingChange = this.#getBlockingChange();
|
|
61
96
|
if (blockingChange) return { kind: "stop", error: blockingChange };
|
|
62
|
-
if (this.#requestRetries >=
|
|
63
|
-
return { kind: "stop", error };
|
|
97
|
+
if (this.#requestRetries >= MAX_REQUEST_RETRIES) {
|
|
98
|
+
return { kind: "stop", error, reason };
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
this.#requestRetries++;
|
|
67
|
-
|
|
102
|
+
this.#retryReason ??= reason;
|
|
103
|
+
return { kind: "retry", reason };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#getRetryReason(error: unknown): SemanticInputRequestRetryReason | undefined {
|
|
107
|
+
if (isSemanticInputEnrollmentError(error)) return "enrollment";
|
|
108
|
+
if (this.policy.retryContentModified && isContentModifiedError(error)) {
|
|
109
|
+
return "content-modified";
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
68
112
|
}
|
|
69
113
|
|
|
70
114
|
#getBlockingChange(): SemanticInputSynchronizationError | undefined {
|
|
71
|
-
if (this.#
|
|
72
|
-
const currentChange = this.currentChangeSince(this.#
|
|
115
|
+
if (this.#protectedRevision === undefined) return undefined;
|
|
116
|
+
const currentChange = this.currentChangeSince(this.#protectedRevision);
|
|
73
117
|
return currentChange?.changeKind === "enrollment" ? undefined : currentChange;
|
|
74
118
|
}
|
|
75
119
|
}
|
|
@@ -21,10 +21,19 @@ const DEFAULT_MAX_CONCURRENT_READS = 16;
|
|
|
21
21
|
export interface SemanticInputDocument {
|
|
22
22
|
readonly uri: string;
|
|
23
23
|
readonly filePath: string;
|
|
24
|
+
readonly version: number;
|
|
24
25
|
readonly content: string;
|
|
25
26
|
readonly contentFingerprint: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** One open buffer and disk fingerprint verified by an input synchronization pass. */
|
|
30
|
+
export interface SemanticInputDocumentSnapshot {
|
|
31
|
+
readonly filePath: string;
|
|
32
|
+
readonly version: number;
|
|
33
|
+
readonly contentFingerprint: string;
|
|
34
|
+
readonly diskContentFingerprint: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
/** One open document update selected by the barrier after a full read. */
|
|
29
38
|
export interface SemanticInputUpdate {
|
|
30
39
|
readonly document: SemanticInputDocument;
|
|
@@ -48,11 +57,16 @@ export interface SemanticInputBarrierOptions {
|
|
|
48
57
|
readonly readFile?: SemanticInputFileReader;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
/** Revision token
|
|
52
|
-
export interface
|
|
60
|
+
/** Revision token used to check one semantic input generation. */
|
|
61
|
+
export interface SemanticInputRevision {
|
|
53
62
|
readonly revision: number;
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
/** Verified inputs returned by one complete synchronization pass. */
|
|
66
|
+
export interface SemanticInputSnapshot extends SemanticInputRevision {
|
|
67
|
+
readonly documents: readonly SemanticInputDocumentSnapshot[];
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
interface OpenDocumentRead {
|
|
57
71
|
readonly document: SemanticInputDocument;
|
|
58
72
|
readonly result:
|
|
@@ -150,10 +164,10 @@ export class SemanticInputBarrier {
|
|
|
150
164
|
|
|
151
165
|
/** Recheck the full input set after a semantic request completes. */
|
|
152
166
|
async assertCurrent(
|
|
153
|
-
snapshot:
|
|
167
|
+
snapshot: SemanticInputRevision,
|
|
154
168
|
control?: CodeRequestControl,
|
|
155
169
|
contentOverrides?: ReadonlyMap<string, string>,
|
|
156
|
-
): Promise<
|
|
170
|
+
): Promise<SemanticInputSnapshot> {
|
|
157
171
|
if (!this.#host.isOperational()) {
|
|
158
172
|
throw new Error("Semantic input synchronization is unavailable because the client stopped.");
|
|
159
173
|
}
|
|
@@ -176,6 +190,7 @@ export class SemanticInputBarrier {
|
|
|
176
190
|
)
|
|
177
191
|
);
|
|
178
192
|
}
|
|
193
|
+
return current;
|
|
179
194
|
}
|
|
180
195
|
|
|
181
196
|
/** Return the current typed input change after a revision, when present. */
|
|
@@ -305,7 +320,24 @@ export class SemanticInputBarrier {
|
|
|
305
320
|
// resulting generation as owned by this completed pass so a late caller
|
|
306
321
|
// can join it instead of starting a redundant read pass.
|
|
307
322
|
pending.generation = this.#revision;
|
|
308
|
-
|
|
323
|
+
const currentDocuments = this.#host.getOpenDocuments();
|
|
324
|
+
const diskFingerprints = new Map(plan.observedDiskFingerprints);
|
|
325
|
+
return {
|
|
326
|
+
revision: this.#revision,
|
|
327
|
+
documents: currentDocuments.flatMap((document) => {
|
|
328
|
+
const diskContentFingerprint = diskFingerprints.get(document.uri);
|
|
329
|
+
return diskContentFingerprint === undefined
|
|
330
|
+
? []
|
|
331
|
+
: [
|
|
332
|
+
{
|
|
333
|
+
filePath: document.filePath,
|
|
334
|
+
version: document.version,
|
|
335
|
+
contentFingerprint: document.contentFingerprint,
|
|
336
|
+
diskContentFingerprint,
|
|
337
|
+
},
|
|
338
|
+
];
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
309
341
|
}
|
|
310
342
|
|
|
311
343
|
#assertPendingCanRun(pending: PendingSynchronization): void {
|