@mrclrchtr/supi-lsp 3.2.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.
Files changed (52) hide show
  1. package/README.md +72 -62
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +8 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +2 -4
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
  9. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  10. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +10 -1
  11. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +19 -29
  12. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +7 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +42 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +15 -10
  15. package/node_modules/@mrclrchtr/supi-core/package.json +2 -4
  16. package/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
  17. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
  18. package/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
  19. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
  20. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
  21. package/package.json +4 -5
  22. package/src/api.ts +9 -9
  23. package/src/client/client.ts +70 -41
  24. package/src/client/transport.ts +16 -13
  25. package/src/config/config.ts +2 -2
  26. package/src/config/lsp-settings.ts +0 -2
  27. package/src/index.ts +6 -6
  28. package/src/manager/manager-diagnostics.ts +5 -95
  29. package/src/manager/manager-workspace-recovery.ts +4 -3
  30. package/src/manager/manager-workspace-symbol.ts +44 -6
  31. package/src/manager/manager.ts +47 -65
  32. package/src/provider/lsp-semantic-provider.ts +132 -79
  33. package/src/provider/refactor-planning.ts +6 -14
  34. package/src/session/readiness.ts +38 -0
  35. package/src/session/runtime-controller.ts +87 -75
  36. package/src/session/runtime-registration.ts +5 -5
  37. package/src/session/{service-registry.ts → runtime-registry.ts} +163 -71
  38. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
  39. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
  40. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
  41. package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
  42. package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
  43. package/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
  44. package/src/diagnostics/diagnostic-augmentation.ts +0 -82
  45. package/src/diagnostics/diagnostic-context.ts +0 -116
  46. package/src/diagnostics/diagnostic-display.ts +0 -69
  47. package/src/manager/capability-index.ts +0 -24
  48. package/src/manager/client-pool.ts +0 -33
  49. package/src/manager/diagnostic-store.ts +0 -51
  50. package/src/manager/manager-stale-resync.ts +0 -47
  51. package/src/manager/recovery-coordinator.ts +0 -37
  52. 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<{ results: WorkspaceSymbolLike[]; hasSupport: boolean }> {
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) results.push(...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[] | null> {
64
- const { results, hasSupport } = await collectWorkspaceSymbols(clients, query);
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(
@@ -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
- mapCascadeDiagnosticsToFiles,
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
- /** Wait until all started clients are query-ready, then warm one project file per client/root. */
365
- async waitUntilWorkspaceReady(): Promise<void> {
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) return [];
396
+ if (!client) {
397
+ return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
398
+ }
401
399
  try {
402
- const { primary, cascade } = await syncClientFileAndGetCascadingDiagnostics(
403
- client,
404
- resolvedPath,
405
- maxSeverity,
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
- return [];
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
- refreshedClients: number;
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(query: string): Promise<(SymbolInformation | WorkspaceSymbol)[] | null> {
617
- const helper = await import("./manager-workspace-symbol.ts");
618
- const initial = await helper.collectWorkspaceSymbols(this.clients.values(), query);
619
- if (!initial.hasSupport) return null;
620
- if (initial.results.length > 0) return initial.results;
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
- helper.findWorkspaceSymbolWarmTargets,
624
- helper.getWorkspaceSymbolWarmPosition,
625
- helper.collectWorkspaceSymbols,
619
+ findWorkspaceSymbolWarmTargets,
620
+ getWorkspaceSymbolWarmPosition,
621
+ collectWorkspaceSymbols,
626
622
  query,
627
623
  );
628
- if (warmed.results) return warmed.results;
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: Awaited<ReturnType<LspClient["documentSymbols"]>>,
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; results: (SymbolInformation | WorkspaceSymbol)[] | null }> {
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
- try {
691
- const symbols = await openedClient.documentSymbols(target.file);
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, results: collected.results };
689
+ return { warmedAny, collection: collected };
703
690
  }
704
691
  }
705
692
  }
706
693
 
707
- return { warmedAny, results: null };
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
- 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
- }
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 SessionLspService into the shared
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
- normalizeRefactorOperation,
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 { SessionLspService } from "../session/service-registry.ts";
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 SessionLspService.
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: SessionLspService): SemanticProvider {
46
+ export function createLspSemanticProvider(lsp: WorkspaceLspRuntime): SemanticProvider {
39
47
  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;
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
- const result = await lsp.hover(filePath, position);
57
- if (!result) return null;
58
- return convertLspHover(result);
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(filePath: string, position: CodePosition): Promise<CodeLocation[] | null> {
62
- const refResult = await lsp.references(filePath, position);
63
- if (!refResult) return null;
64
- const mapped: CodeLocation[] = [];
65
- for (const item of refResult) {
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(filePath: string, position: CodePosition): Promise<CodeLocation[] | null> {
73
- const implResult = await lsp.implementation(filePath, position);
74
- if (!implResult) return null;
75
- const normalized = Array.isArray(implResult) ? implResult : [implResult];
76
- const mapped: CodeLocation[] = [];
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<CodeSymbol[] | null> {
85
- const symbols = await lsp.documentSymbols(filePath);
86
- if (!symbols) return null;
87
- return flattenDocumentSymbols(symbols, filePath);
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[] | null> {
91
- const results = await lsp.workspaceSymbol(query);
92
- if (!results) return null;
93
- return results.map((sym) => toCodeSymbol(sym as SymbolInformation));
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
- // 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) {
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: SessionLspService,
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 = null,
255
- ): CodeSymbol[] {
256
- const result: CodeSymbol[] = [];
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
- const ds = sym as DocumentSymbol;
263
- const si = sym as SymbolInformation;
264
- const declStart = ds.range?.start ?? si.location?.range?.start;
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 = ds.selectionRange?.start;
267
-
268
- const symbol: CodeSymbol = {
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 (Array.isArray(ds.children) && ds.children.length > 0) {
282
- result.push(...flattenDocumentSymbols(ds.children, filePath, sym.name));
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: uri.startsWith("file://") ? decodeURIComponent(uri.slice(7)) : uri,
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 { SessionLspService } from "../session/service-registry.ts";
4
+ import type { WorkspaceLspRuntime } from "../session/runtime-registry.ts";
4
5
 
5
6
  export async function runRenameRefactor(
6
- lsp: SessionLspService,
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: SessionLspService;
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 = resolveFileFromUri(tdEdit.textDocument.uri);
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 = resolveFileFromUri(uri);
193
+ const file = uriToFile(uri);
202
194
  for (const te of textEdits) {
203
195
  out.push({
204
196
  file,