@mrclrchtr/supi-lsp 1.16.1 → 2.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 +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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { CodePosition, RefactorResult, SourceRange } from "@mrclrchtr/supi-code-runtime/api";
|
|
2
|
+
import type { CodeAction, TextDocumentEdit, TextEdit, WorkspaceEdit } from "../config/types.ts";
|
|
3
|
+
import type { SessionLspService } from "../session/service-registry.ts";
|
|
4
|
+
|
|
5
|
+
export async function runRenameRefactor(
|
|
6
|
+
lsp: SessionLspService,
|
|
7
|
+
file: string,
|
|
8
|
+
position: CodePosition,
|
|
9
|
+
newName: string,
|
|
10
|
+
): Promise<RefactorResult> {
|
|
11
|
+
const edit = await lsp.rename(file, position, newName);
|
|
12
|
+
return convertLspWorkspaceEdit(edit);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function collectCodeActionResults(actions: CodeAction[]): RefactorResult[] {
|
|
16
|
+
const results: RefactorResult[] = [];
|
|
17
|
+
for (const action of actions) {
|
|
18
|
+
if (!action.edit) {
|
|
19
|
+
results.push({
|
|
20
|
+
kind: "unavailable",
|
|
21
|
+
reason: `Code action "${action.title}" has no edit`,
|
|
22
|
+
});
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = convertCodeActionToResult(action);
|
|
27
|
+
results.push(
|
|
28
|
+
result?.kind === "precise"
|
|
29
|
+
? result
|
|
30
|
+
: {
|
|
31
|
+
kind: "unavailable",
|
|
32
|
+
reason: `Code action "${action.title}" could not produce precise edits`,
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runFilteredCodeActionRefactor(options: {
|
|
40
|
+
lsp: SessionLspService;
|
|
41
|
+
file: string;
|
|
42
|
+
position: CodePosition;
|
|
43
|
+
operation: "update_imports" | "delete_dead_code" | "extract_function" | "extract_variable";
|
|
44
|
+
range?: SourceRange;
|
|
45
|
+
matches: (action: CodeAction) => boolean;
|
|
46
|
+
}): Promise<RefactorResult> {
|
|
47
|
+
const { lsp, file, position, operation, matches } = options;
|
|
48
|
+
const actions = await lsp.codeActions(file, options.range ?? position);
|
|
49
|
+
if (!actions || actions.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
kind: "unavailable",
|
|
52
|
+
reason: `No code actions are available for refactor operation "${operation}".`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const matching = actions.filter(matches);
|
|
57
|
+
if (matching.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
kind: "unavailable",
|
|
60
|
+
reason: `No matching precise code action is available for refactor operation "${operation}".`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const action of matching) {
|
|
65
|
+
const converted = convertCodeActionToResult(action);
|
|
66
|
+
if (converted?.kind === "precise") {
|
|
67
|
+
return converted;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
kind: "unavailable",
|
|
73
|
+
reason: `Matching code actions for refactor operation "${operation}" did not produce precise edits.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isUpdateImportsCodeAction(action: CodeAction): boolean {
|
|
78
|
+
const kind = action.kind ?? "";
|
|
79
|
+
const title = action.title.trim().toLowerCase();
|
|
80
|
+
const kindlessTitleMatch = kind === "" && title === "organize imports";
|
|
81
|
+
return (
|
|
82
|
+
kind === "source.organizeImports" ||
|
|
83
|
+
kind.startsWith("source.organizeImports.") ||
|
|
84
|
+
kindlessTitleMatch
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isExtractFunctionCodeAction(action: CodeAction): boolean {
|
|
89
|
+
const kind = action.kind ?? "";
|
|
90
|
+
const title = action.title.trim().toLowerCase();
|
|
91
|
+
return (
|
|
92
|
+
kind === "refactor.extract.function" ||
|
|
93
|
+
kind.startsWith("refactor.extract.function.") ||
|
|
94
|
+
(title.includes("extract") && /\b(function|method)\b/.test(title))
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isExtractVariableCodeAction(action: CodeAction): boolean {
|
|
99
|
+
const kind = action.kind ?? "";
|
|
100
|
+
const title = action.title.trim().toLowerCase();
|
|
101
|
+
return (
|
|
102
|
+
kind === "refactor.extract.constant" ||
|
|
103
|
+
kind.startsWith("refactor.extract.constant.") ||
|
|
104
|
+
kind === "refactor.extract.variable" ||
|
|
105
|
+
kind.startsWith("refactor.extract.variable.") ||
|
|
106
|
+
(title.includes("extract") && /\b(constant|const|variable)\b/.test(title))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isDeleteDeadCodeCodeAction(action: CodeAction): boolean {
|
|
111
|
+
const kind = action.kind ?? "";
|
|
112
|
+
const title = action.title.trim().toLowerCase();
|
|
113
|
+
const kindMatches =
|
|
114
|
+
kind === "quickfix" ||
|
|
115
|
+
kind.startsWith("quickfix.") ||
|
|
116
|
+
kind === "refactor.rewrite" ||
|
|
117
|
+
kind.startsWith("refactor.rewrite.");
|
|
118
|
+
const titleMatches =
|
|
119
|
+
/(unused|dead code|remove unused|remove unreachable|remove declaration)/.test(title);
|
|
120
|
+
return kindMatches && titleMatches;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function convertCodeActionToResult(action: CodeAction): RefactorResult | null {
|
|
124
|
+
if (!action.edit) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return convertLspWorkspaceEdit(action.edit);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function convertLspWorkspaceEdit(edit: WorkspaceEdit | null): RefactorResult {
|
|
131
|
+
if (!edit) {
|
|
132
|
+
return { kind: "unavailable", reason: "LSP server returned no edit" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let fileEdits = edit.documentChanges?.length
|
|
136
|
+
? collectDocumentChangeEdits(edit.documentChanges)
|
|
137
|
+
: [];
|
|
138
|
+
if (fileEdits.length === 0 && edit.changes) {
|
|
139
|
+
fileEdits = collectChangesEdits(edit.changes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (fileEdits.length === 0) {
|
|
143
|
+
return { kind: "unavailable", reason: "Workspace edit contains no file edits" };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { kind: "precise", edits: { edits: fileEdits } };
|
|
147
|
+
}
|
|
148
|
+
|
|
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
|
+
function collectDocumentChangeEdits(
|
|
159
|
+
docChanges: NonNullable<WorkspaceEdit["documentChanges"]>,
|
|
160
|
+
): Array<{
|
|
161
|
+
file: string;
|
|
162
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
163
|
+
newText: string;
|
|
164
|
+
}> {
|
|
165
|
+
const out: Array<{
|
|
166
|
+
file: string;
|
|
167
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
168
|
+
newText: string;
|
|
169
|
+
}> = [];
|
|
170
|
+
for (const change of docChanges) {
|
|
171
|
+
const tdEdit = change as TextDocumentEdit;
|
|
172
|
+
if (!tdEdit.textDocument || !tdEdit.edits) continue;
|
|
173
|
+
const file = resolveFileFromUri(tdEdit.textDocument.uri);
|
|
174
|
+
for (const singleEdit of tdEdit.edits) {
|
|
175
|
+
const te = singleEdit as TextEdit;
|
|
176
|
+
out.push({
|
|
177
|
+
file,
|
|
178
|
+
range: {
|
|
179
|
+
start: { line: te.range.start.line, character: te.range.start.character },
|
|
180
|
+
end: { line: te.range.end.line, character: te.range.end.character },
|
|
181
|
+
},
|
|
182
|
+
newText: te.newText,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function collectChangesEdits(changes: NonNullable<WorkspaceEdit["changes"]>): Array<{
|
|
190
|
+
file: string;
|
|
191
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
192
|
+
newText: string;
|
|
193
|
+
}> {
|
|
194
|
+
const out: Array<{
|
|
195
|
+
file: string;
|
|
196
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
197
|
+
newText: string;
|
|
198
|
+
}> = [];
|
|
199
|
+
for (const [uri, textEdits] of Object.entries(changes)) {
|
|
200
|
+
if (!textEdits || textEdits.length === 0) continue;
|
|
201
|
+
const file = resolveFileFromUri(uri);
|
|
202
|
+
for (const te of textEdits) {
|
|
203
|
+
out.push({
|
|
204
|
+
file,
|
|
205
|
+
range: {
|
|
206
|
+
start: { line: te.range.start.line, character: te.range.start.character },
|
|
207
|
+
end: { line: te.range.end.line, character: te.range.end.character },
|
|
208
|
+
},
|
|
209
|
+
newText: te.newText,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// LSP session runtime controller — pi-independent lifecycle for LSP session management.
|
|
2
|
+
//
|
|
3
|
+
// This controller owns session start/shutdown for one cwd:
|
|
4
|
+
// - Creates and disposes the LspManager
|
|
5
|
+
// - Publishes SessionLspService states through the existing registry
|
|
6
|
+
// - Exposes the data the umbrella adapter will need later
|
|
7
|
+
//
|
|
8
|
+
// It does NOT import pi event types or ExtensionAPI.
|
|
9
|
+
|
|
10
|
+
import type { WorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
|
|
11
|
+
import { loadConfig } from "../config/config.ts";
|
|
12
|
+
import { type LspSettings, loadLspSettings } from "../config/lsp-settings.ts";
|
|
13
|
+
import { clearTsconfigCache } from "../config/tsconfig-scope.ts";
|
|
14
|
+
import type { DetectedProjectServer, LspConfig, ProjectServerInfo } from "../config/types.ts";
|
|
15
|
+
import { scanWorkspaceSentinels } from "../diagnostics/workspace-sentinels.ts";
|
|
16
|
+
import { LspManager } from "../manager/manager.ts";
|
|
17
|
+
import {
|
|
18
|
+
markLspCapabilitiesReady,
|
|
19
|
+
registerPendingLspCapabilities,
|
|
20
|
+
unregisterLspCapabilities,
|
|
21
|
+
} from "./runtime-registration.ts";
|
|
22
|
+
import { scanMissingServers, scanProjectCapabilities, startDetectedServers } from "./scanner.ts";
|
|
23
|
+
import {
|
|
24
|
+
clearSessionLspService,
|
|
25
|
+
SessionLspService,
|
|
26
|
+
setSessionLspServiceState,
|
|
27
|
+
} from "./service-registry.ts";
|
|
28
|
+
|
|
29
|
+
// ── Types ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Starting state before {@link LspRuntimeController.start} is called. */
|
|
32
|
+
export type LspControllerState =
|
|
33
|
+
| LspControllerInitial
|
|
34
|
+
| LspControllerPending
|
|
35
|
+
| LspControllerReady
|
|
36
|
+
| LspControllerDisabled
|
|
37
|
+
| LspControllerUnavailable;
|
|
38
|
+
|
|
39
|
+
interface LspControllerInitial {
|
|
40
|
+
kind: "initial";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface LspControllerPending {
|
|
44
|
+
kind: "pending";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface LspControllerReady {
|
|
48
|
+
kind: "ready";
|
|
49
|
+
manager: LspManager;
|
|
50
|
+
service: SessionLspService;
|
|
51
|
+
projectServers: ProjectServerInfo[];
|
|
52
|
+
detectedServers: DetectedProjectServer[];
|
|
53
|
+
settings: LspSettings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface LspControllerDisabled {
|
|
57
|
+
kind: "disabled";
|
|
58
|
+
message: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface LspControllerUnavailable {
|
|
62
|
+
kind: "unavailable";
|
|
63
|
+
reason: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Result type from {@link LspRuntimeController.start}. */
|
|
67
|
+
export type LspStartResult =
|
|
68
|
+
| { kind: "ready"; manager: LspManager; service: SessionLspService }
|
|
69
|
+
| { kind: "disabled"; message: string }
|
|
70
|
+
| { kind: "unavailable"; reason: string };
|
|
71
|
+
|
|
72
|
+
// ── Controller ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pi-independent LSP session lifecycle controller.
|
|
76
|
+
*
|
|
77
|
+
* Use this in the umbrella extension (supi-code-intelligence) instead of
|
|
78
|
+
* reaching into substrate extension internals.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const controller = new LspRuntimeController(cwd);
|
|
83
|
+
* const result = await controller.start();
|
|
84
|
+
* if (result.kind === "ready") {
|
|
85
|
+
* // use controller.manager, controller.service
|
|
86
|
+
* }
|
|
87
|
+
* // later
|
|
88
|
+
* await controller.shutdown();
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export class LspRuntimeController {
|
|
92
|
+
readonly #cwd: string;
|
|
93
|
+
#state: LspControllerState;
|
|
94
|
+
#runtime: WorkspaceRuntime | null;
|
|
95
|
+
#readinessGeneration = 0;
|
|
96
|
+
|
|
97
|
+
constructor(cwd: string, runtime?: WorkspaceRuntime) {
|
|
98
|
+
this.#cwd = cwd;
|
|
99
|
+
this.#state = { kind: "initial" };
|
|
100
|
+
this.#runtime = runtime ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The workspace cwd this controller was created for. */
|
|
104
|
+
get cwd(): string {
|
|
105
|
+
return this.#cwd;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Current controller state. */
|
|
109
|
+
get kind(): LspControllerState["kind"] {
|
|
110
|
+
return this.#state.kind;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The LspManager, only available when state is "ready". */
|
|
114
|
+
get manager(): LspManager | null {
|
|
115
|
+
return this.#state.kind === "ready" ? this.#state.manager : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The SessionLspService, only available when state is "ready". */
|
|
119
|
+
get service(): SessionLspService | null {
|
|
120
|
+
return this.#state.kind === "ready" ? this.#state.service : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Project server info, only available when state is "ready". */
|
|
124
|
+
get projectServers(): ProjectServerInfo[] {
|
|
125
|
+
if (this.#state.kind === "ready") return this.#state.projectServers;
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Detected servers, only available when state is "ready". */
|
|
130
|
+
get detectedServers(): DetectedProjectServer[] {
|
|
131
|
+
if (this.#state.kind === "ready") return this.#state.detectedServers;
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** LSP settings used for this session. */
|
|
136
|
+
get settings(): LspSettings | null {
|
|
137
|
+
if (this.#state.kind === "ready") return this.#state.settings;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The WorkspaceRuntime registered for this session's cwd. */
|
|
142
|
+
get runtime(): WorkspaceRuntime | null {
|
|
143
|
+
return this.#runtime;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Attach a WorkspaceRuntime for capability registration. */
|
|
147
|
+
setRuntime(runtime: WorkspaceRuntime): void {
|
|
148
|
+
this.#runtime = runtime;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Start the LSP session for this controller's cwd.
|
|
153
|
+
*
|
|
154
|
+
* Loads settings, creates the manager, starts detected servers,
|
|
155
|
+
* publishes the session service, and registers capabilities.
|
|
156
|
+
*
|
|
157
|
+
* Always attempts detected servers unless they were explicitly disabled
|
|
158
|
+
* per language via `lsp.servers.<language>.enabled: false`.
|
|
159
|
+
* The global `lsp.enabled` and `lsp.active` keys are deprecated and ignored.
|
|
160
|
+
*
|
|
161
|
+
* Returns the start result and updates the controller's state.
|
|
162
|
+
*/
|
|
163
|
+
async start(): Promise<LspStartResult> {
|
|
164
|
+
clearTsconfigCache();
|
|
165
|
+
|
|
166
|
+
// Restart safety: shut down any existing session before creating a new one
|
|
167
|
+
await this.cleanupExistingSession();
|
|
168
|
+
|
|
169
|
+
const lspSettings = loadLspSettings(this.#cwd);
|
|
170
|
+
// Note: lspSettings.enabled is ignored — the global switch is deprecated.
|
|
171
|
+
// Per-language `lsp.servers.<language>.enabled: false` is the supported
|
|
172
|
+
// way to opt out and is already handled by loadConfig.
|
|
173
|
+
// lspSettings.active is also ignored — the allowlist is deprecated.
|
|
174
|
+
|
|
175
|
+
const config = loadConfig(this.#cwd);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
return await this.initializeLspSession(config, lspSettings);
|
|
179
|
+
} catch (error: unknown) {
|
|
180
|
+
return this.setUnavailable(error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Shut down any existing LSP session before starting a new one.
|
|
186
|
+
*/
|
|
187
|
+
private async cleanupExistingSession(): Promise<void> {
|
|
188
|
+
this.#readinessGeneration++;
|
|
189
|
+
if (this.#state.kind !== "ready") return;
|
|
190
|
+
await this.#state.manager.shutdownAll();
|
|
191
|
+
if (this.#runtime) unregisterLspCapabilities(this.#runtime, this.#cwd);
|
|
192
|
+
clearSessionLspService(this.#cwd);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Set controller state to unavailable with the given error. */
|
|
196
|
+
private setUnavailable(error: unknown): LspStartResult {
|
|
197
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
198
|
+
this.#state = { kind: "unavailable", reason };
|
|
199
|
+
setSessionLspServiceState(this.#cwd, { kind: "unavailable", reason });
|
|
200
|
+
return { kind: "unavailable", reason };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Initialize the LSP session: create manager, detect and start servers,
|
|
205
|
+
* publish service state and capabilities.
|
|
206
|
+
*/
|
|
207
|
+
private async initializeLspSession(
|
|
208
|
+
config: LspConfig,
|
|
209
|
+
settings: LspSettings,
|
|
210
|
+
): Promise<LspStartResult> {
|
|
211
|
+
clearSessionLspService(this.#cwd);
|
|
212
|
+
this.#state = { kind: "pending" };
|
|
213
|
+
|
|
214
|
+
const manager = new LspManager(config, this.#cwd);
|
|
215
|
+
manager.setExcludePatterns(settings.exclude);
|
|
216
|
+
setSessionLspServiceState(this.#cwd, { kind: "pending" });
|
|
217
|
+
|
|
218
|
+
const detectedServers = scanProjectCapabilities(config, this.#cwd);
|
|
219
|
+
manager.registerDetectedServers(detectedServers);
|
|
220
|
+
await startDetectedServers(manager, detectedServers);
|
|
221
|
+
|
|
222
|
+
scanWorkspaceSentinels(this.#cwd);
|
|
223
|
+
|
|
224
|
+
const service = new SessionLspService(manager);
|
|
225
|
+
setSessionLspServiceState(this.#cwd, { kind: "ready", service });
|
|
226
|
+
|
|
227
|
+
if (this.#runtime) {
|
|
228
|
+
registerPendingLspCapabilities(this.#runtime, this.#cwd, service);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const projectServers = manager.getKnownProjectServers(detectedServers);
|
|
232
|
+
|
|
233
|
+
this.#state = {
|
|
234
|
+
kind: "ready",
|
|
235
|
+
manager,
|
|
236
|
+
service,
|
|
237
|
+
projectServers,
|
|
238
|
+
detectedServers,
|
|
239
|
+
settings,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const readinessGeneration = ++this.#readinessGeneration;
|
|
243
|
+
void this.promoteSemanticReadiness(manager, detectedServers, readinessGeneration);
|
|
244
|
+
|
|
245
|
+
return { kind: "ready", manager, service };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async promoteSemanticReadiness(
|
|
249
|
+
manager: LspManager,
|
|
250
|
+
detectedServers: DetectedProjectServer[],
|
|
251
|
+
readinessGeneration: number,
|
|
252
|
+
): Promise<void> {
|
|
253
|
+
try {
|
|
254
|
+
await manager.waitUntilWorkspaceReady();
|
|
255
|
+
} catch {
|
|
256
|
+
this.retractPendingCapabilities();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (
|
|
261
|
+
readinessGeneration !== this.#readinessGeneration ||
|
|
262
|
+
this.#state.kind !== "ready" ||
|
|
263
|
+
this.#state.manager !== manager
|
|
264
|
+
) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.#state.projectServers = manager.getKnownProjectServers(detectedServers);
|
|
269
|
+
if (this.#runtime) {
|
|
270
|
+
markLspCapabilitiesReady(this.#runtime, this.#cwd);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Retract the pending semantic registration when warm-up fails.
|
|
276
|
+
* Leaves the workspace in unavailable state so callers see a definitive
|
|
277
|
+
* failure rather than an orphaned pending capability.
|
|
278
|
+
*/
|
|
279
|
+
private retractPendingCapabilities(): void {
|
|
280
|
+
if (this.#runtime) {
|
|
281
|
+
unregisterLspCapabilities(this.#runtime, this.#cwd);
|
|
282
|
+
}
|
|
283
|
+
setSessionLspServiceState(this.#cwd, {
|
|
284
|
+
kind: "unavailable",
|
|
285
|
+
reason: "LSP warm-up failed. Check code_health for server status.",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Shut down the LSP session.
|
|
291
|
+
*
|
|
292
|
+
* Unregisters capabilities, clears the service state, and shuts down
|
|
293
|
+
* all LSP clients.
|
|
294
|
+
*/
|
|
295
|
+
async shutdown(): Promise<void> {
|
|
296
|
+
this.#readinessGeneration++;
|
|
297
|
+
clearTsconfigCache();
|
|
298
|
+
|
|
299
|
+
if (this.#runtime) {
|
|
300
|
+
unregisterLspCapabilities(this.#runtime, this.#cwd);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (this.#cwd) {
|
|
304
|
+
clearSessionLspService(this.#cwd);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (this.#state.kind === "ready") {
|
|
308
|
+
await this.#state.manager.shutdownAll();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.#state = { kind: "initial" };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Get the missing servers warning (servers whose binary is not on PATH). */
|
|
315
|
+
getMissingServers(): Array<{ name: string; command: string }> {
|
|
316
|
+
if (this.#state.kind !== "ready") return [];
|
|
317
|
+
const config = loadConfig(this.#cwd);
|
|
318
|
+
return scanMissingServers(config, this.#cwd);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -11,12 +11,28 @@ import { createLspSemanticProvider } from "../provider/lsp-semantic-provider.ts"
|
|
|
11
11
|
import type { SessionLspService } from "./service-registry.ts";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Register LSP capabilities for a workspace cwd.
|
|
14
|
+
* Register LSP capabilities for a workspace cwd in pending state.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
16
|
+
* Use this once the session service exists but before the language server has
|
|
17
|
+
* finished its initial readiness pipeline. Calls can then be deferred instead
|
|
18
|
+
* of silently observing "no provider" during startup.
|
|
19
|
+
*/
|
|
20
|
+
export function registerPendingLspCapabilities(
|
|
21
|
+
runtime: WorkspaceRuntime,
|
|
22
|
+
cwd: string,
|
|
23
|
+
service: SessionLspService,
|
|
24
|
+
): void {
|
|
25
|
+
const provider = createLspSemanticProvider(service);
|
|
26
|
+
runtime.registerSemanticPending(cwd, provider);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register LSP capabilities for a workspace cwd as ready.
|
|
31
|
+
*
|
|
32
|
+
* Wraps SessionLspService into a SemanticProvider via the existing semantic
|
|
33
|
+
* adapter and publishes it into the shared workspace runtime so that
|
|
34
|
+
* code-intelligence and other consumers can discover semantic analysis
|
|
35
|
+
* availability.
|
|
20
36
|
*/
|
|
21
37
|
export function registerLspCapabilities(
|
|
22
38
|
runtime: WorkspaceRuntime,
|
|
@@ -27,6 +43,11 @@ export function registerLspCapabilities(
|
|
|
27
43
|
runtime.registerSemantic(cwd, provider);
|
|
28
44
|
}
|
|
29
45
|
|
|
46
|
+
/** Promote an already-registered pending semantic provider to ready. */
|
|
47
|
+
export function markLspCapabilitiesReady(runtime: WorkspaceRuntime, cwd: string): void {
|
|
48
|
+
runtime.markSemanticReady(cwd);
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
/**
|
|
31
52
|
* Unregister LSP capabilities for a workspace cwd.
|
|
32
53
|
*
|