@mrclrchtr/supi-lsp 3.1.0 → 4.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 +72 -62
- package/node_modules/@mrclrchtr/supi-code-runtime/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +2 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +10 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +19 -29
- package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +7 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +42 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +15 -10
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -4
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
- package/package.json +4 -5
- package/src/api.ts +9 -9
- package/src/client/client.ts +70 -41
- package/src/client/transport.ts +16 -13
- package/src/config/config.ts +2 -2
- package/src/config/lsp-settings.ts +0 -2
- package/src/index.ts +6 -6
- package/src/manager/manager-diagnostics.ts +5 -95
- package/src/manager/manager-workspace-recovery.ts +4 -3
- package/src/manager/manager-workspace-symbol.ts +44 -6
- package/src/manager/manager.ts +47 -65
- package/src/provider/lsp-semantic-provider.ts +132 -79
- package/src/provider/refactor-planning.ts +6 -14
- package/src/session/readiness.ts +38 -0
- package/src/session/runtime-controller.ts +87 -75
- package/src/session/runtime-registration.ts +5 -5
- package/src/session/{service-registry.ts → runtime-registry.ts} +163 -71
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
- package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
- package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
- package/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
- package/src/diagnostics/diagnostic-augmentation.ts +0 -82
- package/src/diagnostics/diagnostic-context.ts +0 -116
- package/src/diagnostics/diagnostic-display.ts +0 -69
- package/src/manager/capability-index.ts +0 -24
- package/src/manager/client-pool.ts +0 -33
- package/src/manager/diagnostic-store.ts +0 -51
- package/src/manager/manager-stale-resync.ts +0 -47
- package/src/manager/recovery-coordinator.ts +0 -37
- package/src/manager/workspace-router.ts +0 -44
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type CodeQueryResult,
|
|
5
|
+
completedCodeQuery,
|
|
6
|
+
partialCodeQuery,
|
|
7
|
+
unavailableCodeQuery,
|
|
8
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
3
9
|
import { walkProject } from "@mrclrchtr/supi-core/project";
|
|
4
10
|
import type { LspClient } from "../client/client.ts";
|
|
5
11
|
import type {
|
|
@@ -39,30 +45,62 @@ export function getWorkspaceSymbolWarmPosition(
|
|
|
39
45
|
return null;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
export interface WorkspaceSymbolCollection {
|
|
49
|
+
results: WorkspaceSymbolLike[];
|
|
50
|
+
hasSupport: boolean;
|
|
51
|
+
completedClientCount: number;
|
|
52
|
+
failures: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
export async function collectWorkspaceSymbols(
|
|
43
56
|
clients: Iterable<LspClient>,
|
|
44
57
|
query: string,
|
|
45
|
-
): Promise<
|
|
58
|
+
): Promise<WorkspaceSymbolCollection> {
|
|
46
59
|
const results: WorkspaceSymbolLike[] = [];
|
|
60
|
+
const failures: string[] = [];
|
|
47
61
|
let hasSupport = false;
|
|
62
|
+
let completedClientCount = 0;
|
|
48
63
|
|
|
49
64
|
for (const client of clients) {
|
|
50
65
|
if (client.status !== "running") continue;
|
|
51
66
|
if (!client.serverCapabilities?.workspaceSymbolProvider) continue;
|
|
52
67
|
hasSupport = true;
|
|
53
68
|
const result = await client.workspaceSymbol(query);
|
|
54
|
-
if (result)
|
|
69
|
+
if (result.kind === "unavailable") {
|
|
70
|
+
failures.push(result.reason);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
completedClientCount++;
|
|
74
|
+
results.push(...(result.data ?? []));
|
|
75
|
+
if (result.kind === "partial") failures.push(result.reason);
|
|
55
76
|
}
|
|
56
77
|
|
|
57
|
-
return { results, hasSupport };
|
|
78
|
+
return { results, hasSupport, completedClientCount, failures };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Project one multi-client collection into the shared typed query contract. */
|
|
82
|
+
export function workspaceSymbolCollectionResult(
|
|
83
|
+
collection: WorkspaceSymbolCollection,
|
|
84
|
+
): CodeQueryResult<WorkspaceSymbolLike[]> {
|
|
85
|
+
if (!collection.hasSupport) {
|
|
86
|
+
return unavailableCodeQuery("No active LSP client supports workspace-symbol requests.");
|
|
87
|
+
}
|
|
88
|
+
if (collection.completedClientCount === 0) {
|
|
89
|
+
return unavailableCodeQuery(
|
|
90
|
+
collection.failures.join("; ") || "No workspace-symbol request completed.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (collection.failures.length > 0) {
|
|
94
|
+
return partialCodeQuery(collection.results, collection.failures.join("; "));
|
|
95
|
+
}
|
|
96
|
+
return completedCodeQuery(collection.results);
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
export async function managerWorkspaceSymbol(
|
|
61
100
|
clients: Iterable<LspClient>,
|
|
62
101
|
query: string,
|
|
63
|
-
): Promise<WorkspaceSymbolLike[]
|
|
64
|
-
|
|
65
|
-
return hasSupport ? results : null;
|
|
102
|
+
): Promise<CodeQueryResult<WorkspaceSymbolLike[]>> {
|
|
103
|
+
return workspaceSymbolCollectionResult(await collectWorkspaceSymbols(clients, query));
|
|
66
104
|
}
|
|
67
105
|
|
|
68
106
|
export function findWorkspaceSymbolWarmTargets(
|
package/src/manager/manager.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// biome-ignore-all lint/style/noExcessiveLinesPerFile: LspManager stays cohesive; recovery and sync helpers are split into manager-*.ts modules.
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { type CodeQueryResult, unavailableCodeQuery } from "@mrclrchtr/supi-code-runtime/api";
|
|
5
6
|
import * as projectRoots from "@mrclrchtr/supi-core/project";
|
|
6
7
|
import { LspClient } from "../client/client.ts";
|
|
7
8
|
import { getServerForFile } from "../config/config.ts";
|
|
@@ -36,8 +37,7 @@ import {
|
|
|
36
37
|
} from "./manager-client-state.ts";
|
|
37
38
|
import {
|
|
38
39
|
collectOutstandingDiagnosticsDetailed,
|
|
39
|
-
|
|
40
|
-
syncClientFileAndGetCascadingDiagnostics,
|
|
40
|
+
syncClientFileAndGetDiagnostics,
|
|
41
41
|
} from "./manager-diagnostics.ts";
|
|
42
42
|
import {
|
|
43
43
|
clientKey,
|
|
@@ -56,8 +56,11 @@ import type {
|
|
|
56
56
|
} from "./manager-types.ts";
|
|
57
57
|
import { recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl } from "./manager-workspace-recovery.ts";
|
|
58
58
|
import {
|
|
59
|
+
collectWorkspaceSymbols,
|
|
59
60
|
findWorkspaceSymbolWarmTargets,
|
|
60
61
|
getWorkspaceSymbolWarmPosition,
|
|
62
|
+
type WorkspaceSymbolCollection,
|
|
63
|
+
workspaceSymbolCollectionResult,
|
|
61
64
|
} from "./manager-workspace-symbol.ts";
|
|
62
65
|
|
|
63
66
|
type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
@@ -361,8 +364,11 @@ export class LspManager {
|
|
|
361
364
|
return client;
|
|
362
365
|
}
|
|
363
366
|
|
|
364
|
-
/**
|
|
365
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Wait until all started clients are query-ready, then warm one project file per client/root.
|
|
369
|
+
* Returns the number of concrete clients that reached readiness.
|
|
370
|
+
*/
|
|
371
|
+
async waitUntilWorkspaceReady(): Promise<number> {
|
|
366
372
|
const activeClients = Array.from(this.clients.values()).filter(
|
|
367
373
|
(client) => client.status === "running",
|
|
368
374
|
);
|
|
@@ -379,38 +385,26 @@ export class LspManager {
|
|
|
379
385
|
if (!target) continue;
|
|
380
386
|
await this.warmSemanticProject(client, target.file);
|
|
381
387
|
}
|
|
388
|
+
return activeClients.length;
|
|
382
389
|
}
|
|
383
390
|
async syncFileAndGetDiagnostics(
|
|
384
391
|
filePath: string,
|
|
385
392
|
maxSeverity: number = 1,
|
|
386
|
-
): Promise<Diagnostic[]
|
|
387
|
-
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
388
|
-
return (
|
|
389
|
-
(await this.syncFileAndGetCascadingDiagnostics(resolvedPath, maxSeverity)).find(
|
|
390
|
-
(entry) => entry.file === resolvedPath,
|
|
391
|
-
)?.diagnostics ?? []
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
async syncFileAndGetCascadingDiagnostics(
|
|
395
|
-
filePath: string,
|
|
396
|
-
maxSeverity: number = 1,
|
|
397
|
-
): Promise<Array<{ file: string; diagnostics: Diagnostic[] }>> {
|
|
393
|
+
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
398
394
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
399
395
|
const client = await this.getClientForFile(resolvedPath);
|
|
400
|
-
if (!client)
|
|
396
|
+
if (!client) {
|
|
397
|
+
return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
|
|
398
|
+
}
|
|
401
399
|
try {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
resolvedPath,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return [
|
|
408
|
-
...(primary.length > 0 ? [{ file: resolvedPath, diagnostics: primary }] : []),
|
|
409
|
-
...mapCascadeDiagnosticsToFiles(cascade),
|
|
410
|
-
];
|
|
411
|
-
} catch {
|
|
400
|
+
return {
|
|
401
|
+
kind: "completed",
|
|
402
|
+
data: await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity),
|
|
403
|
+
};
|
|
404
|
+
} catch (error) {
|
|
412
405
|
this.closeFile(resolvedPath);
|
|
413
|
-
|
|
406
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
407
|
+
return unavailableCodeQuery(`Diagnostic collection failed for ${resolvedPath}: ${detail}`);
|
|
414
408
|
}
|
|
415
409
|
}
|
|
416
410
|
/** Close a file across any active LSP clients and clear its cached diagnostics. */
|
|
@@ -447,7 +441,7 @@ export class LspManager {
|
|
|
447
441
|
maxWaitMs?: number;
|
|
448
442
|
quietMs?: number;
|
|
449
443
|
}): Promise<{
|
|
450
|
-
|
|
444
|
+
attemptedClients: number;
|
|
451
445
|
restartedClients: number;
|
|
452
446
|
staleAssessment: {
|
|
453
447
|
suspected: boolean;
|
|
@@ -613,21 +607,21 @@ export class LspManager {
|
|
|
613
607
|
maxSeverity,
|
|
614
608
|
);
|
|
615
609
|
}
|
|
616
|
-
async workspaceSymbol(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (initial.results.length > 0)
|
|
610
|
+
async workspaceSymbol(
|
|
611
|
+
query: string,
|
|
612
|
+
): Promise<CodeQueryResult<(SymbolInformation | WorkspaceSymbol)[]>> {
|
|
613
|
+
const initial = await collectWorkspaceSymbols(this.clients.values(), query);
|
|
614
|
+
if (!initial.hasSupport || initial.results.length > 0) {
|
|
615
|
+
return workspaceSymbolCollectionResult(initial);
|
|
616
|
+
}
|
|
621
617
|
|
|
622
618
|
const warmed = await this.warmWorkspaceSymbolProjectsUntilResult(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
619
|
+
findWorkspaceSymbolWarmTargets,
|
|
620
|
+
getWorkspaceSymbolWarmPosition,
|
|
621
|
+
collectWorkspaceSymbols,
|
|
626
622
|
query,
|
|
627
623
|
);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
return initial.results;
|
|
624
|
+
return workspaceSymbolCollectionResult(warmed.collection ?? initial);
|
|
631
625
|
}
|
|
632
626
|
async ensureFileOpen(filePath: string): Promise<LspClient | null> {
|
|
633
627
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
@@ -650,14 +644,11 @@ export class LspManager {
|
|
|
650
644
|
fileTypes: string[],
|
|
651
645
|
) => Array<{ projectRoot: string; file: string }>,
|
|
652
646
|
getWarmPosition: (
|
|
653
|
-
symbols:
|
|
647
|
+
symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
|
|
654
648
|
) => import("../config/types.ts").Position | null,
|
|
655
|
-
collect: (
|
|
656
|
-
clients: Iterable<LspClient>,
|
|
657
|
-
query: string,
|
|
658
|
-
) => Promise<{ results: (SymbolInformation | WorkspaceSymbol)[]; hasSupport: boolean }>,
|
|
649
|
+
collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>,
|
|
659
650
|
query: string,
|
|
660
|
-
): Promise<{ warmedAny: boolean;
|
|
651
|
+
): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
|
|
661
652
|
let warmedAny = false;
|
|
662
653
|
|
|
663
654
|
for (const client of Array.from(this.clients.values())) {
|
|
@@ -687,24 +678,20 @@ export class LspManager {
|
|
|
687
678
|
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
688
679
|
warmedAny = true;
|
|
689
680
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const hoverPosition = getWarmPosition(symbols);
|
|
693
|
-
if (hoverPosition)
|
|
694
|
-
await openedClient.hover(target.file, hoverPosition);
|
|
695
|
-
}
|
|
696
|
-
} catch {
|
|
697
|
-
// Best-effort warm-up only.
|
|
681
|
+
const symbols = await openedClient.documentSymbols(target.file);
|
|
682
|
+
if (symbols.kind !== "unavailable") {
|
|
683
|
+
const hoverPosition = getWarmPosition(symbols.data);
|
|
684
|
+
if (hoverPosition) await openedClient.hover(target.file, hoverPosition);
|
|
698
685
|
}
|
|
699
686
|
|
|
700
687
|
const collected = await collect(this.clients.values(), query);
|
|
701
688
|
if (collected.hasSupport && collected.results.length > 0) {
|
|
702
|
-
return { warmedAny,
|
|
689
|
+
return { warmedAny, collection: collected };
|
|
703
690
|
}
|
|
704
691
|
}
|
|
705
692
|
}
|
|
706
693
|
|
|
707
|
-
return { warmedAny,
|
|
694
|
+
return { warmedAny, collection: null };
|
|
708
695
|
}
|
|
709
696
|
|
|
710
697
|
private async warmSemanticProject(
|
|
@@ -750,15 +737,10 @@ export class LspManager {
|
|
|
750
737
|
|
|
751
738
|
this.warmedSemanticProjects.add(projectKey);
|
|
752
739
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
await openedClient.hover(resolvedFile, hoverPosition);
|
|
758
|
-
}
|
|
759
|
-
} catch {
|
|
760
|
-
// Best-effort warm-up only.
|
|
761
|
-
}
|
|
740
|
+
const symbols = await openedClient.documentSymbols(resolvedFile);
|
|
741
|
+
if (symbols.kind === "unavailable") return;
|
|
742
|
+
const hoverPosition = getWorkspaceSymbolWarmPosition(symbols.data);
|
|
743
|
+
if (hoverPosition) await openedClient.hover(resolvedFile, hoverPosition);
|
|
762
744
|
}
|
|
763
745
|
|
|
764
746
|
private hasOpenFileInProject(client: LspClient, projectRoot: string): boolean {
|
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
// LSP semantic provider adapter — wraps
|
|
1
|
+
// LSP semantic provider adapter — wraps WorkspaceLspRuntime into the shared
|
|
2
2
|
// SemanticProvider contract from supi-code-runtime.
|
|
3
3
|
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
4
5
|
import {
|
|
5
6
|
type CodeLocation,
|
|
6
7
|
type CodePosition,
|
|
8
|
+
type CodeQueryResult,
|
|
7
9
|
type CodeSymbol,
|
|
8
|
-
|
|
10
|
+
completedCodeQuery,
|
|
11
|
+
type DeclarationNesting,
|
|
12
|
+
type DocumentCodeSymbol,
|
|
13
|
+
mapCodeQueryResult,
|
|
14
|
+
partialCodeQuery,
|
|
9
15
|
type RefactorRequest,
|
|
10
16
|
type RefactorResult,
|
|
11
17
|
type SemanticProvider,
|
|
12
18
|
type SourceRange,
|
|
19
|
+
unavailableCodeQuery,
|
|
13
20
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
21
|
+
import { uriToFile } from "@mrclrchtr/supi-core/path";
|
|
14
22
|
import type {
|
|
15
23
|
CodeAction,
|
|
16
24
|
DocumentSymbol,
|
|
@@ -20,7 +28,7 @@ import type {
|
|
|
20
28
|
MarkupContent,
|
|
21
29
|
SymbolInformation,
|
|
22
30
|
} from "../config/types.ts";
|
|
23
|
-
import type {
|
|
31
|
+
import type { WorkspaceLspRuntime } from "../session/runtime-registry.ts";
|
|
24
32
|
import {
|
|
25
33
|
collectCodeActionResults,
|
|
26
34
|
isDeleteDeadCodeCodeAction,
|
|
@@ -32,74 +40,62 @@ import {
|
|
|
32
40
|
} from "./refactor-planning.ts";
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
* Create a SemanticProvider backed by a
|
|
43
|
+
* Create a SemanticProvider backed by a WorkspaceLspRuntime.
|
|
36
44
|
* Maps LSP types into the shared code-runtime types.
|
|
37
45
|
*/
|
|
38
|
-
export function createLspSemanticProvider(lsp:
|
|
46
|
+
export function createLspSemanticProvider(lsp: WorkspaceLspRuntime): SemanticProvider {
|
|
39
47
|
return {
|
|
40
|
-
async definition(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
for (const item of normalized) {
|
|
46
|
-
const loc = toCodeLocation(item);
|
|
47
|
-
if (loc) mapped.push(loc);
|
|
48
|
-
}
|
|
49
|
-
return mapped;
|
|
48
|
+
async definition(
|
|
49
|
+
filePath: string,
|
|
50
|
+
position: CodePosition,
|
|
51
|
+
): Promise<CodeQueryResult<CodeLocation[]>> {
|
|
52
|
+
return mapCodeQueryResult(await lsp.definition(filePath, position), mapLocations);
|
|
50
53
|
},
|
|
51
54
|
|
|
52
55
|
async hover(
|
|
53
56
|
filePath: string,
|
|
54
57
|
position: CodePosition,
|
|
55
|
-
): Promise<{ contents: string; range?: SourceRange } | null
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
): Promise<CodeQueryResult<{ contents: string; range?: SourceRange } | null>> {
|
|
59
|
+
return mapCodeQueryResult(await lsp.hover(filePath, position), (hover) =>
|
|
60
|
+
hover ? convertLspHover(hover) : null,
|
|
61
|
+
);
|
|
59
62
|
},
|
|
60
63
|
|
|
61
|
-
async references(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const loc = toCodeLocation(item);
|
|
67
|
-
if (loc) mapped.push(loc);
|
|
68
|
-
}
|
|
69
|
-
return mapped;
|
|
64
|
+
async references(
|
|
65
|
+
filePath: string,
|
|
66
|
+
position: CodePosition,
|
|
67
|
+
): Promise<CodeQueryResult<CodeLocation[]>> {
|
|
68
|
+
return mapCodeQueryResult(await lsp.references(filePath, position), mapLocations);
|
|
70
69
|
},
|
|
71
70
|
|
|
72
|
-
async implementation(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for (const item of normalized) {
|
|
78
|
-
const loc = toCodeLocation(item);
|
|
79
|
-
if (loc) mapped.push(loc);
|
|
80
|
-
}
|
|
81
|
-
return mapped;
|
|
71
|
+
async implementation(
|
|
72
|
+
filePath: string,
|
|
73
|
+
position: CodePosition,
|
|
74
|
+
): Promise<CodeQueryResult<CodeLocation[]>> {
|
|
75
|
+
return mapCodeQueryResult(await lsp.implementation(filePath, position), mapLocations);
|
|
82
76
|
},
|
|
83
77
|
|
|
84
|
-
async documentSymbols(filePath: string): Promise<
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
|
|
78
|
+
async documentSymbols(filePath: string): Promise<CodeQueryResult<DocumentCodeSymbol[]>> {
|
|
79
|
+
const result = await lsp.documentSymbols(filePath);
|
|
80
|
+
if (result.kind === "unavailable") return unavailableCodeQuery(result.reason);
|
|
81
|
+
const sourceLines = await readSourceLines(filePath);
|
|
82
|
+
const symbols = flattenDocumentSymbols(result.data, filePath, null, {
|
|
83
|
+
sourceLines,
|
|
84
|
+
nesting: "top-level",
|
|
85
|
+
});
|
|
86
|
+
return result.kind === "partial"
|
|
87
|
+
? partialCodeQuery(symbols, result.reason)
|
|
88
|
+
: completedCodeQuery(symbols);
|
|
88
89
|
},
|
|
89
90
|
|
|
90
|
-
async workspaceSymbols(query: string): Promise<CodeSymbol[]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
async workspaceSymbols(query: string): Promise<CodeQueryResult<CodeSymbol[]>> {
|
|
92
|
+
return mapCodeQueryResult(await lsp.workspaceSymbol(query), (results) =>
|
|
93
|
+
results.map((symbol) => toCodeSymbol(symbol as SymbolInformation)),
|
|
94
|
+
);
|
|
94
95
|
},
|
|
95
96
|
|
|
96
97
|
async refactor(request: RefactorRequest): Promise<RefactorResult> {
|
|
97
|
-
|
|
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) {
|
|
98
|
+
switch (request.operation) {
|
|
103
99
|
case "rename_symbol":
|
|
104
100
|
if (!request.newName) {
|
|
105
101
|
return {
|
|
@@ -134,13 +130,13 @@ export function createLspSemanticProvider(lsp: SessionLspService): SemanticProvi
|
|
|
134
130
|
// shared file/resource edits and rollback semantics exist.
|
|
135
131
|
return {
|
|
136
132
|
kind: "unavailable",
|
|
137
|
-
reason: `Refactor operation "${operation}" is not supported yet. File/resource operations are deferred.`,
|
|
133
|
+
reason: `Refactor operation "${request.operation}" is not supported yet. File/resource operations are deferred.`,
|
|
138
134
|
};
|
|
139
135
|
}
|
|
140
136
|
|
|
141
137
|
return {
|
|
142
138
|
kind: "unavailable",
|
|
143
|
-
reason: `Refactor operation "${operation}" is not supported by the active semantic provider.`,
|
|
139
|
+
reason: `Refactor operation "${request.operation}" is not supported by the active semantic provider.`,
|
|
144
140
|
};
|
|
145
141
|
},
|
|
146
142
|
|
|
@@ -153,22 +149,11 @@ export function createLspSemanticProvider(lsp: SessionLspService): SemanticProvi
|
|
|
153
149
|
if (!actions) return [];
|
|
154
150
|
return collectCodeActionResults(actions);
|
|
155
151
|
},
|
|
156
|
-
|
|
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 }));
|
|
166
|
-
},
|
|
167
152
|
};
|
|
168
153
|
}
|
|
169
154
|
|
|
170
155
|
function runExtractRefactor(
|
|
171
|
-
lsp:
|
|
156
|
+
lsp: WorkspaceLspRuntime,
|
|
172
157
|
request: RefactorRequest,
|
|
173
158
|
operation: "extract_function" | "extract_variable",
|
|
174
159
|
matches: (action: CodeAction) => boolean,
|
|
@@ -192,6 +177,15 @@ function runExtractRefactor(
|
|
|
192
177
|
|
|
193
178
|
// ── Type conversion helpers ───────────────────────────────────────────
|
|
194
179
|
|
|
180
|
+
function mapLocations(value: Location | Location[] | LocationLink[] | null): CodeLocation[] {
|
|
181
|
+
if (!value) return [];
|
|
182
|
+
const locations = Array.isArray(value) ? value : [value];
|
|
183
|
+
return locations.flatMap((item) => {
|
|
184
|
+
const location = toCodeLocation(item);
|
|
185
|
+
return location ? [location] : [];
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
195
189
|
function toCodeLocation(item: Location | LocationLink): CodeLocation | null {
|
|
196
190
|
const loc = item as Record<string, unknown>;
|
|
197
191
|
const uri = loc.uri ?? loc.targetUri;
|
|
@@ -251,26 +245,35 @@ function symbolKindName(kind: number): string {
|
|
|
251
245
|
function flattenDocumentSymbols(
|
|
252
246
|
symbols: DocumentSymbol[] | SymbolInformation[],
|
|
253
247
|
filePath: string,
|
|
254
|
-
container: string | null
|
|
255
|
-
|
|
256
|
-
|
|
248
|
+
container: string | null,
|
|
249
|
+
context: { sourceLines: readonly string[] | null; nesting: DeclarationNesting },
|
|
250
|
+
): DocumentCodeSymbol[] {
|
|
251
|
+
const result: DocumentCodeSymbol[] = [];
|
|
257
252
|
|
|
258
253
|
for (const sym of symbols) {
|
|
259
254
|
// DocumentSymbol.range = full defining node (declaration anchor);
|
|
260
255
|
// .selectionRange = identifier token (name anchor). SymbolInformation has
|
|
261
256
|
// only location.range (declaration) and no selectionRange.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
257
|
+
let document: DocumentSymbol | null = null;
|
|
258
|
+
let information: SymbolInformation | null = null;
|
|
259
|
+
if (isSymbolInformation(sym)) information = sym;
|
|
260
|
+
else document = sym;
|
|
261
|
+
const declStart = document?.range.start ?? information?.location.range.start;
|
|
265
262
|
if (!declStart) continue;
|
|
266
|
-
const nameStart =
|
|
267
|
-
|
|
268
|
-
|
|
263
|
+
const nameStart = document
|
|
264
|
+
? resolveDocumentSymbolNameStart(document, context.sourceLines)
|
|
265
|
+
: null;
|
|
266
|
+
const reportedContainer = information?.containerName ?? container;
|
|
267
|
+
// SymbolInformation is flat: containerName remains metadata, not hierarchy evidence.
|
|
268
|
+
const nesting: DeclarationNesting = information ? "unknown" : context.nesting;
|
|
269
|
+
|
|
270
|
+
const symbol: DocumentCodeSymbol = {
|
|
269
271
|
name: sym.name,
|
|
270
272
|
kind: symbolKindName(sym.kind),
|
|
271
273
|
file: filePath,
|
|
272
274
|
declarationAnchor: { line: declStart.line + 1, character: declStart.character + 1 },
|
|
273
|
-
container,
|
|
275
|
+
container: reportedContainer,
|
|
276
|
+
nesting,
|
|
274
277
|
};
|
|
275
278
|
if (nameStart) {
|
|
276
279
|
symbol.nameAnchor = { line: nameStart.line + 1, character: nameStart.character + 1 };
|
|
@@ -278,21 +281,71 @@ function flattenDocumentSymbols(
|
|
|
278
281
|
|
|
279
282
|
result.push(symbol);
|
|
280
283
|
|
|
281
|
-
if (
|
|
282
|
-
result.push(
|
|
284
|
+
if (document?.children && document.children.length > 0) {
|
|
285
|
+
result.push(
|
|
286
|
+
...flattenDocumentSymbols(document.children, filePath, sym.name, {
|
|
287
|
+
sourceLines: context.sourceLines,
|
|
288
|
+
nesting: "nested",
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
283
291
|
}
|
|
284
292
|
}
|
|
285
293
|
|
|
286
294
|
return result;
|
|
287
295
|
}
|
|
288
296
|
|
|
297
|
+
function isSymbolInformation(
|
|
298
|
+
symbol: DocumentSymbol | SymbolInformation,
|
|
299
|
+
): symbol is SymbolInformation {
|
|
300
|
+
return "location" in symbol;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function readSourceLines(filePath: string): Promise<readonly string[] | null> {
|
|
304
|
+
try {
|
|
305
|
+
return (await readFile(filePath, "utf-8")).split(/\r?\n/);
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Validate the LSP selection range against source text. Some servers return
|
|
313
|
+
* the full overload declaration as selectionRange; recover the exact symbol
|
|
314
|
+
* token on that line rather than labeling the declaration start as a name.
|
|
315
|
+
*/
|
|
316
|
+
function resolveDocumentSymbolNameStart(
|
|
317
|
+
symbol: DocumentSymbol,
|
|
318
|
+
sourceLines: readonly string[] | null,
|
|
319
|
+
): { line: number; character: number } | null {
|
|
320
|
+
const selectionStart = symbol.selectionRange?.start;
|
|
321
|
+
if (!selectionStart) return null;
|
|
322
|
+
if (!sourceLines) return selectionStart;
|
|
323
|
+
|
|
324
|
+
const sourceLine = sourceLines[selectionStart.line];
|
|
325
|
+
if (sourceLine === undefined) return null;
|
|
326
|
+
if (
|
|
327
|
+
sourceLine.slice(selectionStart.character, selectionStart.character + symbol.name.length) ===
|
|
328
|
+
symbol.name
|
|
329
|
+
) {
|
|
330
|
+
return selectionStart;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const rangeStart = symbol.range?.start;
|
|
334
|
+
const rangeEnd = symbol.range?.end;
|
|
335
|
+
const from = rangeStart?.line === selectionStart.line ? rangeStart.character : 0;
|
|
336
|
+
const until = rangeEnd?.line === selectionStart.line ? rangeEnd.character : sourceLine.length;
|
|
337
|
+
const repairedCharacter = sourceLine.indexOf(symbol.name, from);
|
|
338
|
+
if (repairedCharacter < 0 || repairedCharacter + symbol.name.length > until) return null;
|
|
339
|
+
return { line: selectionStart.line, character: repairedCharacter };
|
|
340
|
+
}
|
|
341
|
+
|
|
289
342
|
function toCodeSymbol(sym: SymbolInformation): CodeSymbol {
|
|
290
343
|
const uri = sym.location?.uri ?? "";
|
|
291
344
|
const start = sym.location?.range?.start;
|
|
292
345
|
return {
|
|
293
346
|
name: sym.name,
|
|
294
347
|
kind: symbolKindName(sym.kind),
|
|
295
|
-
file:
|
|
348
|
+
file: uriToFile(uri),
|
|
296
349
|
// SymbolInformation has no selectionRange — nameAnchor is derived later
|
|
297
350
|
// by the orchestration layer's refine, or left absent.
|
|
298
351
|
declarationAnchor: {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { CodePosition, RefactorResult, SourceRange } from "@mrclrchtr/supi-code-runtime/api";
|
|
2
|
+
import { uriToFile } from "@mrclrchtr/supi-core/path";
|
|
2
3
|
import type { CodeAction, TextDocumentEdit, TextEdit, WorkspaceEdit } from "../config/types.ts";
|
|
3
|
-
import type {
|
|
4
|
+
import type { WorkspaceLspRuntime } from "../session/runtime-registry.ts";
|
|
4
5
|
|
|
5
6
|
export async function runRenameRefactor(
|
|
6
|
-
lsp:
|
|
7
|
+
lsp: WorkspaceLspRuntime,
|
|
7
8
|
file: string,
|
|
8
9
|
position: CodePosition,
|
|
9
10
|
newName: string,
|
|
@@ -37,7 +38,7 @@ export function collectCodeActionResults(actions: CodeAction[]): RefactorResult[
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export async function runFilteredCodeActionRefactor(options: {
|
|
40
|
-
lsp:
|
|
41
|
+
lsp: WorkspaceLspRuntime;
|
|
41
42
|
file: string;
|
|
42
43
|
position: CodePosition;
|
|
43
44
|
operation: "update_imports" | "delete_dead_code" | "extract_function" | "extract_variable";
|
|
@@ -146,15 +147,6 @@ function convertLspWorkspaceEdit(edit: WorkspaceEdit | null): RefactorResult {
|
|
|
146
147
|
return { kind: "precise", edits: { edits: fileEdits } };
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
function resolveFileFromUri(uri: string): string {
|
|
150
|
-
if (!uri.startsWith("file://")) return uri;
|
|
151
|
-
try {
|
|
152
|
-
return decodeURIComponent(uri.slice(7));
|
|
153
|
-
} catch {
|
|
154
|
-
return uri;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
150
|
function collectDocumentChangeEdits(
|
|
159
151
|
docChanges: NonNullable<WorkspaceEdit["documentChanges"]>,
|
|
160
152
|
): Array<{
|
|
@@ -170,7 +162,7 @@ function collectDocumentChangeEdits(
|
|
|
170
162
|
for (const change of docChanges) {
|
|
171
163
|
const tdEdit = change as TextDocumentEdit;
|
|
172
164
|
if (!tdEdit.textDocument || !tdEdit.edits) continue;
|
|
173
|
-
const file =
|
|
165
|
+
const file = uriToFile(tdEdit.textDocument.uri);
|
|
174
166
|
for (const singleEdit of tdEdit.edits) {
|
|
175
167
|
const te = singleEdit as TextEdit;
|
|
176
168
|
out.push({
|
|
@@ -198,7 +190,7 @@ function collectChangesEdits(changes: NonNullable<WorkspaceEdit["changes"]>): Ar
|
|
|
198
190
|
}> = [];
|
|
199
191
|
for (const [uri, textEdits] of Object.entries(changes)) {
|
|
200
192
|
if (!textEdits || textEdits.length === 0) continue;
|
|
201
|
-
const file =
|
|
193
|
+
const file = uriToFile(uri);
|
|
202
194
|
for (const te of textEdits) {
|
|
203
195
|
out.push({
|
|
204
196
|
file,
|