@mrclrchtr/supi-lsp 11.4.0 → 12.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.
@@ -1,14 +1,28 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import type {
3
4
  CodePosition,
4
5
  CodeRequestControl,
6
+ DisambiguationCandidate,
7
+ RefactorActionIdentity,
5
8
  RefactorResult,
6
9
  SourceRange,
7
10
  } from "@mrclrchtr/supi-code-runtime/api";
8
11
  import { CodeActionTriggerKind } from "vscode-languageserver-types";
9
- import { codeActionKindsForOperation } from "../config/operation-support.ts";
12
+ import { isCodeActionKindInRange, isCodeActionKindWithin } from "../config/operation-support.ts";
10
13
  import type { CodeAction } from "../config/types.ts";
11
- import type { WorkspaceLspRuntime } from "../session/runtime-registry.ts";
14
+ import { raceRequestControl } from "../session/readiness.ts";
15
+ import type { RoutedCodeActionResponse, WorkspaceLspRuntime } from "../session/runtime-registry.ts";
16
+ import {
17
+ COMMAND_ONLY_ACTION_REASON,
18
+ captureCommandAction,
19
+ captureEligibleActions,
20
+ commandCandidates,
21
+ isCommandOnlyAction,
22
+ } from "./refactor-command-capture.ts";
23
+
24
+ export { COMMAND_ONLY_ACTION_REASON };
25
+
12
26
  import {
13
27
  normalizeSemanticEdit,
14
28
  type SemanticEditNormalizationContext,
@@ -70,48 +84,135 @@ export function collectCodeActionResults(
70
84
  return results;
71
85
  }
72
86
 
73
- export async function runFilteredCodeActionRefactor(options: {
87
+ /** Options for one code- or source-action planning request. */
88
+ export interface CodeActionRefactorOptions {
74
89
  lsp: WorkspaceLspRuntime;
75
90
  file: string;
76
- position: CodePosition;
77
- operation: "update_imports" | "delete_dead_code";
78
- range?: SourceRange;
79
- matches: (action: CodeAction) => boolean;
91
+ range: SourceRange;
92
+ kind: string;
93
+ title?: string;
94
+ /** Operation label used in unavailable reasons. */
95
+ label: "Code action" | "Source action";
80
96
  control?: CodeRequestControl;
81
- }): Promise<RefactorResult> {
82
- const { lsp, file, position, operation, matches } = options;
83
- const response = await lsp.codeActions(file, options.range ?? position, options.control, {
84
- only: codeActionKindsForOperation(operation),
97
+ }
98
+
99
+ /**
100
+ * Plan one code action of the requested kind over a file range.
101
+ *
102
+ * `kind` matches itself and its dotted sub-kinds, per the LSP CodeActionKind
103
+ * hierarchy. Disabled actions never match. `title` narrows matches to one exact
104
+ * title. Distinct precise plans stay ambiguous. One eligible command-only
105
+ * action runs once so its pushed edit can be captured; see
106
+ * `refactor-command-capture.ts`.
107
+ */
108
+ export async function runCodeActionRefactor(
109
+ options: CodeActionRefactorOptions,
110
+ ): Promise<RefactorResult> {
111
+ return (await attemptCodeActionRefactor(options)).result;
112
+ }
113
+
114
+ /** Delays before re-requesting a source action that returned no action of the kind. */
115
+ const SOURCE_ACTION_RETRY_DELAYS_MS = [500, 1000] as const;
116
+
117
+ /**
118
+ * Plan a whole-file source action, retrying briefly when a server that
119
+ * advertises the requested kind returns no action of it.
120
+ *
121
+ * typescript-language-server returns no fix-all or unused-code source actions
122
+ * while diagnostics for a just-opened or just-changed file are pending, and
123
+ * waits itself about 500 ms in the equivalent quick-fix path.
124
+ */
125
+ export async function runSourceActionRefactor(
126
+ options: CodeActionRefactorOptions,
127
+ ): Promise<RefactorResult> {
128
+ let attempt = await attemptCodeActionRefactor(options);
129
+ for (const delayMs of SOURCE_ACTION_RETRY_DELAYS_MS) {
130
+ if (!attempt.retryable) break;
131
+ await raceRequestControl(
132
+ new Promise((resolve) => setTimeout(resolve, delayMs)),
133
+ options.control,
134
+ );
135
+ attempt = await attemptCodeActionRefactor(options);
136
+ }
137
+ return attempt.result;
138
+ }
139
+
140
+ /**
141
+ * One planning attempt. `retryable` marks a zero-match response from a server
142
+ * that advertises the requested kind, so a later request may still return it.
143
+ */
144
+ async function attemptCodeActionRefactor(
145
+ options: CodeActionRefactorOptions,
146
+ ): Promise<{ result: RefactorResult; retryable: boolean }> {
147
+ const { lsp, file, range, kind, title, label } = options;
148
+ const response = await lsp.codeActions(file, range, options.control, {
149
+ only: [kind],
85
150
  triggerKind: CodeActionTriggerKind.Invoked,
86
151
  });
87
- if (!response || response.reason) {
152
+ if (!response) {
88
153
  return {
89
- kind: "unavailable",
90
- reason: `Refactor operation "${operation}" is unavailable: ${response?.reason ?? "No code actions are available."}`,
154
+ result: {
155
+ kind: "unavailable",
156
+ reason: `${label} "${kind}" is unavailable: no routed LSP client.`,
157
+ },
158
+ retryable: false,
91
159
  };
92
160
  }
93
- const actions = response.value;
94
- if (!actions || actions.length === 0) {
95
- return {
96
- kind: "unavailable",
97
- reason: `No code actions are available for refactor operation "${operation}".`,
98
- };
161
+ const roots = [...response.authorizedMutationRoots];
162
+ const unavailable = (reason: string, retryable = false) => ({
163
+ result: { kind: "unavailable" as const, reason, authorizedMutationRoots: roots },
164
+ retryable,
165
+ });
166
+ if (response.reason || !response.value) {
167
+ return unavailable(
168
+ `${label} "${kind}" is unavailable: ${response.reason ?? "no code actions were returned."}`,
169
+ );
99
170
  }
100
171
 
101
- const matching = actions.filter(matches);
172
+ const ofKind = response.value
173
+ .filter(isTitledCodeAction)
174
+ .filter(
175
+ (action) => isCodeActionKindWithin(action.kind, kind) && !Object.hasOwn(action, "disabled"),
176
+ );
177
+ if (ofKind.length === 0) {
178
+ const advertised = advertisedFamilyKinds(response.advertisedKinds, kind);
179
+ return unavailable(
180
+ noKindMatchReason(kind, advertised),
181
+ advertised.kinds.some((candidate) => isCodeActionKindInRange(candidate, kind)),
182
+ );
183
+ }
184
+ const matching = title === undefined ? ofKind : ofKind.filter((action) => action.title === title);
102
185
  if (matching.length === 0) {
103
- return {
104
- kind: "unavailable",
105
- reason: `No matching precise code action is available for refactor operation "${operation}".`,
106
- };
186
+ const titles = [...new Set(ofKind.map((action) => `"${action.title}"`))].join(", ");
187
+ return unavailable(
188
+ `No "${kind}" code action has the title "${title}". Available titles: ${titles}.`,
189
+ );
107
190
  }
108
191
 
109
- const context: SemanticEditNormalizationContext = {
192
+ return { result: await planMatchingActions(options, matching, response), retryable: false };
193
+ }
194
+
195
+ /** Turn the matching actions into one plan, candidates, or an unavailable reason. */
196
+ async function planMatchingActions(
197
+ options: CodeActionRefactorOptions,
198
+ matching: CodeAction[],
199
+ response: RoutedCodeActionResponse,
200
+ ): Promise<RefactorResult> {
201
+ const { lsp, file, kind } = options;
202
+ const roots = [...response.authorizedMutationRoots];
203
+ const unavailable = (reason: string): RefactorResult => ({
204
+ kind: "unavailable",
205
+ reason,
206
+ authorizedMutationRoots: roots,
207
+ });
208
+ const plans = collectPrecisePlans(matching, {
110
209
  getOpenDocumentVersion: (candidate) => lsp.getOpenDocumentVersion(candidate),
111
210
  authorizedMutationRoots: response.authorizedMutationRoots,
112
- };
113
- const plans = collectPrecisePlans(matching, context);
114
- if (plans.precise.length === 1) return plans.precise[0].result;
211
+ });
212
+ const [single] = plans.precise;
213
+ if (plans.precise.length === 1 && single) {
214
+ return { ...single.result, action: { ...actionIdentity(single.action), provenance: "edit" } };
215
+ }
115
216
  if (plans.precise.length > 1) {
116
217
  return {
117
218
  kind: "ambiguous",
@@ -120,26 +221,84 @@ export async function runFilteredCodeActionRefactor(options: {
120
221
  ),
121
222
  };
122
223
  }
123
- return {
124
- kind: "unavailable",
125
- reason: plans.unavailableReason
224
+ if (matching.every(isCommandOnlyAction)) {
225
+ const eligible = captureEligibleActions(matching, response.advertisedCommands);
226
+ const [only] = eligible;
227
+ if (eligible.length === 1 && only) {
228
+ return captureCommandAction({ lsp, file, action: only, control: options.control });
229
+ }
230
+ return eligible.length > 1
231
+ ? commandCandidates(eligible)
232
+ : unavailable(COMMAND_ONLY_ACTION_REASON);
233
+ }
234
+ return unavailable(
235
+ plans.unavailableReason
126
236
  ? `Matching code action is unavailable: ${plans.unavailableReason}`
127
- : `Matching code actions for refactor operation "${operation}" did not produce precise edits.`,
128
- };
237
+ : `Matching "${kind}" code actions did not produce precise edits.`,
238
+ );
129
239
  }
130
240
 
131
- export function isUpdateImportsCodeAction(action: CodeAction): boolean {
132
- const kind = typeof action?.kind === "string" ? action.kind : "";
133
- return kind === "source.organizeImports" || kind.startsWith("source.organizeImports.");
241
+ /** Compute the zero-based UTF-16 range that covers a file's full current disk text. */
242
+ export function wholeDocumentRange(file: string): SourceRange | null {
243
+ let text: string;
244
+ try {
245
+ text = readFileSync(file, "utf-8");
246
+ } catch {
247
+ return null;
248
+ }
249
+ const lines = text.split(/\r\n|\r|\n/);
250
+ return {
251
+ start: { line: 0, character: 0 },
252
+ end: { line: lines.length - 1, character: lines.at(-1)?.length ?? 0 },
253
+ };
134
254
  }
135
255
 
136
- export function isDeleteDeadCodeCodeAction(action: CodeAction): boolean {
137
- const kind = typeof action?.kind === "string" ? action.kind : "";
138
- return codeActionKindsForOperation("delete_dead_code").some(
139
- (requested) => kind === requested || kind.startsWith(`${requested}.`),
256
+ function isTitledCodeAction(value: unknown): value is CodeAction {
257
+ return (
258
+ typeof value === "object" &&
259
+ value !== null &&
260
+ typeof (value as { title?: unknown }).title === "string"
140
261
  );
141
262
  }
142
263
 
264
+ /** Advertised kinds in one CodeActionKind family, such as `source*`. */
265
+ interface AdvertisedFamilyKinds {
266
+ /** Listed non-empty kinds in the requested kind's top-level family. */
267
+ readonly kinds: readonly string[];
268
+ /** The server lists CodeActionKind.Empty or no kinds, so it may return any kind. */
269
+ readonly anyKind: boolean;
270
+ }
271
+
272
+ function advertisedFamilyKinds(
273
+ advertised: readonly string[] | undefined,
274
+ kind: string,
275
+ ): AdvertisedFamilyKinds {
276
+ const family = kind.split(".")[0] ?? kind;
277
+ return {
278
+ kinds: (advertised ?? []).filter(
279
+ (candidate) => candidate !== "" && isCodeActionKindWithin(candidate, family),
280
+ ),
281
+ anyKind: !advertised || advertised.includes(""),
282
+ };
283
+ }
284
+
285
+ function noKindMatchReason(kind: string, advertised: AdvertisedFamilyKinds): string {
286
+ const parts = [`No code action matches kind "${kind}" here.`];
287
+ if (advertised.kinds.length > 0) {
288
+ parts.push(`Server-advertised kinds: ${advertised.kinds.join(", ")}.`);
289
+ }
290
+ if (advertised.anyKind) {
291
+ parts.push("The server may return any code-action kind but returned none of this kind.");
292
+ } else if (advertised.kinds.length === 0) {
293
+ parts.push("The server advertises no kinds in this family.");
294
+ }
295
+ return parts.join(" ");
296
+ }
297
+
298
+ function actionIdentity(action: CodeAction): RefactorActionIdentity {
299
+ return { kind: typeof action.kind === "string" ? action.kind : "", title: action.title };
300
+ }
301
+
143
302
  interface PreciseCodeActionPlan {
144
303
  action: CodeAction;
145
304
  result: Extract<RefactorResult, { kind: "precise" }>;
@@ -187,18 +346,14 @@ function refactorCandidate(
187
346
  result: Extract<RefactorResult, { kind: "precise" }>,
188
347
  index: number,
189
348
  plans: readonly { action: CodeAction }[],
190
- ): {
191
- description: string;
192
- file?: string;
193
- line?: number;
194
- character?: number;
195
- } {
349
+ ): DisambiguationCandidate {
196
350
  const firstEdit = result.edits.edits[0];
197
351
  const sameTitleCount = plans.filter((plan) => plan.action.title === action.title).length;
198
352
  const description =
199
353
  sameTitleCount > 1 ? `${action.title} (alternative ${index + 1})` : action.title;
200
354
  return {
201
355
  description,
356
+ ...actionIdentity(action),
202
357
  ...(firstEdit
203
358
  ? {
204
359
  file: firstEdit.file,
@@ -0,0 +1,26 @@
1
+ /** Project-root boundary for mutation requests on one routed client. */
2
+
3
+ import { realpathSync } from "node:fs";
4
+ import { isWithinOrEqual } from "@mrclrchtr/supi-core/project";
5
+
6
+ /**
7
+ * Return why a file cannot be refactored by the client routed to it, or null.
8
+ *
9
+ * A file without its own project markers routes to a server rooted at the
10
+ * session directory. Its edits would fail the apply-time root check, so refuse
11
+ * before the request. Real paths are compared, so a link inside the root
12
+ * cannot reach an outside file.
13
+ */
14
+ export function outsideRouteRootReason(file: string, root: string): string | null {
15
+ return isWithinOrEqual(canonical(root), canonical(file))
16
+ ? null
17
+ : `File ${file} is outside the language server's project root ${root}.`;
18
+ }
19
+
20
+ function canonical(file: string): string {
21
+ try {
22
+ return realpathSync(file);
23
+ } catch {
24
+ return file;
25
+ }
26
+ }
@@ -18,6 +18,8 @@ import {
18
18
  import { resolveToolPath as resolveSessionPath } from "@mrclrchtr/supi-core/path";
19
19
  import { createSessionStateRegistry } from "@mrclrchtr/supi-core/session";
20
20
  import { CodeActionTriggerKind } from "vscode-languageserver-types";
21
+ import type { CommandEditCaptureResult } from "../client/client-edit-capture.ts";
22
+ import { advertisedCodeActionKinds } from "../config/operation-support.ts";
21
23
  import type {
22
24
  CodeAction,
23
25
  Diagnostic,
@@ -39,7 +41,12 @@ import type {
39
41
  WorkspaceSentinelSyncResult,
40
42
  } from "../diagnostics/workspace-sentinels.ts";
41
43
  import type { LspManager } from "../manager/manager.ts";
44
+ import {
45
+ COMMAND_ONLY_ACTION_REASON,
46
+ captureEligibleActions,
47
+ } from "../provider/refactor-command-capture.ts";
42
48
  import { raceReadinessValue } from "./readiness.ts";
49
+ import { outsideRouteRootReason } from "./route-boundary.ts";
43
50
  import type {
44
51
  DiagnosticEvidenceSummary,
45
52
  ProcessCrashRecoveryReport,
@@ -49,6 +56,7 @@ import type {
49
56
  import { runRoutedRename } from "./runtime-rename.ts";
50
57
  import type {
51
58
  BulkTrackFilesResult,
59
+ RoutedCodeActionResponse,
52
60
  RoutedMutationResponse,
53
61
  SemanticReadinessResult,
54
62
  WorkspaceCodeActionRequestOptions,
@@ -56,6 +64,9 @@ import type {
56
64
  WorkspaceLspRuntimeState,
57
65
  } from "./workspace-lsp-runtime.ts";
58
66
 
67
+ /** Upper bound for a server command run to capture its pushed edit. */
68
+ const COMMAND_CAPTURE_TIMEOUT_MS = 10_000;
69
+
59
70
  export type {
60
71
  ProcessCrashDiagnosticDemand,
61
72
  WorkspaceLspDiagnosticSurface,
@@ -84,6 +95,7 @@ export {
84
95
  export type {
85
96
  BulkTrackFileOutcome,
86
97
  BulkTrackFilesResult,
98
+ RoutedCodeActionResponse,
87
99
  RoutedMutationDocumentSnapshot,
88
100
  RoutedMutationResponse,
89
101
  SemanticReadinessResult,
@@ -288,12 +300,39 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
288
300
  return this.manager.getOpenDocumentVersion(this.resolveFilePath(filePath));
289
301
  }
290
302
 
303
+ async captureCodeActionEdit(
304
+ filePath: string,
305
+ action: CodeAction,
306
+ control?: CodeRequestControl,
307
+ ): Promise<RoutedMutationResponse<CommandEditCaptureResult | null> | null> {
308
+ const resolvedPath = this.resolveFilePath(filePath);
309
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
310
+ recoverProcessCrash: true,
311
+ protectInputs: true,
312
+ control,
313
+ });
314
+ if (!client) return null;
315
+ const authorizedMutationRoots = [client.root];
316
+ const outside = outsideRouteRootReason(resolvedPath, client.root);
317
+ if (outside) return { value: null, authorizedMutationRoots, reason: outside };
318
+ const listed = client.serverCapabilities?.executeCommandProvider?.commands;
319
+ const [eligible] = captureEligibleActions([action], Array.isArray(listed) ? listed : undefined);
320
+ if (!eligible) {
321
+ return { value: null, authorizedMutationRoots, reason: COMMAND_ONLY_ACTION_REASON };
322
+ }
323
+ const value = await client.captureCommandEdits(eligible.command, {
324
+ timeoutMs: COMMAND_CAPTURE_TIMEOUT_MS,
325
+ ...(control ? { control } : {}),
326
+ });
327
+ return { value, authorizedMutationRoots };
328
+ }
329
+
291
330
  async codeActions(
292
331
  filePath: string,
293
332
  positionOrRange: Position | Range,
294
333
  control?: CodeRequestControl,
295
334
  options?: WorkspaceCodeActionRequestOptions,
296
- ): Promise<RoutedMutationResponse<CodeAction[] | null> | null> {
335
+ ): Promise<RoutedCodeActionResponse | null> {
297
336
  const resolvedPath = this.resolveFilePath(filePath);
298
337
  const client = await this.manager.ensureFileOpen(resolvedPath, {
299
338
  recoverProcessCrash: true,
@@ -301,6 +340,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
301
340
  control,
302
341
  });
303
342
  if (!client) return null;
343
+ const outside = outsideRouteRootReason(resolvedPath, client.root);
344
+ if (outside) {
345
+ return { value: null, authorizedMutationRoots: [client.root], reason: outside };
346
+ }
304
347
 
305
348
  const range = isRange(positionOrRange)
306
349
  ? positionOrRange
@@ -315,21 +358,23 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
315
358
  triggerKind: options?.triggerKind ?? CodeActionTriggerKind.Invoked,
316
359
  ...(options?.only ? { only: [...options.only] } : {}),
317
360
  };
361
+ const advertisedKinds = advertisedCodeActionKinds(client.serverCapabilities ?? null);
362
+ const advertisedCommands = client.serverCapabilities?.executeCommandProvider?.commands;
363
+ const routed = {
364
+ authorizedMutationRoots: [client.root],
365
+ ...(advertisedKinds ? { advertisedKinds } : {}),
366
+ ...(Array.isArray(advertisedCommands) ? { advertisedCommands } : {}),
367
+ };
318
368
  if (typeof client.codeActionsDetailed === "function") {
319
369
  const result = await client.codeActionsDetailed(resolvedPath, range, context, control);
320
- return {
321
- value: result.kind === "completed" ? result.data : null,
322
- authorizedMutationRoots: [client.root],
323
- ...(result.kind === "unavailable"
324
- ? { reason: result.reason }
325
- : result.data === null
326
- ? { reason: "LSP server returned no code actions." }
327
- : {}),
328
- };
370
+ // A completed `null` result means the server has no actions here (LSP spec).
371
+ return result.kind === "completed"
372
+ ? { ...routed, value: result.data ?? [] }
373
+ : { ...routed, value: null, reason: result.reason };
329
374
  }
330
375
  return {
331
- value: await client.codeActions(resolvedPath, range, context, control),
332
- authorizedMutationRoots: [client.root],
376
+ ...routed,
377
+ value: (await client.codeActions(resolvedPath, range, context, control)) ?? [],
333
378
  };
334
379
  }
335
380
 
@@ -5,6 +5,7 @@ import type { SemanticInputDocumentSnapshot } from "../client/client-semantic-in
5
5
  import type { Position, WorkspaceEdit } from "../config/types.ts";
6
6
  import type { LspManager } from "../manager/manager.ts";
7
7
  import { discoverSemanticEditFiles } from "../provider/semantic-edit-discovery.ts";
8
+ import { outsideRouteRootReason } from "./route-boundary.ts";
8
9
  import type {
9
10
  RoutedMutationDocumentSnapshot,
10
11
  RoutedMutationResponse,
@@ -26,6 +27,8 @@ export async function runRoutedRename(options: {
26
27
  });
27
28
  if (!client) return null;
28
29
  const authorizedMutationRoots = [client.root];
30
+ const outside = outsideRouteRootReason(file, client.root);
31
+ if (outside) return { value: null, authorizedMutationRoots, reason: outside };
29
32
 
30
33
  if (typeof client.renameDetailedWithSnapshots === "function") {
31
34
  return renameWithSnapshots({
@@ -1,4 +1,5 @@
1
1
  import type { CodeQueryResult, CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
2
+ import type { CommandEditCaptureResult } from "../client/client-edit-capture.ts";
2
3
  import type {
3
4
  CodeAction,
4
5
  CodeActionContext,
@@ -80,6 +81,14 @@ export interface RoutedMutationResponse<T> {
80
81
  readonly reason?: string;
81
82
  }
82
83
 
84
+ /** One code-action response with the routed server's negotiated kinds. */
85
+ export interface RoutedCodeActionResponse extends RoutedMutationResponse<CodeAction[] | null> {
86
+ /** Negotiated `codeActionProvider.codeActionKinds`; undefined when the server did not narrow them. */
87
+ readonly advertisedKinds?: readonly string[];
88
+ /** Negotiated `executeCommandProvider.commands`; undefined when the server lists none. */
89
+ readonly advertisedCommands?: readonly string[];
90
+ }
91
+
83
92
  /** Extra protocol context for one explicit code-action request. */
84
93
  export interface WorkspaceCodeActionRequestOptions {
85
94
  /** Operation-specific CodeActionKind filters. */
@@ -164,7 +173,19 @@ export interface WorkspaceLspRuntime extends WorkspaceLspDiagnosticSurface {
164
173
  positionOrRange: Position | Range,
165
174
  control?: CodeRequestControl,
166
175
  options?: WorkspaceCodeActionRequestOptions,
167
- ): Promise<RoutedMutationResponse<CodeAction[] | null> | null>;
176
+ ): Promise<RoutedCodeActionResponse | null>;
177
+ /**
178
+ * Run the command of one command-only code action on the client routed to
179
+ * `filePath` and capture the single edit the server pushes. No pushed edit is
180
+ * applied. The runtime enforces eligibility itself: a `refactor*`,
181
+ * `source.organizeImports*`, or `source.fixAll*` kind, and a command the
182
+ * server lists when it lists any; other actions are unavailable unrun.
183
+ */
184
+ captureCodeActionEdit(
185
+ filePath: string,
186
+ action: CodeAction,
187
+ control?: CodeRequestControl,
188
+ ): Promise<RoutedMutationResponse<CommandEditCaptureResult | null> | null>;
168
189
  getOpenDocumentVersion(filePath: string): number | null;
169
190
  /**
170
191
  * Wait for file readiness; an explicit refresh may make one shared route