@ian-pascoe/pi-lsp 0.1.0 → 0.1.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 +11 -3
- package/package.json +6 -6
- package/src/lsp-post-edit-diagnostics.ts +1 -0
- package/src/lsp-server-manager.ts +40 -5
- package/src/pi-lsp-extension.ts +13 -3
- package/src/pi-lsp-settings.ts +152 -70
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ Pi LSP reads only the `lsp` key from Pi's global `settings.json` and trusted pro
|
|
|
45
45
|
"languageId": "typescriptreact"
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
|
+
"requireRootMarker": true,
|
|
48
49
|
"rootMarkers": ["tsconfig.json", "package.json", ".git"],
|
|
49
50
|
"initializationOptions": {},
|
|
50
51
|
"settings": {},
|
|
@@ -58,14 +59,18 @@ Pi LSP reads only the `lsp` key from Pi's global `settings.json` and trusted pro
|
|
|
58
59
|
Every field is optional except an enabled server's non-empty `command` and `languages`. Each
|
|
59
60
|
language needs a non-empty `languageId` and at least one extension or exact filename. Extensions
|
|
60
61
|
include their leading period. `rootMarkers` are basename glob patterns; the nearest matching
|
|
61
|
-
ancestor becomes the server root and Pi's working directory is the fallback.
|
|
62
|
+
ancestor becomes the server root and Pi's working directory is the fallback. Set
|
|
63
|
+
`requireRootMarker` to `true` to exclude the server for files without any matching ancestor; it
|
|
64
|
+
defaults to `false`. A required empty `rootMarkers` list is invalid. Explicit requests naming an
|
|
65
|
+
otherwise compatible excluded server report that its required root marker was not found.
|
|
62
66
|
|
|
63
67
|
Global and project timeouts merge by field. A project server replaces the complete global server
|
|
64
68
|
with the same ID; set a project server to `null` to remove it. `initializationOptions` is sent only
|
|
65
69
|
during initialization. `settings` is used for `workspace/didChangeConfiguration` and
|
|
66
70
|
`workspace/configuration`. Environment strings override `process.env`; `null` removes a variable.
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
Invalid server definitions and timeout fields are quarantined individually and remain visible
|
|
72
|
+
through `status`; unrelated valid settings continue to work. An invalid project server replacement
|
|
73
|
+
still shadows the global definition. Untrusted project settings are ignored.
|
|
69
74
|
|
|
70
75
|
Pi's `/reload` reloads configuration. Servers start on first use, live for one Pi session, and stay
|
|
71
76
|
unavailable after a process or protocol failure until `restart` or `/reload`.
|
|
@@ -164,6 +169,9 @@ Changed, created, and renamed destination files are diagnosed; deleted files are
|
|
|
164
169
|
recognized result gets an explicit outcome, including `no diagnostics`, `no configured server`,
|
|
165
170
|
timeout, unavailable server, or an `apply_patch` adapter-version warning. Diagnostics preserve
|
|
166
171
|
duplicates from independent servers and never change the original tool's success or error state.
|
|
172
|
+
Only servers that advertise document diagnostics participate; formatting-only servers remain
|
|
173
|
+
available for explicit LSP formatting operations without appearing in Post-edit Diagnostics.
|
|
174
|
+
Files excluded by every matching server's Activation Gate are skipped silently.
|
|
167
175
|
|
|
168
176
|
Findings, matched-server failures, timeouts, and adapter warnings also appear in one expandable
|
|
169
177
|
Post-edit Diagnostics Entry after the current tool batch. Clean results and files without a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-lsp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Configured language-server tools and post-edit diagnostics for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -30,10 +30,6 @@
|
|
|
30
30
|
"access": "public",
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
35
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
36
|
-
},
|
|
37
33
|
"dependencies": {
|
|
38
34
|
"cross-spawn": "^7.0.6",
|
|
39
35
|
"vscode-languageserver-protocol": "^3.17.5"
|
|
@@ -53,5 +49,9 @@
|
|
|
53
49
|
"extensions": [
|
|
54
50
|
"./src/index.ts"
|
|
55
51
|
]
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
55
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|
|
@@ -273,6 +273,7 @@ export async function appendPostEditDiagnostics(
|
|
|
273
273
|
})),
|
|
274
274
|
...(await diagnostics.runPostEditDiagnostics(extracted.paths)),
|
|
275
275
|
];
|
|
276
|
+
if (outcomes.length === 0) return undefined;
|
|
276
277
|
const patch: PostEditDiagnosticsResultPatch = {
|
|
277
278
|
content: [...event.content, { type: "text", text: formatPostEditDiagnostics(outcomes) }],
|
|
278
279
|
details: event.details,
|
|
@@ -18,6 +18,8 @@ export interface LspServerRoutingDefinition {
|
|
|
18
18
|
readonly serverId: string;
|
|
19
19
|
/** Languages and file patterns accepted by this server. */
|
|
20
20
|
readonly languages: readonly LspServerLanguage[];
|
|
21
|
+
/** Exclude this server when none of its root markers exists above the requested file. */
|
|
22
|
+
readonly requireRootMarker?: boolean;
|
|
21
23
|
/** Basename glob patterns that select this server instance's nearest workspace root. */
|
|
22
24
|
readonly rootMarkers?: readonly string[];
|
|
23
25
|
}
|
|
@@ -71,6 +73,7 @@ export type LspServerFailureCode =
|
|
|
71
73
|
| "no-capable-server"
|
|
72
74
|
| "no-matching-server"
|
|
73
75
|
| "request-failed"
|
|
76
|
+
| "root-marker-not-found"
|
|
74
77
|
| "server-unavailable";
|
|
75
78
|
|
|
76
79
|
/** Preserves one matching server's failure without discarding sibling successes. */
|
|
@@ -167,8 +170,11 @@ function findNearestLspRoot(
|
|
|
167
170
|
rootMarkers: readonly string[] | undefined,
|
|
168
171
|
ancestorDirectories: readonly LspAncestorDirectory[],
|
|
169
172
|
cwd: string,
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
requireRootMarker: boolean,
|
|
174
|
+
): string | undefined {
|
|
175
|
+
if (rootMarkers === undefined || rootMarkers.length === 0) {
|
|
176
|
+
return requireRootMarker ? undefined : resolve(cwd);
|
|
177
|
+
}
|
|
172
178
|
for (const directory of ancestorDirectories) {
|
|
173
179
|
if (
|
|
174
180
|
directory.entryNames.some((entryName) =>
|
|
@@ -178,7 +184,7 @@ function findNearestLspRoot(
|
|
|
178
184
|
return directory.path;
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
|
-
return resolve(cwd);
|
|
187
|
+
return requireRootMarker ? undefined : resolve(cwd);
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
/** Route one file to every matching configured server in stable settings-map order. */
|
|
@@ -196,10 +202,17 @@ export function routeLspServersForFile(
|
|
|
196
202
|
languageMatchesFile(candidate, normalizedFilePath),
|
|
197
203
|
);
|
|
198
204
|
if (language === undefined) continue;
|
|
205
|
+
const rootPath = findNearestLspRoot(
|
|
206
|
+
serverDefinition.rootMarkers,
|
|
207
|
+
ancestorDirectories,
|
|
208
|
+
cwd,
|
|
209
|
+
serverDefinition.requireRootMarker ?? false,
|
|
210
|
+
);
|
|
211
|
+
if (rootPath === undefined) continue;
|
|
199
212
|
routes.push({
|
|
200
213
|
serverId: serverDefinition.serverId,
|
|
201
214
|
language,
|
|
202
|
-
rootPath
|
|
215
|
+
rootPath,
|
|
203
216
|
});
|
|
204
217
|
}
|
|
205
218
|
|
|
@@ -288,12 +301,21 @@ export class LspServerManager<TClient extends LspManagedServerClient = LspManage
|
|
|
288
301
|
const ancestors = await readLspAncestorDirectories(absolutePath);
|
|
289
302
|
const definitions = [...this.input.settings.servers.values()].map((definition) => ({
|
|
290
303
|
languages: definition.languages,
|
|
304
|
+
requireRootMarker: definition.requireRootMarker,
|
|
291
305
|
rootMarkers: definition.rootMarkers,
|
|
292
306
|
serverId: definition.id,
|
|
293
307
|
}));
|
|
294
308
|
return routeLspServersForFile(definitions, absolutePath, this.input.cwd, ancestors);
|
|
295
309
|
}
|
|
296
310
|
|
|
311
|
+
/** Report whether any Server Definition accepts the file language before activation gating. */
|
|
312
|
+
hasConfiguredLanguageServerForFile(filePath: string): boolean {
|
|
313
|
+
const absolutePath = resolve(this.input.cwd, normalizeLspFilePath(filePath));
|
|
314
|
+
return [...this.input.settings.servers.values()].some((definition) =>
|
|
315
|
+
definition.languages.some((language) => languageMatchesFile(language, absolutePath)),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
297
319
|
/** Query every matching capable instance while retaining independent successes and failures. */
|
|
298
320
|
async runRead<T>(
|
|
299
321
|
filePath: string,
|
|
@@ -446,10 +468,23 @@ export class LspServerManager<TClient extends LspManagedServerClient = LspManage
|
|
|
446
468
|
}
|
|
447
469
|
|
|
448
470
|
private noMatchingFailure(serverId: string | undefined, filePath: string): LspServerFailure {
|
|
471
|
+
const normalizedFilePath = normalizeLspFilePath(filePath);
|
|
472
|
+
const definition =
|
|
473
|
+
serverId === undefined ? undefined : this.input.settings.servers.get(serverId);
|
|
474
|
+
if (
|
|
475
|
+
definition?.requireRootMarker === true &&
|
|
476
|
+
definition.languages.some((language) => languageMatchesFile(language, normalizedFilePath))
|
|
477
|
+
) {
|
|
478
|
+
return {
|
|
479
|
+
code: "root-marker-not-found",
|
|
480
|
+
message: `Pi LSP: required root marker not found for server ${definition.id} and ${normalizedFilePath}; expected one of: ${definition.rootMarkers.join(", ")}`,
|
|
481
|
+
serverId: definition.id,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
449
484
|
const requestedServer = serverId === undefined ? "any configured server" : `server ${serverId}`;
|
|
450
485
|
return {
|
|
451
486
|
code: "no-matching-server",
|
|
452
|
-
message: `Pi LSP: ${requestedServer} does not match ${
|
|
487
|
+
message: `Pi LSP: ${requestedServer} does not match ${normalizedFilePath}`,
|
|
453
488
|
serverId: serverId ?? "*",
|
|
454
489
|
};
|
|
455
490
|
}
|
package/src/pi-lsp-extension.ts
CHANGED
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { type Static, Type } from "typebox";
|
|
16
16
|
import { Value } from "typebox/value";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DocumentDiagnosticRequest,
|
|
19
|
+
PositionEncodingKind,
|
|
20
|
+
type Diagnostic,
|
|
21
|
+
} from "vscode-languageserver-protocol/node";
|
|
18
22
|
import {
|
|
19
23
|
appendPiPostEditDiagnostics,
|
|
20
24
|
type PostEditDiagnosticOutcome,
|
|
@@ -154,7 +158,7 @@ class ManagerPostEditDiagnosticsRunner implements PostEditDiagnosticsRunner {
|
|
|
154
158
|
const result = await this.session.manager.runRead(
|
|
155
159
|
filePath,
|
|
156
160
|
undefined,
|
|
157
|
-
() =>
|
|
161
|
+
(client) => client.hasCapability(DocumentDiagnosticRequest.method),
|
|
158
162
|
async (client, route): Promise<readonly PostEditDiagnosticOutcome[]> => {
|
|
159
163
|
const diagnostics = await client.documentDiagnostics(
|
|
160
164
|
filePath,
|
|
@@ -185,7 +189,13 @@ class ManagerPostEditDiagnosticsRunner implements PostEditDiagnosticsRunner {
|
|
|
185
189
|
const successfulOutcomes = result.successes.flatMap(({ value }) => value);
|
|
186
190
|
outcomes.push(...successfulOutcomes);
|
|
187
191
|
outcomes.push(
|
|
188
|
-
...result.failures.
|
|
192
|
+
...result.failures.flatMap((failure) =>
|
|
193
|
+
failure.code === "no-capable-server" ||
|
|
194
|
+
(failure.code === "no-matching-server" &&
|
|
195
|
+
this.session.manager.hasConfiguredLanguageServerForFile(filePath))
|
|
196
|
+
? []
|
|
197
|
+
: [failureDiagnosticOutcome(filePath, failure)],
|
|
198
|
+
),
|
|
189
199
|
);
|
|
190
200
|
}
|
|
191
201
|
return outcomes;
|
package/src/pi-lsp-settings.ts
CHANGED
|
@@ -12,6 +12,7 @@ const DEFAULT_LSP_TIMEOUTS = {
|
|
|
12
12
|
const NonEmptyStringSchema = Type.String({ minLength: 1 });
|
|
13
13
|
const PositiveMillisecondsSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
14
14
|
const JsonValueSchema = Type.Any();
|
|
15
|
+
const JsonObjectSchema = Type.Record(Type.String(), JsonValueSchema);
|
|
15
16
|
const LspLanguageMappingSchema = Type.Object(
|
|
16
17
|
{
|
|
17
18
|
extensions: Type.Optional(Type.Array(NonEmptyStringSchema, { minItems: 1 })),
|
|
@@ -29,6 +30,7 @@ const LspServerDefinitionSchema = Type.Object(
|
|
|
29
30
|
),
|
|
30
31
|
initializationOptions: Type.Optional(JsonValueSchema),
|
|
31
32
|
languages: Type.Optional(Type.Array(LspLanguageMappingSchema, { minItems: 1 })),
|
|
33
|
+
requireRootMarker: Type.Optional(Type.Boolean()),
|
|
32
34
|
rootMarkers: Type.Optional(Type.Array(NonEmptyStringSchema)),
|
|
33
35
|
settings: Type.Optional(JsonValueSchema),
|
|
34
36
|
},
|
|
@@ -43,18 +45,6 @@ const LspTimeoutsSchema = Type.Object(
|
|
|
43
45
|
},
|
|
44
46
|
{ additionalProperties: false },
|
|
45
47
|
);
|
|
46
|
-
const LspLayerSchema = Type.Object(
|
|
47
|
-
{
|
|
48
|
-
servers: Type.Optional(
|
|
49
|
-
Type.Record(
|
|
50
|
-
Type.String({ minLength: 1 }),
|
|
51
|
-
Type.Union([LspServerDefinitionSchema, Type.Null()]),
|
|
52
|
-
),
|
|
53
|
-
),
|
|
54
|
-
timeouts: Type.Optional(LspTimeoutsSchema),
|
|
55
|
-
},
|
|
56
|
-
{ additionalProperties: false },
|
|
57
|
-
);
|
|
58
48
|
const SettingsDocumentSchema = Type.Object({ lsp: Type.Optional(JsonValueSchema) });
|
|
59
49
|
|
|
60
50
|
interface JsonObject {
|
|
@@ -64,7 +54,16 @@ interface JsonObject {
|
|
|
64
54
|
type JsonValue = null | boolean | number | string | readonly JsonValue[] | JsonObject;
|
|
65
55
|
type LspServerDefinitionWire = Static<typeof LspServerDefinitionSchema>;
|
|
66
56
|
type LspTimeoutsWire = Static<typeof LspTimeoutsSchema>;
|
|
67
|
-
type
|
|
57
|
+
type LspTimeoutName = keyof typeof DEFAULT_LSP_TIMEOUTS;
|
|
58
|
+
const LSP_TIMEOUT_NAMES: readonly LspTimeoutName[] = [
|
|
59
|
+
"diagnosticsMs",
|
|
60
|
+
"initializeMs",
|
|
61
|
+
"requestMs",
|
|
62
|
+
"shutdownMs",
|
|
63
|
+
];
|
|
64
|
+
type ParsedLspServerDefinition =
|
|
65
|
+
| { readonly kind: "excluded" }
|
|
66
|
+
| { readonly kind: "valid"; readonly value: LspServerDefinitionWire };
|
|
68
67
|
|
|
69
68
|
/** Describes one configured filename or extension mapping to an LSP language identifier. */
|
|
70
69
|
export interface LspLanguageMapping {
|
|
@@ -73,7 +72,7 @@ export interface LspLanguageMapping {
|
|
|
73
72
|
readonly languageId: string;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
/** Contains the parsed command and protocol values for one
|
|
75
|
+
/** Contains the parsed command and protocol values for one configured language server. */
|
|
77
76
|
export interface LspServerDefinition {
|
|
78
77
|
readonly args: readonly string[];
|
|
79
78
|
readonly command: string;
|
|
@@ -81,6 +80,8 @@ export interface LspServerDefinition {
|
|
|
81
80
|
readonly id: string;
|
|
82
81
|
readonly initializationOptions?: JsonValue;
|
|
83
82
|
readonly languages: readonly LspLanguageMapping[];
|
|
83
|
+
/** Require any root marker above a candidate file; false falls back to Pi's working directory. */
|
|
84
|
+
readonly requireRootMarker: boolean;
|
|
84
85
|
readonly rootMarkers: readonly string[];
|
|
85
86
|
readonly settings?: JsonValue;
|
|
86
87
|
}
|
|
@@ -93,7 +94,7 @@ export interface LspTimeouts {
|
|
|
93
94
|
readonly shutdownMs: number;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
/** Reports resolved trusted configuration,
|
|
97
|
+
/** Reports resolved trusted configuration, retaining valid entries when other settings are invalid. */
|
|
97
98
|
export interface ResolvedLspSettings {
|
|
98
99
|
readonly enabled: boolean;
|
|
99
100
|
readonly servers: ReadonlyMap<string, LspServerDefinition>;
|
|
@@ -113,58 +114,150 @@ type PiSettingsDocument = ReturnType<SettingsManager["getGlobalSettings"]>;
|
|
|
113
114
|
/** Pi's core Settings type or a test/boundary document carrying the extension-owned `lsp` value. */
|
|
114
115
|
export type LspSettingsDocumentInput = PiSettingsDocument | { readonly lsp?: JsonValue };
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
interface ParsedLspLayer {
|
|
118
|
+
readonly servers: ReadonlyMap<string, ParsedLspServerDefinition>;
|
|
119
|
+
readonly timeouts: LspTimeoutsWire;
|
|
120
|
+
readonly warnings: readonly string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isJsonObject(value: JsonValue): value is JsonObject {
|
|
124
|
+
return Value.Check(JsonObjectSchema, value);
|
|
125
|
+
}
|
|
120
126
|
|
|
121
|
-
function
|
|
122
|
-
|
|
127
|
+
function schemaValidationWarning(
|
|
128
|
+
schema: typeof LspServerDefinitionSchema | typeof PositiveMillisecondsSchema,
|
|
129
|
+
value: JsonValue,
|
|
130
|
+
prefix: string,
|
|
131
|
+
): string {
|
|
132
|
+
const error = Value.Errors(schema, value)[0];
|
|
123
133
|
const path = error?.instancePath.replaceAll("/", ".") ?? "";
|
|
124
134
|
const unknownField =
|
|
125
135
|
error?.keyword === "additionalProperties" ? error.params.additionalProperties[0] : undefined;
|
|
126
|
-
|
|
136
|
+
const unknownFieldSuffix = unknownField === undefined ? "" : `.${String(unknownField)}`;
|
|
137
|
+
return `${prefix}${path}${unknownFieldSuffix}: ${error?.message ?? "invalid settings"}`;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
function
|
|
130
|
-
|
|
140
|
+
function parseLspServerDefinitions(
|
|
141
|
+
value: JsonValue | undefined,
|
|
131
142
|
scope: "global" | "project",
|
|
132
|
-
): ParsedLspLayer {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
return { kind: "invalid", warning: lspValidationWarning(settings.lsp, scope) };
|
|
143
|
+
): Pick<ParsedLspLayer, "servers" | "warnings"> {
|
|
144
|
+
const warnings: string[] = [];
|
|
145
|
+
const servers = new Map<string, ParsedLspServerDefinition>();
|
|
146
|
+
if (value === undefined) return { servers, warnings };
|
|
147
|
+
if (!isJsonObject(value)) {
|
|
148
|
+
return { servers, warnings: [`${scope} lsp.servers: expected a JSON object`] };
|
|
139
149
|
}
|
|
140
|
-
for (const [
|
|
141
|
-
if (
|
|
150
|
+
for (const [id, server] of Object.entries(value)) {
|
|
151
|
+
if (!Value.Check(NonEmptyStringSchema, id)) {
|
|
152
|
+
warnings.push(`${scope} lsp.servers.${String(id)}: server ID must be a non-empty string`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (server === null) {
|
|
156
|
+
servers.set(id, { kind: "excluded" });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!Value.Check(LspServerDefinitionSchema, server)) {
|
|
160
|
+
warnings.push(
|
|
161
|
+
schemaValidationWarning(LspServerDefinitionSchema, server, `${scope} lsp.servers.${id}`),
|
|
162
|
+
);
|
|
163
|
+
servers.set(id, { kind: "excluded" });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
142
166
|
if (server.command === undefined || server.languages === undefined) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
};
|
|
167
|
+
warnings.push(`${scope} lsp.servers.${id}: command and languages are required`);
|
|
168
|
+
servers.set(id, { kind: "excluded" });
|
|
169
|
+
continue;
|
|
147
170
|
}
|
|
148
171
|
if (
|
|
149
172
|
server.languages.some(
|
|
150
173
|
(language) => language.extensions === undefined && language.fileNames === undefined,
|
|
151
174
|
)
|
|
152
175
|
) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
176
|
+
warnings.push(
|
|
177
|
+
`${scope} lsp.servers.${id}.languages: each language needs extensions or fileNames`,
|
|
178
|
+
);
|
|
179
|
+
servers.set(id, { kind: "excluded" });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (server.requireRootMarker === true && (server.rootMarkers?.length ?? 0) === 0) {
|
|
183
|
+
warnings.push(
|
|
184
|
+
`${scope} lsp.servers.${id}.rootMarkers: at least one root marker is required when requireRootMarker is true`,
|
|
185
|
+
);
|
|
186
|
+
servers.set(id, { kind: "excluded" });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
servers.set(id, { kind: "valid", value: server });
|
|
190
|
+
}
|
|
191
|
+
return { servers, warnings };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isLspTimeoutName(name: string): name is LspTimeoutName {
|
|
195
|
+
return LSP_TIMEOUT_NAMES.some((timeoutName) => timeoutName === name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseLspTimeouts(
|
|
199
|
+
value: JsonValue | undefined,
|
|
200
|
+
scope: "global" | "project",
|
|
201
|
+
): Pick<ParsedLspLayer, "timeouts" | "warnings"> {
|
|
202
|
+
const warnings: string[] = [];
|
|
203
|
+
const timeouts: LspTimeoutsWire = {};
|
|
204
|
+
if (value === undefined) return { timeouts, warnings };
|
|
205
|
+
if (!isJsonObject(value)) {
|
|
206
|
+
return { timeouts, warnings: [`${scope} lsp.timeouts: expected a JSON object`] };
|
|
207
|
+
}
|
|
208
|
+
for (const [name, timeout] of Object.entries(value)) {
|
|
209
|
+
if (!isLspTimeoutName(name)) {
|
|
210
|
+
warnings.push(`${scope} lsp.timeouts.${name}: unknown field`);
|
|
211
|
+
continue;
|
|
157
212
|
}
|
|
213
|
+
if (!Value.Check(PositiveMillisecondsSchema, timeout)) {
|
|
214
|
+
warnings.push(
|
|
215
|
+
schemaValidationWarning(
|
|
216
|
+
PositiveMillisecondsSchema,
|
|
217
|
+
timeout,
|
|
218
|
+
`${scope} lsp.timeouts.${name}`,
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
timeouts[name] = timeout;
|
|
158
224
|
}
|
|
159
|
-
return {
|
|
225
|
+
return { timeouts, warnings };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readLspLayer(
|
|
229
|
+
settings: LspSettingsDocumentInput,
|
|
230
|
+
scope: "global" | "project",
|
|
231
|
+
): ParsedLspLayer {
|
|
232
|
+
if (!Value.Check(SettingsDocumentSchema, settings)) {
|
|
233
|
+
return {
|
|
234
|
+
servers: new Map(),
|
|
235
|
+
timeouts: {},
|
|
236
|
+
warnings: [`${scope} settings: expected a JSON object`],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (settings.lsp === undefined) return { servers: new Map(), timeouts: {}, warnings: [] };
|
|
240
|
+
if (!isJsonObject(settings.lsp)) {
|
|
241
|
+
return {
|
|
242
|
+
servers: new Map(),
|
|
243
|
+
timeouts: {},
|
|
244
|
+
warnings: [`${scope} lsp: expected a JSON object`],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const warnings = Object.keys(settings.lsp)
|
|
248
|
+
.filter((field) => field !== "servers" && field !== "timeouts")
|
|
249
|
+
.map((field) => `${scope} lsp.${field}: unknown field`);
|
|
250
|
+
const parsedServers = parseLspServerDefinitions(settings.lsp.servers, scope);
|
|
251
|
+
const parsedTimeouts = parseLspTimeouts(settings.lsp.timeouts, scope);
|
|
252
|
+
return {
|
|
253
|
+
servers: parsedServers.servers,
|
|
254
|
+
timeouts: parsedTimeouts.timeouts,
|
|
255
|
+
warnings: [...warnings, ...parsedServers.warnings, ...parsedTimeouts.warnings],
|
|
256
|
+
};
|
|
160
257
|
}
|
|
161
258
|
|
|
162
259
|
function mergeLspTimeouts(globalLayer: ParsedLspLayer, projectLayer: ParsedLspLayer): LspTimeouts {
|
|
163
|
-
|
|
164
|
-
globalLayer.kind === "valid" ? globalLayer.value.timeouts : undefined,
|
|
165
|
-
projectLayer.kind === "valid" ? projectLayer.value.timeouts : undefined,
|
|
166
|
-
];
|
|
167
|
-
return Object.assign({}, DEFAULT_LSP_TIMEOUTS, ...timeoutValues);
|
|
260
|
+
return Object.assign({}, DEFAULT_LSP_TIMEOUTS, globalLayer.timeouts, projectLayer.timeouts);
|
|
168
261
|
}
|
|
169
262
|
|
|
170
263
|
function resolveLspEnvironment(
|
|
@@ -199,6 +292,7 @@ function resolveLspServerDefinition(
|
|
|
199
292
|
environment: resolveLspEnvironment(server.environment),
|
|
200
293
|
id,
|
|
201
294
|
languages,
|
|
295
|
+
requireRootMarker: server.requireRootMarker ?? false,
|
|
202
296
|
rootMarkers: server.rootMarkers ?? [],
|
|
203
297
|
};
|
|
204
298
|
const initializationOptions = server.initializationOptions;
|
|
@@ -224,18 +318,13 @@ function mergeLspServers(
|
|
|
224
318
|
projectLayer: ParsedLspLayer,
|
|
225
319
|
): ReadonlyMap<string, LspServerDefinition> {
|
|
226
320
|
const serverDefinitions = new Map<string, LspServerDefinitionWire>();
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
serverDefinitions.set(id, server);
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
addServers(globalLayer, false);
|
|
238
|
-
addServers(projectLayer, true);
|
|
321
|
+
for (const [id, server] of globalLayer.servers) {
|
|
322
|
+
if (server.kind === "valid") serverDefinitions.set(id, server.value);
|
|
323
|
+
}
|
|
324
|
+
for (const [id, server] of projectLayer.servers) {
|
|
325
|
+
serverDefinitions.delete(id);
|
|
326
|
+
if (server.kind === "valid") serverDefinitions.set(id, server.value);
|
|
327
|
+
}
|
|
239
328
|
return new Map(
|
|
240
329
|
[...serverDefinitions]
|
|
241
330
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
@@ -243,21 +332,14 @@ function mergeLspServers(
|
|
|
243
332
|
);
|
|
244
333
|
}
|
|
245
334
|
|
|
246
|
-
/** Resolve global and trusted-project LSP settings
|
|
335
|
+
/** Resolve global and trusted-project LSP settings, quarantining invalid entries instead of disabling valid settings. */
|
|
247
336
|
export function resolveLspSettings(reader: LspSettingsReader): ResolvedLspSettings {
|
|
248
337
|
const globalLayer = readLspLayer(reader.getGlobalSettings(), "global");
|
|
249
338
|
const projectLayer = readLspLayer(reader.getProjectSettings(), "project");
|
|
250
|
-
const warnings = [globalLayer, projectLayer]
|
|
251
|
-
.filter(
|
|
252
|
-
(layer): layer is Extract<ParsedLspLayer, { readonly kind: "invalid" }> =>
|
|
253
|
-
layer.kind === "invalid",
|
|
254
|
-
)
|
|
255
|
-
.map((layer) => layer.warning);
|
|
256
|
-
const enabled = warnings.length === 0;
|
|
257
339
|
return {
|
|
258
|
-
enabled,
|
|
259
|
-
servers:
|
|
340
|
+
enabled: true,
|
|
341
|
+
servers: mergeLspServers(globalLayer, projectLayer),
|
|
260
342
|
timeouts: mergeLspTimeouts(globalLayer, projectLayer),
|
|
261
|
-
warnings,
|
|
343
|
+
warnings: [...globalLayer.warnings, ...projectLayer.warnings],
|
|
262
344
|
};
|
|
263
345
|
}
|