@mrclrchtr/supi-lsp 7.4.0 → 8.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 +29 -6
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +22 -4
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +110 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +6 -1
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +2 -1
- package/package.json +3 -3
- package/src/api.ts +25 -3
- package/src/client/client-diagnostic-host.ts +2 -1
- package/src/client/client-diagnostic-refresh.ts +5 -2
- package/src/client/client-diagnostics.ts +39 -6
- package/src/client/client-document-sync.ts +82 -12
- package/src/client/client-semantic-input-barrier.ts +28 -5
- package/src/client/client.ts +606 -30
- package/src/config/capabilities.ts +9 -0
- package/src/config/config.ts +69 -8
- package/src/config/operation-support.ts +196 -0
- package/src/config/server-config.ts +15 -0
- package/src/config/types.ts +9 -1
- package/src/manager/manager-project-info.ts +2 -0
- package/src/manager/manager-workspace-symbol.ts +31 -2
- package/src/manager/manager.ts +72 -2
- package/src/provider/lsp-refactor-provider.ts +9 -43
- package/src/provider/refactor-planning.ts +102 -50
- package/src/provider/semantic-edit-normalizer.ts +49 -3
- package/src/session/runtime-controller.ts +78 -18
- package/src/session/runtime-registry.ts +46 -2
- package/src/session/workspace-lsp-runtime.ts +14 -0
- package/src/workspace-path-policy.ts +30 -4
- package/src/config/lsp-settings.ts +0 -22
|
@@ -38,14 +38,22 @@ export const CLIENT_CAPABILITIES: ClientCapabilities = {
|
|
|
38
38
|
"quickfix",
|
|
39
39
|
"refactor",
|
|
40
40
|
"refactor.extract",
|
|
41
|
+
"refactor.extract.function",
|
|
42
|
+
"refactor.extract.constant",
|
|
43
|
+
"refactor.extract.variable",
|
|
41
44
|
"refactor.inline",
|
|
42
45
|
"refactor.rewrite",
|
|
43
46
|
"source",
|
|
44
47
|
"source.organizeImports",
|
|
45
48
|
"source.fixAll",
|
|
49
|
+
"source.removeUnused",
|
|
46
50
|
],
|
|
47
51
|
},
|
|
48
52
|
},
|
|
53
|
+
dataSupport: true,
|
|
54
|
+
// SuPi resolves only the edit property. Commands and other fields
|
|
55
|
+
// must remain server-owned and are never advertised as resolvable.
|
|
56
|
+
resolveSupport: { properties: ["edit"] },
|
|
49
57
|
},
|
|
50
58
|
publishDiagnostics: {
|
|
51
59
|
relatedInformation: true,
|
|
@@ -64,6 +72,7 @@ export const CLIENT_CAPABILITIES: ClientCapabilities = {
|
|
|
64
72
|
workDoneProgress: true,
|
|
65
73
|
},
|
|
66
74
|
workspace: {
|
|
75
|
+
configuration: true,
|
|
67
76
|
workspaceFolders: true,
|
|
68
77
|
workspaceEdit: {
|
|
69
78
|
documentChanges: true,
|
package/src/config/config.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { loadSupiConfigForScope } from "@mrclrchtr/supi-core/config";
|
|
6
|
+
import { loadSupiConfigForScope, loadSupiConfigSectionForScope } from "@mrclrchtr/supi-core/config";
|
|
7
7
|
import { truncateDebugIdentity } from "@mrclrchtr/supi-core/debug";
|
|
8
|
-
import type { LspConfig, ServerConfig } from "./types.ts";
|
|
8
|
+
import type { JsonObject, JsonValue, LspConfig, ServerConfig } from "./types.ts";
|
|
9
9
|
|
|
10
10
|
const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
@@ -18,6 +18,8 @@ const DEFAULTS: LspConfig = JSON.parse(
|
|
|
18
18
|
|
|
19
19
|
export interface LoadConfigOptions {
|
|
20
20
|
homeDir?: string;
|
|
21
|
+
/** Whether project-scoped server configuration is trusted. Defaults to true. */
|
|
22
|
+
projectTrusted?: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** Map from language alias → canonical config key. */
|
|
@@ -25,6 +27,7 @@ export const LANGUAGE_ALIASES: Record<string, string> = {
|
|
|
25
27
|
cpp: "c",
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
/** Alias fields override canonical fields, independent of property order. */
|
|
28
31
|
function resolveAliasesInOverrides(servers: Record<string, unknown>): void {
|
|
29
32
|
for (const [alias, target] of Object.entries(LANGUAGE_ALIASES)) {
|
|
30
33
|
if (!Object.hasOwn(servers, alias)) continue;
|
|
@@ -52,18 +55,60 @@ export function loadConfig(cwd: string, options?: LoadConfigOptions): LspConfig
|
|
|
52
55
|
{ servers: {} as Record<string, ServerConfig> },
|
|
53
56
|
{ scope: "global", homeDir: options?.homeDir },
|
|
54
57
|
);
|
|
55
|
-
const projectLsp =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const projectLsp =
|
|
59
|
+
options?.projectTrusted === false
|
|
60
|
+
? { servers: {} as Record<string, ServerConfig> }
|
|
61
|
+
: loadSupiConfigForScope(
|
|
62
|
+
"lsp",
|
|
63
|
+
cwd,
|
|
64
|
+
{ servers: {} as Record<string, ServerConfig> },
|
|
65
|
+
{ scope: "project" },
|
|
66
|
+
);
|
|
61
67
|
|
|
62
68
|
const merged = mergeServerConfigs(defaults.servers, globalLsp.servers, projectLsp.servers);
|
|
63
69
|
|
|
64
70
|
return { servers: merged };
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Read effective explicit language disablement for the active LSP snapshot.
|
|
75
|
+
* Project values override global values, including an explicit re-enable.
|
|
76
|
+
*/
|
|
77
|
+
export function getExplicitlyDisabledLanguages(cwd: string, options?: LoadConfigOptions): string[] {
|
|
78
|
+
const effective = new Map<string, boolean>();
|
|
79
|
+
applyEnabledOverrides(
|
|
80
|
+
effective,
|
|
81
|
+
loadSupiConfigSectionForScope("lsp", cwd, {
|
|
82
|
+
scope: "global",
|
|
83
|
+
homeDir: options?.homeDir,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
if (options?.projectTrusted !== false) {
|
|
87
|
+
applyEnabledOverrides(
|
|
88
|
+
effective,
|
|
89
|
+
loadSupiConfigSectionForScope("lsp", cwd, { scope: "project" }),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return [...effective]
|
|
93
|
+
.filter(([, enabled]) => enabled === false)
|
|
94
|
+
.map(([name]) => name)
|
|
95
|
+
.sort((a, b) => a.localeCompare(b));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applyEnabledOverrides(
|
|
99
|
+
effective: Map<string, boolean>,
|
|
100
|
+
section: Record<string, unknown> | null,
|
|
101
|
+
): void {
|
|
102
|
+
const servers = section?.servers;
|
|
103
|
+
if (!isRecord(servers)) return;
|
|
104
|
+
const normalized = { ...servers };
|
|
105
|
+
resolveAliasesInOverrides(normalized);
|
|
106
|
+
for (const [name, value] of Object.entries(normalized)) {
|
|
107
|
+
if (!isRecord(value) || typeof value.enabled !== "boolean") continue;
|
|
108
|
+
effective.set(name, value.enabled);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
67
112
|
/**
|
|
68
113
|
* Find which server config handles a given file extension.
|
|
69
114
|
* Returns [languageName, config] or null.
|
|
@@ -217,6 +262,11 @@ const SERVER_OVERRIDE_SETTERS: Record<string, ServerOverrideSetter> = {
|
|
|
217
262
|
override.env = candidate;
|
|
218
263
|
return true;
|
|
219
264
|
},
|
|
265
|
+
settings(override, candidate) {
|
|
266
|
+
if (!isJsonObject(candidate)) return false;
|
|
267
|
+
override.settings = candidate;
|
|
268
|
+
return true;
|
|
269
|
+
},
|
|
220
270
|
initializationOptions(override, candidate) {
|
|
221
271
|
override.initializationOptions = candidate;
|
|
222
272
|
return true;
|
|
@@ -262,6 +312,17 @@ function isStringRecord(value: unknown): value is Record<string, string> {
|
|
|
262
312
|
return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
263
313
|
}
|
|
264
314
|
|
|
315
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
316
|
+
return isJsonValue(value) && isRecord(value);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isJsonValue(value: unknown): value is JsonValue {
|
|
320
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
321
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
322
|
+
if (Array.isArray(value)) return value.every((entry) => isJsonValue(entry));
|
|
323
|
+
return isRecord(value) && Object.values(value).every((entry) => isJsonValue(entry));
|
|
324
|
+
}
|
|
325
|
+
|
|
265
326
|
function warnInvalidServerConfig(name: string, fields: readonly string[], skipped: boolean): void {
|
|
266
327
|
const boundedName = truncateDebugIdentity(name);
|
|
267
328
|
const boundedFields = truncateDebugIdentity(fields.join(", "));
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ServerCapabilities } from "vscode-languageserver-protocol";
|
|
2
|
+
|
|
3
|
+
/** Refactor operations that the public LSP runtime can report. */
|
|
4
|
+
export type ServerOperationName =
|
|
5
|
+
| "rename_symbol"
|
|
6
|
+
| "prepare_rename"
|
|
7
|
+
| "extract_function"
|
|
8
|
+
| "extract_variable"
|
|
9
|
+
| "update_imports"
|
|
10
|
+
| "delete_dead_code"
|
|
11
|
+
| "code_action_resolve";
|
|
12
|
+
|
|
13
|
+
/** Whether a server advertises one operation in its negotiated capabilities. */
|
|
14
|
+
export type ServerAdvertisement = "advertised" | "not-advertised" | "unknown";
|
|
15
|
+
|
|
16
|
+
/** SuPi support and server advertisement for one operation. */
|
|
17
|
+
export interface ServerOperationSupport {
|
|
18
|
+
readonly server: ServerAdvertisement;
|
|
19
|
+
readonly supi: "supported" | "limited" | "unsupported";
|
|
20
|
+
/** A stable explanation for a SuPi planning limitation or deferral. */
|
|
21
|
+
readonly reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Operation support projected onto a project-server health entry. */
|
|
25
|
+
export type ServerOperationSupportMap = Partial<
|
|
26
|
+
Record<ServerOperationName, ServerOperationSupport>
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
/** Code-action kinds requested by each operation-specific planner. */
|
|
30
|
+
export const REFACTOR_CODE_ACTION_KINDS = {
|
|
31
|
+
extract_function: ["refactor.extract.function"],
|
|
32
|
+
extract_variable: ["refactor.extract.constant", "refactor.extract.variable"],
|
|
33
|
+
update_imports: ["source.organizeImports"],
|
|
34
|
+
delete_dead_code: ["source.removeUnused"],
|
|
35
|
+
} as const satisfies Record<
|
|
36
|
+
"extract_function" | "extract_variable" | "update_imports" | "delete_dead_code",
|
|
37
|
+
readonly string[]
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
const EXTRACTION_NAME_LIMITATION =
|
|
41
|
+
"Standard LSP code actions do not provide a safe exact extraction-name contract.";
|
|
42
|
+
|
|
43
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function codeActionOptions(capabilities: ServerCapabilities | null): {
|
|
48
|
+
kinds?: readonly string[];
|
|
49
|
+
resolveProvider?: boolean;
|
|
50
|
+
} | null {
|
|
51
|
+
const provider = capabilities?.codeActionProvider;
|
|
52
|
+
if (provider === true) return {};
|
|
53
|
+
if (!isRecord(provider)) return null;
|
|
54
|
+
return {
|
|
55
|
+
...(Array.isArray(provider.codeActionKinds)
|
|
56
|
+
? {
|
|
57
|
+
kinds: provider.codeActionKinds.filter(
|
|
58
|
+
(kind): kind is string => typeof kind === "string",
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
: {}),
|
|
62
|
+
...(provider.resolveProvider === true ? { resolveProvider: true } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renameProvider(
|
|
67
|
+
capabilities: ServerCapabilities | null,
|
|
68
|
+
): Record<string, unknown> | true | null {
|
|
69
|
+
const provider = capabilities?.renameProvider;
|
|
70
|
+
if (provider === true) return true;
|
|
71
|
+
return isRecord(provider) ? provider : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Return true when a server advertises rename requests. */
|
|
75
|
+
export function supportsRename(capabilities: ServerCapabilities | null): boolean {
|
|
76
|
+
return renameProvider(capabilities) !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Return true when a server advertises `prepareRename`. */
|
|
80
|
+
export function supportsPrepareRename(capabilities: ServerCapabilities | null): boolean {
|
|
81
|
+
const provider = renameProvider(capabilities);
|
|
82
|
+
return isRecord(provider) && provider.prepareProvider === true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Return true when a server advertises code actions. */
|
|
86
|
+
export function supportsCodeActions(capabilities: ServerCapabilities | null): boolean {
|
|
87
|
+
return codeActionOptions(capabilities) !== null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Return true when a server advertises code-action resolution. */
|
|
91
|
+
export function supportsCodeActionResolve(capabilities: ServerCapabilities | null): boolean {
|
|
92
|
+
return codeActionOptions(capabilities)?.resolveProvider === true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Test the documented hierarchical relationship between two code-action kinds. */
|
|
96
|
+
export function isCodeActionKindInRange(kind: string, requested: string): boolean {
|
|
97
|
+
if (kind === "" || requested === "") return true;
|
|
98
|
+
return kind === requested || kind.startsWith(`${requested}.`) || requested.startsWith(`${kind}.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check whether a requested kind can be supplied by a server's advertised
|
|
103
|
+
* code-action kinds. An absent kind list means the server did not narrow its
|
|
104
|
+
* provider, so the request remains eligible.
|
|
105
|
+
*/
|
|
106
|
+
export function supportsRequestedCodeActionKinds(
|
|
107
|
+
capabilities: ServerCapabilities | null,
|
|
108
|
+
requested: readonly string[],
|
|
109
|
+
): boolean {
|
|
110
|
+
const options = codeActionOptions(capabilities);
|
|
111
|
+
if (!options || requested.length === 0 || !options.kinds) return options !== null;
|
|
112
|
+
if (options.kinds.length === 0) return false;
|
|
113
|
+
return requested.some((wanted) =>
|
|
114
|
+
options.kinds?.some((kind) => isCodeActionKindInRange(kind, wanted)),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function codeActionAdvertisement(
|
|
119
|
+
capabilities: ServerCapabilities | null,
|
|
120
|
+
requested: readonly string[],
|
|
121
|
+
): ServerAdvertisement {
|
|
122
|
+
if (capabilities === null) return "unknown";
|
|
123
|
+
const options = codeActionOptions(capabilities);
|
|
124
|
+
if (!options) return "not-advertised";
|
|
125
|
+
if (requested.length === 0 || !options.kinds) return "unknown";
|
|
126
|
+
if (!supportsRequestedCodeActionKinds(capabilities, requested)) return "not-advertised";
|
|
127
|
+
|
|
128
|
+
// A generic parent or the empty kind proves eligibility, but it does not
|
|
129
|
+
// prove that this exact operation is implemented by the server.
|
|
130
|
+
const exactOrChild = requested.some((wanted) =>
|
|
131
|
+
options.kinds?.some(
|
|
132
|
+
(kind) => kind === wanted || (wanted !== "" && kind.startsWith(`${wanted}.`)),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
return exactOrChild ? "advertised" : "unknown";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function supportedOperation(
|
|
139
|
+
server: ServerAdvertisement,
|
|
140
|
+
supi: ServerOperationSupport["supi"] = "supported",
|
|
141
|
+
reason?: string,
|
|
142
|
+
): ServerOperationSupport {
|
|
143
|
+
return { server, supi, ...(reason ? { reason } : {}) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Build the honest operation-support projection for one negotiated server. */
|
|
147
|
+
export function getServerOperationSupport(
|
|
148
|
+
capabilities: ServerCapabilities | null,
|
|
149
|
+
): ServerOperationSupportMap {
|
|
150
|
+
const renameServer =
|
|
151
|
+
capabilities === null
|
|
152
|
+
? "unknown"
|
|
153
|
+
: supportsRename(capabilities)
|
|
154
|
+
? "advertised"
|
|
155
|
+
: "not-advertised";
|
|
156
|
+
const prepareServer =
|
|
157
|
+
capabilities === null
|
|
158
|
+
? "unknown"
|
|
159
|
+
: supportsPrepareRename(capabilities)
|
|
160
|
+
? "advertised"
|
|
161
|
+
: "not-advertised";
|
|
162
|
+
const resolveServer =
|
|
163
|
+
capabilities === null
|
|
164
|
+
? "unknown"
|
|
165
|
+
: supportsCodeActionResolve(capabilities)
|
|
166
|
+
? "advertised"
|
|
167
|
+
: "not-advertised";
|
|
168
|
+
return {
|
|
169
|
+
rename_symbol: supportedOperation(renameServer),
|
|
170
|
+
prepare_rename: supportedOperation(prepareServer),
|
|
171
|
+
extract_function: supportedOperation(
|
|
172
|
+
codeActionAdvertisement(capabilities, REFACTOR_CODE_ACTION_KINDS.extract_function),
|
|
173
|
+
"limited",
|
|
174
|
+
EXTRACTION_NAME_LIMITATION,
|
|
175
|
+
),
|
|
176
|
+
extract_variable: supportedOperation(
|
|
177
|
+
codeActionAdvertisement(capabilities, REFACTOR_CODE_ACTION_KINDS.extract_variable),
|
|
178
|
+
"limited",
|
|
179
|
+
EXTRACTION_NAME_LIMITATION,
|
|
180
|
+
),
|
|
181
|
+
update_imports: supportedOperation(
|
|
182
|
+
codeActionAdvertisement(capabilities, REFACTOR_CODE_ACTION_KINDS.update_imports),
|
|
183
|
+
),
|
|
184
|
+
delete_dead_code: supportedOperation(
|
|
185
|
+
codeActionAdvertisement(capabilities, REFACTOR_CODE_ACTION_KINDS.delete_dead_code),
|
|
186
|
+
),
|
|
187
|
+
code_action_resolve: supportedOperation(resolveServer),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Get the exact operation-specific kinds used in a code-action request. */
|
|
192
|
+
export function codeActionKindsForOperation(
|
|
193
|
+
operation: keyof typeof REFACTOR_CODE_ACTION_KINDS,
|
|
194
|
+
): readonly string[] {
|
|
195
|
+
return REFACTOR_CODE_ACTION_KINDS[operation];
|
|
196
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// SuPi-specific server configuration types — not part of the LSP specification.
|
|
2
|
+
|
|
3
|
+
import type { ServerOperationSupportMap } from "./operation-support.ts";
|
|
2
4
|
// These are our own types for server discovery, configuration, and status tracking.
|
|
3
5
|
|
|
6
|
+
/** JSON values accepted by a server's advanced settings object. */
|
|
7
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
|
|
8
|
+
|
|
9
|
+
/** A JSON object passed to a language server as advanced settings. */
|
|
10
|
+
export interface JsonObject {
|
|
11
|
+
readonly [key: string]: JsonValue;
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
export interface ServerConfig {
|
|
5
15
|
command: string;
|
|
6
16
|
args?: string[];
|
|
@@ -10,6 +20,9 @@ export interface ServerConfig {
|
|
|
10
20
|
enabled?: boolean;
|
|
11
21
|
/** Environment values added when the server process starts. */
|
|
12
22
|
env?: Record<string, string>;
|
|
23
|
+
/** Server-owned configuration settings. This object replaces inherited settings as a whole. */
|
|
24
|
+
settings?: JsonObject;
|
|
25
|
+
/** Options sent during initialize; kept separate from `settings`. */
|
|
13
26
|
initializationOptions?: unknown;
|
|
14
27
|
/** Maximum time to wait for a single $/progress cycle, in ms. Default: 10_000. */
|
|
15
28
|
readinessTimeoutMs?: number;
|
|
@@ -39,6 +52,8 @@ export interface ProjectServerInfo extends DetectedProjectServer {
|
|
|
39
52
|
openFiles: string[];
|
|
40
53
|
/** Whether the LSP server is currently not indexing and ready to serve queries. */
|
|
41
54
|
ready: boolean;
|
|
55
|
+
/** Negotiated operation support, when the route has a capability snapshot. */
|
|
56
|
+
operationSupport?: ServerOperationSupportMap;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
/** A language whose source files are present but the server binary is missing. */
|
package/src/config/types.ts
CHANGED
|
@@ -24,6 +24,7 @@ export {
|
|
|
24
24
|
AnnotatedTextEdit,
|
|
25
25
|
CodeAction,
|
|
26
26
|
type CodeActionContext,
|
|
27
|
+
CodeActionTriggerKind,
|
|
27
28
|
Command,
|
|
28
29
|
Diagnostic,
|
|
29
30
|
DiagnosticRelatedInformation,
|
|
@@ -48,10 +49,17 @@ export {
|
|
|
48
49
|
WorkspaceEdit,
|
|
49
50
|
WorkspaceSymbol,
|
|
50
51
|
} from "vscode-languageserver-types";
|
|
51
|
-
|
|
52
|
+
export type {
|
|
53
|
+
ServerAdvertisement,
|
|
54
|
+
ServerOperationName,
|
|
55
|
+
ServerOperationSupport,
|
|
56
|
+
ServerOperationSupportMap,
|
|
57
|
+
} from "./operation-support.ts";
|
|
52
58
|
// ── SuPi-specific server config ──────────────────────────────────────
|
|
53
59
|
export type {
|
|
54
60
|
DetectedProjectServer,
|
|
61
|
+
JsonObject,
|
|
62
|
+
JsonValue,
|
|
55
63
|
LspConfig,
|
|
56
64
|
MissingServer,
|
|
57
65
|
ProjectServerInfo,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LspClient } from "../client/client.ts";
|
|
2
|
+
import { getServerOperationSupport } from "../config/operation-support.ts";
|
|
2
3
|
import type { ProjectServerInfo, ProjectServerStatusReason } from "../config/server-config.ts";
|
|
3
4
|
import { displayRelativeFilePath } from "../summary.ts";
|
|
4
5
|
|
|
@@ -38,5 +39,6 @@ export function buildProjectServerInfo(
|
|
|
38
39
|
.filter((file) => input.includeOpenFile?.(file) ?? true)
|
|
39
40
|
.map((file) => displayRelativeFilePath(file, cwd)) ?? [],
|
|
40
41
|
ready: input.client?.ready ?? false,
|
|
42
|
+
operationSupport: getServerOperationSupport(input.client?.serverCapabilities ?? null),
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -55,6 +55,10 @@ export interface WorkspaceSymbolCollection {
|
|
|
55
55
|
hasSupport: boolean;
|
|
56
56
|
completedClientCount: number;
|
|
57
57
|
failures: string[];
|
|
58
|
+
/** Results removed by the automatic path policy. */
|
|
59
|
+
excludedCount?: number;
|
|
60
|
+
/** Bounded paths removed by the automatic path policy. */
|
|
61
|
+
excludedExamples?: string[];
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export async function collectWorkspaceSymbols(
|
|
@@ -84,6 +88,16 @@ export async function collectWorkspaceSymbols(
|
|
|
84
88
|
return { results, hasSupport, completedClientCount, failures };
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
function workspaceSymbolExclusionReason(collection: WorkspaceSymbolCollection): string | null {
|
|
92
|
+
const excludedCount = collection.excludedCount ?? 0;
|
|
93
|
+
if (excludedCount === 0) return null;
|
|
94
|
+
const noun = `workspace-symbol result${excludedCount === 1 ? "" : "s"}`;
|
|
95
|
+
const examples = collection.excludedExamples?.length
|
|
96
|
+
? ` Examples: ${collection.excludedExamples.join(", ")}.`
|
|
97
|
+
: "";
|
|
98
|
+
return `Automatic path policy excluded ${excludedCount} ${noun}.${examples}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
/** Project one multi-client collection into the shared typed query contract. */
|
|
88
102
|
export function workspaceSymbolCollectionResult(
|
|
89
103
|
collection: WorkspaceSymbolCollection,
|
|
@@ -96,8 +110,23 @@ export function workspaceSymbolCollectionResult(
|
|
|
96
110
|
collection.failures.join("; ") || "No workspace-symbol request completed.",
|
|
97
111
|
);
|
|
98
112
|
}
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
const exclusion = workspaceSymbolExclusionReason(collection);
|
|
114
|
+
if (collection.failures.length > 0 || exclusion) {
|
|
115
|
+
const excludedCount = collection.excludedCount ?? 0;
|
|
116
|
+
const metadata =
|
|
117
|
+
excludedCount > 0
|
|
118
|
+
? {
|
|
119
|
+
automaticPathExclusion: {
|
|
120
|
+
excludedCount,
|
|
121
|
+
examples: [...(collection.excludedExamples ?? [])],
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
: undefined;
|
|
125
|
+
return partialCodeQuery(
|
|
126
|
+
collection.results,
|
|
127
|
+
[collection.failures.join("; "), exclusion].filter(Boolean).join("; "),
|
|
128
|
+
metadata,
|
|
129
|
+
);
|
|
101
130
|
}
|
|
102
131
|
return completedCodeQuery(collection.results);
|
|
103
132
|
}
|
package/src/manager/manager.ts
CHANGED
|
@@ -1938,6 +1938,10 @@ export class LspManager {
|
|
|
1938
1938
|
{ policy: this.automaticPathPolicy },
|
|
1939
1939
|
)[0];
|
|
1940
1940
|
if (target) await this.warmSemanticProject(firstReady, target.file, false, control);
|
|
1941
|
+
// The warm-up request can start server indexing after the initial
|
|
1942
|
+
// readiness wait resolves. Observe the resulting progress before
|
|
1943
|
+
// reporting the workspace as ready.
|
|
1944
|
+
await firstReady.getReady(control);
|
|
1941
1945
|
}
|
|
1942
1946
|
|
|
1943
1947
|
throwIfCodeRequestInterrupted(control);
|
|
@@ -2126,6 +2130,23 @@ export class LspManager {
|
|
|
2126
2130
|
this.notifyWorkspaceFileChanges(changes);
|
|
2127
2131
|
}
|
|
2128
2132
|
|
|
2133
|
+
/** Synchronize tracked files after successful writes and send requested saves. */
|
|
2134
|
+
noteWorkspaceWrites(changes: FileEvent[]): void {
|
|
2135
|
+
if (changes.length === 0) return;
|
|
2136
|
+
this.noteWorkspaceChanges(changes);
|
|
2137
|
+
|
|
2138
|
+
const writtenFiles = [
|
|
2139
|
+
...new Set(
|
|
2140
|
+
changes
|
|
2141
|
+
.filter((change) => change.type === FileChangeType.Changed)
|
|
2142
|
+
.map((change) => uriToFile(change.uri)),
|
|
2143
|
+
),
|
|
2144
|
+
];
|
|
2145
|
+
for (const client of this.clients.values()) {
|
|
2146
|
+
for (const filePath of writtenFiles) client.noteWorkspaceWrite(filePath);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2129
2150
|
/** Return per-route diagnostic evidence capability for recovery targeting. */
|
|
2130
2151
|
getClientDiagnosticRoutes(): Array<{
|
|
2131
2152
|
key: string;
|
|
@@ -2630,8 +2651,8 @@ export class LspManager {
|
|
|
2630
2651
|
: null,
|
|
2631
2652
|
control,
|
|
2632
2653
|
);
|
|
2633
|
-
const collect = async (clients: Iterable<LspClient>, value: string) =>
|
|
2634
|
-
this.addWorkspaceSymbolDemandEvidence(
|
|
2654
|
+
const collect = async (clients: Iterable<LspClient>, value: string) => {
|
|
2655
|
+
const collection = this.addWorkspaceSymbolDemandEvidence(
|
|
2635
2656
|
await collectWorkspaceSymbols(
|
|
2636
2657
|
Array.from(clients).filter((client) => routeMatches(client)),
|
|
2637
2658
|
value,
|
|
@@ -2639,6 +2660,8 @@ export class LspManager {
|
|
|
2639
2660
|
),
|
|
2640
2661
|
demand,
|
|
2641
2662
|
);
|
|
2663
|
+
return this.filterWorkspaceSymbolCollection(collection, resolvedScopes);
|
|
2664
|
+
};
|
|
2642
2665
|
const initial = await collect(this.clients.values(), query);
|
|
2643
2666
|
if (!initial.hasSupport || initial.results.length > 0) {
|
|
2644
2667
|
return workspaceSymbolCollectionResult(initial);
|
|
@@ -2669,6 +2692,53 @@ export class LspManager {
|
|
|
2669
2692
|
};
|
|
2670
2693
|
}
|
|
2671
2694
|
|
|
2695
|
+
private filterWorkspaceSymbolCollection(
|
|
2696
|
+
collection: WorkspaceSymbolCollection,
|
|
2697
|
+
scopes: readonly string[] | undefined,
|
|
2698
|
+
): WorkspaceSymbolCollection {
|
|
2699
|
+
const excludedExamples: string[] = [];
|
|
2700
|
+
let excludedCount = 0;
|
|
2701
|
+
const results = collection.results.filter((result) => {
|
|
2702
|
+
const file = uriToFile(result.location.uri);
|
|
2703
|
+
if (this.isWorkspaceSymbolResultAllowed(file, scopes)) return true;
|
|
2704
|
+
excludedCount++;
|
|
2705
|
+
if (excludedExamples.length < 5) {
|
|
2706
|
+
excludedExamples.push(displayRelativeFilePath(file, this.cwd));
|
|
2707
|
+
}
|
|
2708
|
+
return false;
|
|
2709
|
+
});
|
|
2710
|
+
return {
|
|
2711
|
+
...collection,
|
|
2712
|
+
results,
|
|
2713
|
+
excludedCount: (collection.excludedCount ?? 0) + excludedCount,
|
|
2714
|
+
excludedExamples: [...(collection.excludedExamples ?? []), ...excludedExamples].slice(0, 5),
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
/** Keep explicitly selected excluded scopes inspectable while filtering broad results. */
|
|
2719
|
+
private isWorkspaceSymbolResultAllowed(
|
|
2720
|
+
filePath: string,
|
|
2721
|
+
scopes: readonly string[] | undefined,
|
|
2722
|
+
): boolean {
|
|
2723
|
+
if (this.automaticPathPolicy.isEligible(filePath, "file")) return true;
|
|
2724
|
+
if (!scopes || scopes.length === 0) return false;
|
|
2725
|
+
|
|
2726
|
+
const resolvedFile = path.resolve(filePath);
|
|
2727
|
+
return scopes.some((scope) => {
|
|
2728
|
+
const resolvedScope = path.resolve(scope);
|
|
2729
|
+
if (resolvedScope === resolvedFile) return true;
|
|
2730
|
+
try {
|
|
2731
|
+
if (!fs.statSync(resolvedScope).isDirectory()) return false;
|
|
2732
|
+
} catch {
|
|
2733
|
+
return false;
|
|
2734
|
+
}
|
|
2735
|
+
return (
|
|
2736
|
+
!this.automaticPathPolicy.isEligible(resolvedScope, "directory") &&
|
|
2737
|
+
projectRoots.isWithinOrEqual(resolvedScope, resolvedFile)
|
|
2738
|
+
);
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2672
2742
|
private hasWorkspaceSymbolRouteForScopes(scopes: readonly string[]): boolean {
|
|
2673
2743
|
const clients = [
|
|
2674
2744
|
...this.clients.values(),
|
|
@@ -4,13 +4,10 @@ import type {
|
|
|
4
4
|
RefactorResult,
|
|
5
5
|
SemanticProvider,
|
|
6
6
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
7
|
-
import type { CodeAction } from "../config/types.ts";
|
|
8
7
|
import type { WorkspaceLspRuntime } from "../session/runtime-registry.ts";
|
|
9
8
|
import {
|
|
10
9
|
collectCodeActionResults,
|
|
11
10
|
isDeleteDeadCodeCodeAction,
|
|
12
|
-
isExtractFunctionCodeAction,
|
|
13
|
-
isExtractVariableCodeAction,
|
|
14
11
|
isUpdateImportsCodeAction,
|
|
15
12
|
runFilteredCodeActionRefactor,
|
|
16
13
|
runRenameRefactor,
|
|
@@ -28,7 +25,11 @@ export function createLspRefactorProvider(
|
|
|
28
25
|
const response = control
|
|
29
26
|
? await lsp.codeActions(file, position, control)
|
|
30
27
|
: await lsp.codeActions(file, position);
|
|
31
|
-
if (!response
|
|
28
|
+
if (!response) return [];
|
|
29
|
+
if (response.reason && !response.value) {
|
|
30
|
+
return [{ kind: "unavailable", reason: response.reason }];
|
|
31
|
+
}
|
|
32
|
+
if (!response.value) return [];
|
|
32
33
|
return collectCodeActionResults(response.value, {
|
|
33
34
|
getOpenDocumentVersion: (candidate) => lsp.getOpenDocumentVersion(candidate),
|
|
34
35
|
authorizedMutationRoots: response.authorizedMutationRoots,
|
|
@@ -58,21 +59,11 @@ async function planRefactor(
|
|
|
58
59
|
control,
|
|
59
60
|
});
|
|
60
61
|
case "extract_function":
|
|
61
|
-
return runExtractRefactor({
|
|
62
|
-
lsp,
|
|
63
|
-
request,
|
|
64
|
-
operation: "extract_function",
|
|
65
|
-
matches: isExtractFunctionCodeAction,
|
|
66
|
-
control,
|
|
67
|
-
});
|
|
68
62
|
case "extract_variable":
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
request
|
|
72
|
-
|
|
73
|
-
matches: isExtractVariableCodeAction,
|
|
74
|
-
control,
|
|
75
|
-
});
|
|
63
|
+
return {
|
|
64
|
+
kind: "unavailable",
|
|
65
|
+
reason: `Refactor operation "${request.operation}" is unavailable: standard LSP code actions do not provide a safe exact extraction-name contract.`,
|
|
66
|
+
};
|
|
76
67
|
case "update_imports":
|
|
77
68
|
return runFilteredCodeActionRefactor({
|
|
78
69
|
lsp,
|
|
@@ -103,28 +94,3 @@ async function planRefactor(
|
|
|
103
94
|
reason: `Refactor operation "${request.operation}" is not supported by the active semantic provider.`,
|
|
104
95
|
};
|
|
105
96
|
}
|
|
106
|
-
|
|
107
|
-
function runExtractRefactor(options: {
|
|
108
|
-
lsp: WorkspaceLspRuntime;
|
|
109
|
-
request: RefactorRequest;
|
|
110
|
-
operation: "extract_function" | "extract_variable";
|
|
111
|
-
matches: (action: CodeAction) => boolean;
|
|
112
|
-
control?: CodeRequestControl;
|
|
113
|
-
}): Promise<RefactorResult> | RefactorResult {
|
|
114
|
-
const { lsp, request, operation, matches, control } = options;
|
|
115
|
-
if (!request.range) {
|
|
116
|
-
return {
|
|
117
|
-
kind: "unavailable",
|
|
118
|
-
reason: `Refactor operation "${operation}" requires \`range\`.`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
return runFilteredCodeActionRefactor({
|
|
122
|
-
lsp,
|
|
123
|
-
file: request.file,
|
|
124
|
-
position: request.position,
|
|
125
|
-
range: request.range,
|
|
126
|
-
operation,
|
|
127
|
-
matches,
|
|
128
|
-
control,
|
|
129
|
-
});
|
|
130
|
-
}
|