@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.
- package/README.md +21 -16
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +1 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +51 -23
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/package.json +3 -3
- package/src/api.ts +2 -2
- package/src/client/client-diagnostic-capabilities.ts +13 -3
- package/src/client/client-diagnostics.ts +25 -9
- package/src/client/client-document-sync.ts +7 -5
- package/src/client/client-edit-capture.ts +258 -0
- package/src/client/client.ts +64 -11
- package/src/config/capabilities.ts +8 -0
- package/src/config/operation-support.ts +48 -79
- package/src/manager/manager.ts +3 -1
- package/src/provider/lsp-refactor-provider.ts +21 -35
- package/src/provider/refactor-command-capture.ts +139 -0
- package/src/provider/refactor-planning.ts +202 -47
- package/src/session/route-boundary.ts +26 -0
- package/src/session/runtime-registry.ts +57 -12
- package/src/session/runtime-rename.ts +3 -0
- package/src/session/workspace-lsp-runtime.ts +22 -1
|
@@ -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 {
|
|
12
|
+
import { isCodeActionKindInRange, isCodeActionKindWithin } from "../config/operation-support.ts";
|
|
10
13
|
import type { CodeAction } from "../config/types.ts";
|
|
11
|
-
import
|
|
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
|
-
|
|
87
|
+
/** Options for one code- or source-action planning request. */
|
|
88
|
+
export interface CodeActionRefactorOptions {
|
|
74
89
|
lsp: WorkspaceLspRuntime;
|
|
75
90
|
file: string;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
152
|
+
if (!response) {
|
|
88
153
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
114
|
-
if (plans.precise.length === 1)
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
128
|
-
|
|
237
|
+
: `Matching "${kind}" code actions did not produce precise edits.`,
|
|
238
|
+
);
|
|
129
239
|
}
|
|
130
240
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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<
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
...
|
|
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
|
-
|
|
332
|
-
|
|
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<
|
|
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
|