@mrclrchtr/supi-lsp 1.16.1 → 2.0.1
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 +44 -56
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +6 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +3 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +7 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +56 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/evidence-badge.ts +40 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +5 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +76 -3
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +37 -2
- package/node_modules/@mrclrchtr/supi-core/package.json +6 -1
- package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/package.json +4 -31
- package/src/api.ts +27 -0
- package/src/client/client-refresh.ts +5 -0
- package/src/client/client.ts +246 -1
- package/src/client/transport.ts +4 -0
- package/src/config/capabilities.ts +3 -0
- package/src/config/config.ts +4 -2
- package/src/config/lsp-settings.ts +102 -0
- package/src/config/server-actions.ts +59 -0
- package/src/config/server-config.ts +4 -0
- package/src/index.ts +11 -0
- package/src/manager/capability-index.ts +1 -1
- package/src/manager/manager-project-info.ts +2 -1
- package/src/manager/manager.ts +115 -26
- package/src/provider/lsp-semantic-provider.ts +177 -134
- package/src/provider/refactor-planning.ts +214 -0
- package/src/session/runtime-controller.ts +320 -0
- package/src/session/runtime-registration.ts +26 -5
- package/src/session/service-registry.ts +81 -4
- package/src/summary.ts +2 -0
- package/src/extension.ts +0 -1
- package/src/format.ts +0 -373
- package/src/handlers/diagnostic-injection.ts +0 -182
- package/src/handlers/session-lifecycle.ts +0 -149
- package/src/handlers/status-command.ts +0 -38
- package/src/handlers/workspace-recovery.ts +0 -92
- package/src/lsp.ts +0 -37
- package/src/session/lsp-state.ts +0 -93
- package/src/session/settings-registration.ts +0 -296
- package/src/session/tree-persist.ts +0 -75
- package/src/tool/guidance.ts +0 -69
- package/src/tool/names.ts +0 -27
- package/src/tool/overrides.ts +0 -204
- package/src/tool/register-tools.ts +0 -69
- package/src/tool/service-actions.ts +0 -399
- package/src/tool/tool-specs.ts +0 -248
- package/src/ui/renderer.ts +0 -120
- package/src/ui/ui.ts +0 -360
- package/src/workspace-change.ts +0 -40
package/src/manager/manager.ts
CHANGED
|
@@ -55,6 +55,10 @@ import type {
|
|
|
55
55
|
ServerStatus,
|
|
56
56
|
} from "./manager-types.ts";
|
|
57
57
|
import { recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl } from "./manager-workspace-recovery.ts";
|
|
58
|
+
import {
|
|
59
|
+
findWorkspaceSymbolWarmTargets,
|
|
60
|
+
getWorkspaceSymbolWarmPosition,
|
|
61
|
+
} from "./manager-workspace-symbol.ts";
|
|
58
62
|
|
|
59
63
|
type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
60
64
|
|
|
@@ -74,6 +78,10 @@ export class LspManager {
|
|
|
74
78
|
private excludePatterns: string[] = [];
|
|
75
79
|
/** Project roots already warmed for workspace-symbol queries. */
|
|
76
80
|
private warmedWorkspaceSymbolProjects = new Set<string>();
|
|
81
|
+
/** Project roots whose semantic state was warmed with a readiness probe. */
|
|
82
|
+
private warmedSemanticProjects = new Set<string>();
|
|
83
|
+
/** In-flight warm-up probes keyed by project key so concurrent callers share one probe. */
|
|
84
|
+
private pendingWarmProbes = new Map<string, Promise<void>>();
|
|
77
85
|
constructor(
|
|
78
86
|
private readonly config: LspConfig,
|
|
79
87
|
private readonly cwd: string,
|
|
@@ -174,6 +182,8 @@ export class LspManager {
|
|
|
174
182
|
this.clients.delete(key);
|
|
175
183
|
this.unavailable.set(key, "runtime-error");
|
|
176
184
|
this.clearWarmedWorkspaceSymbolProjects(existing.name, existing.root);
|
|
185
|
+
this.clearWarmedSemanticProjects(existing.name, existing.root);
|
|
186
|
+
this.clearPendingWarmProbes(existing.name, existing.root);
|
|
177
187
|
return null;
|
|
178
188
|
}
|
|
179
189
|
|
|
@@ -213,6 +223,8 @@ export class LspManager {
|
|
|
213
223
|
// Spawn new client
|
|
214
224
|
const client = new LspClient(serverName, serverConfig, root);
|
|
215
225
|
this.clearWarmedWorkspaceSymbolProjects(serverName, root);
|
|
226
|
+
this.clearWarmedSemanticProjects(serverName, root);
|
|
227
|
+
this.clearPendingWarmProbes(serverName, root);
|
|
216
228
|
this.clients.set(key, client);
|
|
217
229
|
rememberKnownRoot(this.knownRoots, serverName, root);
|
|
218
230
|
try {
|
|
@@ -276,6 +288,8 @@ export class LspManager {
|
|
|
276
288
|
this.clients.delete(key);
|
|
277
289
|
this.unavailable.delete(key);
|
|
278
290
|
this.clearWarmedWorkspaceSymbolProjects(client.name, client.root);
|
|
291
|
+
this.clearWarmedSemanticProjects(client.name, client.root);
|
|
292
|
+
this.clearPendingWarmProbes(client.name, client.root);
|
|
279
293
|
|
|
280
294
|
const replacement = new LspClient(client.name, serverConfig, client.root);
|
|
281
295
|
this.clients.set(key, replacement);
|
|
@@ -336,6 +350,36 @@ export class LspManager {
|
|
|
336
350
|
a.status.localeCompare(b.status),
|
|
337
351
|
);
|
|
338
352
|
}
|
|
353
|
+
|
|
354
|
+
/** Wait until the client that owns a file is query-ready, then run a light warm-up probe. */
|
|
355
|
+
async waitUntilFileReady(filePath: string): Promise<LspClient | null> {
|
|
356
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
357
|
+
const client = await this.getClientForFile(resolvedPath);
|
|
358
|
+
if (!client) return null;
|
|
359
|
+
await client.getReady();
|
|
360
|
+
await this.warmSemanticProject(client, resolvedPath, true);
|
|
361
|
+
return client;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Wait until all started clients are query-ready, then warm one project file per client/root. */
|
|
365
|
+
async waitUntilWorkspaceReady(): Promise<void> {
|
|
366
|
+
const activeClients = Array.from(this.clients.values()).filter(
|
|
367
|
+
(client) => client.status === "running",
|
|
368
|
+
);
|
|
369
|
+
await Promise.all(activeClients.map((client) => client.getReady()));
|
|
370
|
+
|
|
371
|
+
for (const client of activeClients) {
|
|
372
|
+
const serverConfig = this.config.servers[client.name];
|
|
373
|
+
if (!serverConfig) continue;
|
|
374
|
+
const target = findWorkspaceSymbolWarmTargets(
|
|
375
|
+
client.root,
|
|
376
|
+
serverConfig.rootMarkers,
|
|
377
|
+
serverConfig.fileTypes,
|
|
378
|
+
)[0];
|
|
379
|
+
if (!target) continue;
|
|
380
|
+
await this.warmSemanticProject(client, target.file);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
339
383
|
async syncFileAndGetDiagnostics(
|
|
340
384
|
filePath: string,
|
|
341
385
|
maxSeverity: number = 1,
|
|
@@ -422,6 +466,8 @@ export class LspManager {
|
|
|
422
466
|
this.unavailable.clear();
|
|
423
467
|
this.knownRoots.clear();
|
|
424
468
|
this.warmedWorkspaceSymbolProjects.clear();
|
|
469
|
+
this.warmedSemanticProjects.clear();
|
|
470
|
+
this.pendingWarmProbes.clear();
|
|
425
471
|
}
|
|
426
472
|
/** Get status of all servers. */
|
|
427
473
|
getStatus(): ManagerStatus {
|
|
@@ -580,13 +626,8 @@ export class LspManager {
|
|
|
580
626
|
query,
|
|
581
627
|
);
|
|
582
628
|
if (warmed.results) return warmed.results;
|
|
583
|
-
if (!warmed.warmedAny) return initial.results;
|
|
584
629
|
|
|
585
|
-
return
|
|
586
|
-
helper.collectWorkspaceSymbols,
|
|
587
|
-
query,
|
|
588
|
-
initial.results,
|
|
589
|
-
);
|
|
630
|
+
return initial.results;
|
|
590
631
|
}
|
|
591
632
|
async ensureFileOpen(filePath: string): Promise<LspClient | null> {
|
|
592
633
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
@@ -666,28 +707,58 @@ export class LspManager {
|
|
|
666
707
|
return { warmedAny, results: null };
|
|
667
708
|
}
|
|
668
709
|
|
|
669
|
-
private async
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
710
|
+
private async warmSemanticProject(
|
|
711
|
+
client: LspClient,
|
|
712
|
+
filePath: string,
|
|
713
|
+
preferExactFile: boolean = false,
|
|
714
|
+
): Promise<void> {
|
|
715
|
+
const serverConfig = this.config.servers[client.name];
|
|
716
|
+
if (!serverConfig) return;
|
|
717
|
+
|
|
718
|
+
const resolvedFile = resolveSessionPath(this.cwd, filePath);
|
|
719
|
+
const projectRoot = preferExactFile
|
|
720
|
+
? resolveRootForFile(resolvedFile, client.name, serverConfig.rootMarkers, {
|
|
721
|
+
knownRoots: this.knownRoots,
|
|
722
|
+
cwd: this.cwd,
|
|
723
|
+
})
|
|
724
|
+
: client.root;
|
|
725
|
+
const projectKey = this.workspaceSymbolProjectKey(client.name, projectRoot);
|
|
726
|
+
if (this.warmedSemanticProjects.has(projectKey)) return;
|
|
727
|
+
|
|
728
|
+
const pending = this.pendingWarmProbes.get(projectKey);
|
|
729
|
+
if (pending) {
|
|
730
|
+
await pending;
|
|
731
|
+
return;
|
|
688
732
|
}
|
|
689
733
|
|
|
690
|
-
|
|
734
|
+
const probe = this.performWarmProbe(client, resolvedFile, projectKey);
|
|
735
|
+
this.pendingWarmProbes.set(projectKey, probe);
|
|
736
|
+
try {
|
|
737
|
+
await probe;
|
|
738
|
+
} finally {
|
|
739
|
+
this.pendingWarmProbes.delete(projectKey);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private async performWarmProbe(
|
|
744
|
+
_client: LspClient,
|
|
745
|
+
resolvedFile: string,
|
|
746
|
+
projectKey: string,
|
|
747
|
+
): Promise<void> {
|
|
748
|
+
const openedClient = await this.ensureFileOpen(resolvedFile);
|
|
749
|
+
if (!openedClient) return;
|
|
750
|
+
|
|
751
|
+
this.warmedSemanticProjects.add(projectKey);
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
const symbols = await openedClient.documentSymbols(resolvedFile);
|
|
755
|
+
const hoverPosition = getWorkspaceSymbolWarmPosition(symbols);
|
|
756
|
+
if (hoverPosition) {
|
|
757
|
+
await openedClient.hover(resolvedFile, hoverPosition);
|
|
758
|
+
}
|
|
759
|
+
} catch {
|
|
760
|
+
// Best-effort warm-up only.
|
|
761
|
+
}
|
|
691
762
|
}
|
|
692
763
|
|
|
693
764
|
private hasOpenFileInProject(client: LspClient, projectRoot: string): boolean {
|
|
@@ -706,4 +777,22 @@ export class LspManager {
|
|
|
706
777
|
}
|
|
707
778
|
}
|
|
708
779
|
}
|
|
780
|
+
|
|
781
|
+
private clearWarmedSemanticProjects(serverName: string, root: string): void {
|
|
782
|
+
const prefix = `${serverName}:${path.resolve(root)}`;
|
|
783
|
+
for (const key of Array.from(this.warmedSemanticProjects)) {
|
|
784
|
+
if (key === prefix || key.startsWith(`${prefix}${path.sep}`)) {
|
|
785
|
+
this.warmedSemanticProjects.delete(key);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private clearPendingWarmProbes(serverName: string, root: string): void {
|
|
791
|
+
const prefix = `${serverName}:${path.resolve(root)}`;
|
|
792
|
+
for (const key of Array.from(this.pendingWarmProbes.keys())) {
|
|
793
|
+
if (key === prefix || key.startsWith(`${prefix}${path.sep}`)) {
|
|
794
|
+
this.pendingWarmProbes.delete(key);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
709
798
|
}
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
// LSP semantic provider adapter — wraps SessionLspService into the shared
|
|
2
2
|
// SemanticProvider contract from supi-code-runtime.
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
CodeLocation,
|
|
6
|
-
CodePosition,
|
|
7
|
-
CodeSymbol,
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import {
|
|
5
|
+
type CodeLocation,
|
|
6
|
+
type CodePosition,
|
|
7
|
+
type CodeSymbol,
|
|
8
|
+
normalizeRefactorOperation,
|
|
9
|
+
type RefactorRequest,
|
|
10
|
+
type RefactorResult,
|
|
11
|
+
type SemanticProvider,
|
|
12
|
+
type SourceRange,
|
|
10
13
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
11
14
|
import type {
|
|
15
|
+
CodeAction,
|
|
12
16
|
DocumentSymbol,
|
|
17
|
+
Hover,
|
|
13
18
|
Location,
|
|
14
19
|
LocationLink,
|
|
20
|
+
MarkupContent,
|
|
15
21
|
SymbolInformation,
|
|
16
|
-
TextDocumentEdit,
|
|
17
|
-
TextEdit,
|
|
18
|
-
WorkspaceEdit,
|
|
19
22
|
} from "../config/types.ts";
|
|
20
23
|
import type { SessionLspService } from "../session/service-registry.ts";
|
|
24
|
+
import {
|
|
25
|
+
collectCodeActionResults,
|
|
26
|
+
isDeleteDeadCodeCodeAction,
|
|
27
|
+
isExtractFunctionCodeAction,
|
|
28
|
+
isExtractVariableCodeAction,
|
|
29
|
+
isUpdateImportsCodeAction,
|
|
30
|
+
runFilteredCodeActionRefactor,
|
|
31
|
+
runRenameRefactor,
|
|
32
|
+
} from "./refactor-planning.ts";
|
|
21
33
|
|
|
22
34
|
/**
|
|
23
35
|
* Create a SemanticProvider backed by a SessionLspService.
|
|
@@ -25,6 +37,27 @@ import type { SessionLspService } from "../session/service-registry.ts";
|
|
|
25
37
|
*/
|
|
26
38
|
export function createLspSemanticProvider(lsp: SessionLspService): SemanticProvider {
|
|
27
39
|
return {
|
|
40
|
+
async definition(filePath: string, position: CodePosition): Promise<CodeLocation[] | null> {
|
|
41
|
+
const result = await lsp.definition(filePath, position);
|
|
42
|
+
if (!result) return null;
|
|
43
|
+
const normalized = Array.isArray(result) ? result : [result];
|
|
44
|
+
const mapped: CodeLocation[] = [];
|
|
45
|
+
for (const item of normalized) {
|
|
46
|
+
const loc = toCodeLocation(item);
|
|
47
|
+
if (loc) mapped.push(loc);
|
|
48
|
+
}
|
|
49
|
+
return mapped;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async hover(
|
|
53
|
+
filePath: string,
|
|
54
|
+
position: CodePosition,
|
|
55
|
+
): Promise<{ contents: string; range?: SourceRange } | null> {
|
|
56
|
+
const result = await lsp.hover(filePath, position);
|
|
57
|
+
if (!result) return null;
|
|
58
|
+
return convertLspHover(result);
|
|
59
|
+
},
|
|
60
|
+
|
|
28
61
|
async references(filePath: string, position: CodePosition): Promise<CodeLocation[] | null> {
|
|
29
62
|
const refResult = await lsp.references(filePath, position);
|
|
30
63
|
if (!refResult) return null;
|
|
@@ -60,135 +93,101 @@ export function createLspSemanticProvider(lsp: SessionLspService): SemanticProvi
|
|
|
60
93
|
return results.map((sym) => toCodeSymbol(sym as SymbolInformation));
|
|
61
94
|
},
|
|
62
95
|
|
|
96
|
+
async refactor(request: RefactorRequest): Promise<RefactorResult> {
|
|
97
|
+
// Normalize defensively at the provider boundary as well as in
|
|
98
|
+
// code-intelligence. RefactorRequest still permits the legacy `rename`
|
|
99
|
+
// alias, and the provider may be invoked directly outside the public tool.
|
|
100
|
+
const operation = normalizeRefactorOperation(request.operation);
|
|
101
|
+
|
|
102
|
+
switch (operation) {
|
|
103
|
+
case "rename_symbol":
|
|
104
|
+
if (!request.newName) {
|
|
105
|
+
return {
|
|
106
|
+
kind: "unavailable",
|
|
107
|
+
reason: 'Refactor operation "rename_symbol" requires `newName`.',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return runRenameRefactor(lsp, request.file, request.position, request.newName);
|
|
111
|
+
case "extract_function":
|
|
112
|
+
return runExtractRefactor(lsp, request, "extract_function", isExtractFunctionCodeAction);
|
|
113
|
+
case "extract_variable":
|
|
114
|
+
return runExtractRefactor(lsp, request, "extract_variable", isExtractVariableCodeAction);
|
|
115
|
+
case "update_imports":
|
|
116
|
+
return runFilteredCodeActionRefactor({
|
|
117
|
+
lsp,
|
|
118
|
+
file: request.file,
|
|
119
|
+
position: request.position,
|
|
120
|
+
operation: "update_imports",
|
|
121
|
+
matches: isUpdateImportsCodeAction,
|
|
122
|
+
});
|
|
123
|
+
case "delete_dead_code":
|
|
124
|
+
return runFilteredCodeActionRefactor({
|
|
125
|
+
lsp,
|
|
126
|
+
file: request.file,
|
|
127
|
+
position: request.position,
|
|
128
|
+
operation: "delete_dead_code",
|
|
129
|
+
matches: isDeleteDeadCodeCodeAction,
|
|
130
|
+
});
|
|
131
|
+
case "rename_file":
|
|
132
|
+
case "move_file":
|
|
133
|
+
// TODO(TNDM-D9FEHR): Replace this explicit unavailable result once
|
|
134
|
+
// shared file/resource edits and rollback semantics exist.
|
|
135
|
+
return {
|
|
136
|
+
kind: "unavailable",
|
|
137
|
+
reason: `Refactor operation "${operation}" is not supported yet. File/resource operations are deferred.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
kind: "unavailable",
|
|
143
|
+
reason: `Refactor operation "${operation}" is not supported by the active semantic provider.`,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
|
|
63
147
|
async rename(file: string, position: CodePosition, newName: string): Promise<RefactorResult> {
|
|
64
|
-
|
|
65
|
-
return convertLspWorkspaceEdit(edit);
|
|
148
|
+
return runRenameRefactor(lsp, file, position, newName);
|
|
66
149
|
},
|
|
67
150
|
|
|
68
151
|
async codeActions(file: string, position: CodePosition): Promise<RefactorResult[]> {
|
|
69
152
|
const actions = await lsp.codeActions(file, position);
|
|
70
153
|
if (!actions) return [];
|
|
154
|
+
return collectCodeActionResults(actions);
|
|
155
|
+
},
|
|
71
156
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
const converted = convertLspWorkspaceEdit(edit);
|
|
83
|
-
if (converted.kind === "precise") {
|
|
84
|
-
results.push(converted);
|
|
85
|
-
} else {
|
|
86
|
-
results.push({
|
|
87
|
-
kind: "unavailable",
|
|
88
|
-
reason: `Code action "${action.title}" could not produce precise edits`,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return results;
|
|
157
|
+
async codeActionTitles(
|
|
158
|
+
file: string,
|
|
159
|
+
position: CodePosition,
|
|
160
|
+
): Promise<Array<{ title: string; kind?: string }> | null> {
|
|
161
|
+
const actions = await lsp.codeActions(file, position);
|
|
162
|
+
if (!actions) return null;
|
|
163
|
+
return actions
|
|
164
|
+
.filter((a) => a.title)
|
|
165
|
+
.map((a) => ({ title: a.title, kind: a.kind ?? undefined }));
|
|
93
166
|
},
|
|
94
167
|
};
|
|
95
168
|
}
|
|
96
169
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
function resolveFileFromUri(uri: string): string {
|
|
109
|
-
if (!uri.startsWith("file://")) return uri;
|
|
110
|
-
try {
|
|
111
|
-
return decodeURIComponent(uri.slice(7));
|
|
112
|
-
} catch {
|
|
113
|
-
return uri;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function collectDocumentChangeEdits(
|
|
118
|
-
docChanges: NonNullable<WorkspaceEdit["documentChanges"]>,
|
|
119
|
-
): Array<{
|
|
120
|
-
file: string;
|
|
121
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
122
|
-
newText: string;
|
|
123
|
-
}> {
|
|
124
|
-
const out: Array<{
|
|
125
|
-
file: string;
|
|
126
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
127
|
-
newText: string;
|
|
128
|
-
}> = [];
|
|
129
|
-
for (const change of docChanges) {
|
|
130
|
-
const tdEdit = change as TextDocumentEdit;
|
|
131
|
-
if (!tdEdit.textDocument || !tdEdit.edits) continue;
|
|
132
|
-
const file = resolveFileFromUri(tdEdit.textDocument.uri);
|
|
133
|
-
for (const singleEdit of tdEdit.edits) {
|
|
134
|
-
const te = singleEdit as TextEdit;
|
|
135
|
-
out.push({
|
|
136
|
-
file,
|
|
137
|
-
range: {
|
|
138
|
-
start: { line: te.range.start.line, character: te.range.start.character },
|
|
139
|
-
end: { line: te.range.end.line, character: te.range.end.character },
|
|
140
|
-
},
|
|
141
|
-
newText: te.newText,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function collectChangesEdits(changes: NonNullable<WorkspaceEdit["changes"]>): Array<{
|
|
149
|
-
file: string;
|
|
150
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
151
|
-
newText: string;
|
|
152
|
-
}> {
|
|
153
|
-
const out: Array<{
|
|
154
|
-
file: string;
|
|
155
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
156
|
-
newText: string;
|
|
157
|
-
}> = [];
|
|
158
|
-
for (const [uri, textEdits] of Object.entries(changes)) {
|
|
159
|
-
if (!textEdits || textEdits.length === 0) continue;
|
|
160
|
-
const file = resolveFileFromUri(uri);
|
|
161
|
-
for (const te of textEdits) {
|
|
162
|
-
out.push({
|
|
163
|
-
file,
|
|
164
|
-
range: {
|
|
165
|
-
start: { line: te.range.start.line, character: te.range.start.character },
|
|
166
|
-
end: { line: te.range.end.line, character: te.range.end.character },
|
|
167
|
-
},
|
|
168
|
-
newText: te.newText,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function convertLspWorkspaceEdit(edit: WorkspaceEdit | null): RefactorResult {
|
|
176
|
-
if (!edit) {
|
|
177
|
-
return { kind: "unavailable", reason: "LSP server returned no edit" };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
let fileEdits = edit.documentChanges?.length
|
|
181
|
-
? collectDocumentChangeEdits(edit.documentChanges)
|
|
182
|
-
: [];
|
|
183
|
-
if (fileEdits.length === 0 && edit.changes) {
|
|
184
|
-
fileEdits = collectChangesEdits(edit.changes);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (fileEdits.length === 0) {
|
|
188
|
-
return { kind: "unavailable", reason: "Workspace edit contains no file edits" };
|
|
170
|
+
function runExtractRefactor(
|
|
171
|
+
lsp: SessionLspService,
|
|
172
|
+
request: RefactorRequest,
|
|
173
|
+
operation: "extract_function" | "extract_variable",
|
|
174
|
+
matches: (action: CodeAction) => boolean,
|
|
175
|
+
): Promise<RefactorResult> | RefactorResult {
|
|
176
|
+
if (!request.range) {
|
|
177
|
+
return {
|
|
178
|
+
kind: "unavailable",
|
|
179
|
+
reason: `Refactor operation "${operation}" requires \`range\`.`,
|
|
180
|
+
};
|
|
189
181
|
}
|
|
190
182
|
|
|
191
|
-
return {
|
|
183
|
+
return runFilteredCodeActionRefactor({
|
|
184
|
+
lsp,
|
|
185
|
+
file: request.file,
|
|
186
|
+
position: request.position,
|
|
187
|
+
range: request.range,
|
|
188
|
+
operation,
|
|
189
|
+
matches,
|
|
190
|
+
});
|
|
192
191
|
}
|
|
193
192
|
|
|
194
193
|
// ── Type conversion helpers ───────────────────────────────────────────
|
|
@@ -257,20 +256,27 @@ function flattenDocumentSymbols(
|
|
|
257
256
|
const result: CodeSymbol[] = [];
|
|
258
257
|
|
|
259
258
|
for (const sym of symbols) {
|
|
260
|
-
// DocumentSymbol
|
|
259
|
+
// DocumentSymbol.range = full defining node (declaration anchor);
|
|
260
|
+
// .selectionRange = identifier token (name anchor). SymbolInformation has
|
|
261
|
+
// only location.range (declaration) and no selectionRange.
|
|
261
262
|
const ds = sym as DocumentSymbol;
|
|
262
263
|
const si = sym as SymbolInformation;
|
|
263
|
-
const
|
|
264
|
-
if (!
|
|
264
|
+
const declStart = ds.range?.start ?? si.location?.range?.start;
|
|
265
|
+
if (!declStart) continue;
|
|
266
|
+
const nameStart = ds.selectionRange?.start;
|
|
265
267
|
|
|
266
|
-
|
|
268
|
+
const symbol: CodeSymbol = {
|
|
267
269
|
name: sym.name,
|
|
268
270
|
kind: symbolKindName(sym.kind),
|
|
269
271
|
file: filePath,
|
|
270
|
-
line:
|
|
271
|
-
character: start.character + 1,
|
|
272
|
+
declarationAnchor: { line: declStart.line + 1, character: declStart.character + 1 },
|
|
272
273
|
container,
|
|
273
|
-
}
|
|
274
|
+
};
|
|
275
|
+
if (nameStart) {
|
|
276
|
+
symbol.nameAnchor = { line: nameStart.line + 1, character: nameStart.character + 1 };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
result.push(symbol);
|
|
274
280
|
|
|
275
281
|
if (Array.isArray(ds.children) && ds.children.length > 0) {
|
|
276
282
|
result.push(...flattenDocumentSymbols(ds.children, filePath, sym.name));
|
|
@@ -287,8 +293,45 @@ function toCodeSymbol(sym: SymbolInformation): CodeSymbol {
|
|
|
287
293
|
name: sym.name,
|
|
288
294
|
kind: symbolKindName(sym.kind),
|
|
289
295
|
file: uri.startsWith("file://") ? decodeURIComponent(uri.slice(7)) : uri,
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
// SymbolInformation has no selectionRange — nameAnchor is derived later
|
|
297
|
+
// by the orchestration layer's refine, or left absent.
|
|
298
|
+
declarationAnchor: {
|
|
299
|
+
line: start ? start.line + 1 : 0,
|
|
300
|
+
character: start ? start.character + 1 : 0,
|
|
301
|
+
},
|
|
292
302
|
container: sym.containerName ?? null,
|
|
293
303
|
};
|
|
294
304
|
}
|
|
305
|
+
|
|
306
|
+
// ── Hover conversion helpers ─────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Convert an LSP Hover result into a simplified runtime shape.
|
|
310
|
+
* Extracts text from MarkupContent, MarkedString[], or plain string,
|
|
311
|
+
* and converts the optional LSP Range to a SourceRange.
|
|
312
|
+
*/
|
|
313
|
+
function convertLspHover(hover: Hover): { contents: string; range?: SourceRange } {
|
|
314
|
+
const contents = extractHoverText(hover.contents);
|
|
315
|
+
const result: { contents: string; range?: SourceRange } = { contents };
|
|
316
|
+
if (hover.range) {
|
|
317
|
+
result.range = {
|
|
318
|
+
start: { line: hover.range.start.line, character: hover.range.start.character },
|
|
319
|
+
end: { line: hover.range.end.line, character: hover.range.end.character },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function extractHoverText(contents: Hover["contents"]): string {
|
|
326
|
+
if (typeof contents === "string") return contents;
|
|
327
|
+
if (Array.isArray(contents)) {
|
|
328
|
+
return contents
|
|
329
|
+
.map((item) => {
|
|
330
|
+
if (typeof item === "string") return item;
|
|
331
|
+
return item.value;
|
|
332
|
+
})
|
|
333
|
+
.join("\n");
|
|
334
|
+
}
|
|
335
|
+
// MarkupContent
|
|
336
|
+
return (contents as MarkupContent).value ?? "";
|
|
337
|
+
}
|