@prisma-next/language-server 0.14.0-dev.46 → 0.14.0-dev.48
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 +8 -8
- package/dist/exports/index.d.mts +3 -3
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +211 -133
- package/dist/exports/index.mjs.map +1 -1
- package/package.json +10 -9
- package/src/completion-provider.ts +4 -12
- package/src/project-artifacts.ts +86 -39
- package/src/schema-inputs.ts +3 -0
- package/src/semantic-tokens.ts +18 -20
- package/src/server.ts +268 -126
package/src/server.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
1
|
import { fileURLToPath } from 'node:url';
|
|
3
2
|
import { findNearestConfigPathForFile } from '@prisma-next/config-loader';
|
|
4
3
|
import type { SymbolTable } from '@prisma-next/psl-parser';
|
|
5
4
|
import { type FormatOptions, format } from '@prisma-next/psl-parser/format';
|
|
5
|
+
import { join } from 'pathe';
|
|
6
6
|
import {
|
|
7
7
|
type CompletionItem,
|
|
8
8
|
type Connection,
|
|
9
9
|
type Diagnostic,
|
|
10
10
|
DiagnosticSeverity,
|
|
11
11
|
DidChangeWatchedFilesNotification,
|
|
12
|
+
type DocumentDiagnosticReport,
|
|
13
|
+
DocumentDiagnosticReportKind,
|
|
12
14
|
type FoldingRange,
|
|
15
|
+
type FullDocumentDiagnosticReport,
|
|
13
16
|
type InitializeParams,
|
|
14
17
|
type InitializeResult,
|
|
15
18
|
type Position,
|
|
@@ -25,12 +28,12 @@ import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
|
25
28
|
import { classifyPslCompletionContext } from './completion-context';
|
|
26
29
|
import { providePslCompletionItems } from './completion-provider';
|
|
27
30
|
import { CONFIG_FILENAME, resolveConfigInputs } from './config-resolution';
|
|
28
|
-
import { ParseDiagnosticSeverity } from './diagnostic-mapping';
|
|
31
|
+
import { type LspDiagnostic, ParseDiagnosticSeverity } from './diagnostic-mapping';
|
|
29
32
|
import { computeFoldingRanges } from './folding-ranges';
|
|
30
33
|
import type { PipelineInputs } from './pipeline';
|
|
31
34
|
import {
|
|
32
|
-
type CachedDocument,
|
|
33
35
|
createProjectArtifacts,
|
|
36
|
+
type DocumentArtifacts,
|
|
34
37
|
type ProjectArtifacts,
|
|
35
38
|
} from './project-artifacts';
|
|
36
39
|
import type { SchemaInputSet } from './schema-inputs';
|
|
@@ -42,7 +45,7 @@ export interface LanguageServer {
|
|
|
42
45
|
* Exposed for future features (completion, semantic tokens); nothing consumes
|
|
43
46
|
* them yet.
|
|
44
47
|
*/
|
|
45
|
-
getDocumentAst(uri: string):
|
|
48
|
+
getDocumentAst(uri: string): DocumentArtifacts | undefined;
|
|
46
49
|
getProjectSymbolTable(uri: string): SymbolTable | undefined;
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -58,17 +61,32 @@ interface ProjectState {
|
|
|
58
61
|
readonly artifacts: ProjectArtifacts;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/**
|
|
65
|
+
* One entry per managed config: either the load in flight or the loaded
|
|
66
|
+
* project — never both, never a settled load without an entry decision.
|
|
67
|
+
*/
|
|
68
|
+
type ManagedProject =
|
|
69
|
+
| {
|
|
70
|
+
readonly status: 'loading';
|
|
71
|
+
readonly load: Promise<ProjectState>;
|
|
72
|
+
/**
|
|
73
|
+
* Whether a loaded project existed when this load (chain) began — a
|
|
74
|
+
* failed reload must still clear the markers push clients were shown,
|
|
75
|
+
* while a failed first load must not publish anything.
|
|
76
|
+
*/
|
|
77
|
+
readonly hadLoadedProject: boolean;
|
|
78
|
+
}
|
|
79
|
+
| { readonly status: 'loaded'; readonly project: ProjectState };
|
|
80
|
+
|
|
61
81
|
const semanticTokenSourceLimit = 100_000;
|
|
62
82
|
|
|
63
83
|
export function createServer(connection: Connection): LanguageServer {
|
|
64
84
|
const documents = new TextDocuments(TextDocument);
|
|
65
|
-
const
|
|
66
|
-
const projectLoads = new Map<string, Promise<ProjectState>>();
|
|
85
|
+
const managedProjects = new Map<string, ManagedProject>();
|
|
67
86
|
const documentConfigPaths = new Map<string, string>();
|
|
68
87
|
let rootPath = process.cwd();
|
|
69
88
|
let watchedConfigGlob = join(rootPath, '**', CONFIG_FILENAME);
|
|
70
|
-
let
|
|
71
|
-
let clientSupportsSnippets = false;
|
|
89
|
+
let clientCapabilities = noClientCapabilities;
|
|
72
90
|
let disposed = false;
|
|
73
91
|
|
|
74
92
|
function sendDiagnostics(params: PublishDiagnosticsParams): void {
|
|
@@ -95,27 +113,43 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
95
113
|
documentConfigPaths.delete(uri);
|
|
96
114
|
return;
|
|
97
115
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
document.getText(),
|
|
101
|
-
project.inputs,
|
|
102
|
-
project.controlStack,
|
|
103
|
-
);
|
|
104
|
-
if (computed === null) {
|
|
116
|
+
const artifacts = project.artifacts.document(uri);
|
|
117
|
+
if (artifacts === undefined) {
|
|
105
118
|
sendDiagnostics({ uri, diagnostics: [] });
|
|
106
119
|
return;
|
|
107
120
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
sendDiagnostics({ uri, diagnostics: toDiagnostics(artifacts.diagnostics) });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Project-scoped so a future multi-input symbol table can attach
|
|
126
|
+
* `relatedDocuments` for cross-file effects.
|
|
127
|
+
*/
|
|
128
|
+
function buildDocumentDiagnosticReport(
|
|
129
|
+
project: ProjectState,
|
|
130
|
+
uri: string,
|
|
131
|
+
): FullDocumentDiagnosticReport {
|
|
132
|
+
const artifacts = project.artifacts.document(uri);
|
|
133
|
+
return {
|
|
134
|
+
kind: DocumentDiagnosticReportKind.Full,
|
|
135
|
+
items: artifacts === undefined ? [] : toDiagnostics(artifacts.diagnostics),
|
|
136
|
+
};
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
async function resolveProjectForDocument(uri: string): Promise<ProjectState | undefined> {
|
|
140
|
+
const project = await projectForNearestConfig(uri);
|
|
141
|
+
if (project === undefined || project.inputs.includes(uri)) {
|
|
142
|
+
return project;
|
|
143
|
+
}
|
|
144
|
+
// Only the config's declared inputs are managed: a stray document beside
|
|
145
|
+
// a config keeps no association, so reads and events never reach it — and
|
|
146
|
+
// a project it alone caused to load is dropped again.
|
|
147
|
+
documentConfigPaths.delete(uri);
|
|
148
|
+
dropProjectWithoutManagedDocuments(project.configPath);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function projectForNearestConfig(uri: string): Promise<ProjectState | undefined> {
|
|
119
153
|
const knownConfigPath = documentConfigPaths.get(uri);
|
|
120
154
|
if (knownConfigPath !== undefined) {
|
|
121
155
|
const project = await resolveProjectIfLoadable(knownConfigPath);
|
|
@@ -130,7 +164,13 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
130
164
|
return undefined;
|
|
131
165
|
}
|
|
132
166
|
|
|
133
|
-
|
|
167
|
+
let configPath: string | undefined;
|
|
168
|
+
try {
|
|
169
|
+
configPath = await findNearestConfigPathForFile(filePath);
|
|
170
|
+
} catch {
|
|
171
|
+
// Config discovery walks the filesystem; a failure means "no project".
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
134
174
|
if (configPath === undefined) {
|
|
135
175
|
return undefined;
|
|
136
176
|
}
|
|
@@ -153,40 +193,62 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
async function resolveProject(configPath: string): Promise<ProjectState> {
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
158
|
-
return
|
|
196
|
+
const entry = managedProjects.get(configPath);
|
|
197
|
+
if (entry === undefined) {
|
|
198
|
+
return startProjectLoad(configPath);
|
|
159
199
|
}
|
|
160
|
-
|
|
161
|
-
if (existingLoad !== undefined) {
|
|
162
|
-
return existingLoad;
|
|
163
|
-
}
|
|
164
|
-
return queueProjectLoad(configPath);
|
|
200
|
+
return entry.status === 'loaded' ? entry.project : entry.load;
|
|
165
201
|
}
|
|
166
202
|
|
|
167
203
|
function refreshProject(configPath: string): Promise<ProjectState> {
|
|
168
|
-
return
|
|
204
|
+
return startProjectLoad(configPath);
|
|
169
205
|
}
|
|
170
206
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
// A load replaces the entry with `loading` immediately, so reads during a
|
|
208
|
+
// config reload await the fresh resolution instead of the pre-reload
|
|
209
|
+
// project. A failed load leaves its entry in place — every awaiter funnels
|
|
210
|
+
// the failure into `stopManagingProject`, which needs the entry to decide
|
|
211
|
+
// whether push clears are owed.
|
|
212
|
+
function startProjectLoad(configPath: string): Promise<ProjectState> {
|
|
213
|
+
const existing = managedProjects.get(configPath);
|
|
214
|
+
const previousLoad = existing?.status === 'loading' ? existing.load : undefined;
|
|
215
|
+
const hadLoadedProject =
|
|
216
|
+
existing?.status === 'loaded' ||
|
|
217
|
+
(existing?.status === 'loading' && existing.hadLoadedProject);
|
|
218
|
+
const load: Promise<ProjectState> = (previousLoad ?? Promise.resolve(undefined))
|
|
174
219
|
.catch(() => undefined)
|
|
175
220
|
.then(() => loadProject(configPath))
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
221
|
+
.then((project) => {
|
|
222
|
+
// A load that outlives the last association must not keep a project
|
|
223
|
+
// entry alive.
|
|
224
|
+
if (isCurrentLoad(configPath, load)) {
|
|
225
|
+
if (hasManagedDocuments(configPath)) {
|
|
226
|
+
managedProjects.set(configPath, { status: 'loaded', project });
|
|
227
|
+
} else {
|
|
228
|
+
managedProjects.delete(configPath);
|
|
229
|
+
}
|
|
179
230
|
}
|
|
231
|
+
return project;
|
|
180
232
|
});
|
|
181
|
-
|
|
233
|
+
managedProjects.set(configPath, { status: 'loading', load, hadLoadedProject });
|
|
182
234
|
return load;
|
|
183
235
|
}
|
|
184
236
|
|
|
237
|
+
function isCurrentLoad(configPath: string, load: Promise<ProjectState>): boolean {
|
|
238
|
+
const entry = managedProjects.get(configPath);
|
|
239
|
+
return entry?.status === 'loading' && entry.load === load;
|
|
240
|
+
}
|
|
241
|
+
|
|
185
242
|
async function loadProject(configPath: string): Promise<ProjectState> {
|
|
186
243
|
const resolution = await resolveConfigInputs(configPath);
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
244
|
+
// A fresh store per load: a config reload can change what a parse
|
|
245
|
+
// produces (inputs, control stack), so later reads must derive from the
|
|
246
|
+
// new resolution rather than anything computed under the old one.
|
|
247
|
+
const artifacts = createProjectArtifacts({
|
|
248
|
+
inputs: resolution.inputs,
|
|
249
|
+
controlStack: resolution.controlStack,
|
|
250
|
+
getText: (uri) => documents.get(uri)?.getText(),
|
|
251
|
+
});
|
|
190
252
|
const project: ProjectState =
|
|
191
253
|
resolution.formatter === undefined
|
|
192
254
|
? {
|
|
@@ -202,16 +264,18 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
202
264
|
controlStack: resolution.controlStack,
|
|
203
265
|
artifacts,
|
|
204
266
|
};
|
|
205
|
-
projects.set(configPath, project);
|
|
206
267
|
return project;
|
|
207
268
|
}
|
|
208
269
|
|
|
209
270
|
function stopManagingProject(configPath: string): void {
|
|
210
|
-
const
|
|
271
|
+
const entry = managedProjects.get(configPath);
|
|
272
|
+
const hadProject =
|
|
273
|
+
entry?.status === 'loaded' || (entry?.status === 'loading' && entry.hadLoadedProject);
|
|
274
|
+
managedProjects.delete(configPath);
|
|
211
275
|
for (const document of documents.all()) {
|
|
212
276
|
if (documentConfigPaths.get(document.uri) === configPath) {
|
|
213
277
|
documentConfigPaths.delete(document.uri);
|
|
214
|
-
if (hadProject) {
|
|
278
|
+
if (hadProject && !clientCapabilities.pullDiagnostics) {
|
|
215
279
|
sendDiagnostics({ uri: document.uri, diagnostics: [] });
|
|
216
280
|
}
|
|
217
281
|
}
|
|
@@ -222,6 +286,11 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
222
286
|
for (const document of documents.all()) {
|
|
223
287
|
const knownConfigPath = documentConfigPaths.get(document.uri);
|
|
224
288
|
if (knownConfigPath === configPath) {
|
|
289
|
+
if ((await resolveProjectForDocument(document.uri)) === undefined) {
|
|
290
|
+
// The reload dropped a previously managed document; clear its markers.
|
|
291
|
+
sendDiagnostics({ uri: document.uri, diagnostics: [] });
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
225
294
|
await publish(document.uri);
|
|
226
295
|
continue;
|
|
227
296
|
}
|
|
@@ -253,13 +322,8 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
253
322
|
return [];
|
|
254
323
|
}
|
|
255
324
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
project = await resolveProjectForDocument(uri);
|
|
259
|
-
} catch {
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
if (project === undefined || !project.inputs.includes(uri)) {
|
|
325
|
+
const project = await resolveProjectForDocument(uri);
|
|
326
|
+
if (project === undefined) {
|
|
263
327
|
return [];
|
|
264
328
|
}
|
|
265
329
|
|
|
@@ -298,15 +362,15 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
298
362
|
return emptySemanticTokens();
|
|
299
363
|
}
|
|
300
364
|
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
365
|
+
const artifacts = project.artifacts.document(uri);
|
|
366
|
+
if (artifacts === undefined) {
|
|
303
367
|
return emptySemanticTokens();
|
|
304
368
|
}
|
|
305
369
|
|
|
306
370
|
const source = {
|
|
307
|
-
document:
|
|
308
|
-
sourceFile:
|
|
309
|
-
symbolTable: project.artifacts.
|
|
371
|
+
document: artifacts.document,
|
|
372
|
+
sourceFile: artifacts.sourceFile,
|
|
373
|
+
symbolTable: project.artifacts.symbolTable(),
|
|
310
374
|
scalarTypes: project.controlStack.scalarTypes,
|
|
311
375
|
};
|
|
312
376
|
return buildSemanticTokens(source, range);
|
|
@@ -318,38 +382,32 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
318
382
|
return [];
|
|
319
383
|
}
|
|
320
384
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
project = await resolveProjectForDocument(uri);
|
|
324
|
-
} catch {
|
|
325
|
-
return [];
|
|
326
|
-
}
|
|
327
|
-
if (project === undefined || !project.inputs.includes(uri)) {
|
|
385
|
+
const project = await resolveProjectForDocument(uri);
|
|
386
|
+
if (project === undefined) {
|
|
328
387
|
return [];
|
|
329
388
|
}
|
|
330
389
|
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
if (cached === undefined || symbolTable === undefined) {
|
|
390
|
+
const artifacts = project.artifacts.document(uri);
|
|
391
|
+
if (artifacts === undefined) {
|
|
334
392
|
return [];
|
|
335
393
|
}
|
|
336
394
|
|
|
337
395
|
try {
|
|
338
396
|
const context = classifyPslCompletionContext({
|
|
339
|
-
document:
|
|
340
|
-
sourceFile:
|
|
397
|
+
document: artifacts.document,
|
|
398
|
+
sourceFile: artifacts.sourceFile,
|
|
341
399
|
position,
|
|
342
400
|
});
|
|
343
401
|
return [
|
|
344
402
|
...providePslCompletionItems({
|
|
345
403
|
context,
|
|
346
|
-
sourceFile:
|
|
404
|
+
sourceFile: artifacts.sourceFile,
|
|
347
405
|
candidates: {
|
|
348
406
|
scalarTypes: project.controlStack.scalarTypes,
|
|
349
407
|
pslBlockDescriptors: project.controlStack.pslBlockDescriptors,
|
|
350
|
-
symbolTable,
|
|
408
|
+
symbolTable: project.artifacts.symbolTable(),
|
|
351
409
|
},
|
|
352
|
-
clientSupportsSnippets,
|
|
410
|
+
clientSupportsSnippets: clientCapabilities.completionSnippets,
|
|
353
411
|
}),
|
|
354
412
|
];
|
|
355
413
|
} catch {
|
|
@@ -357,27 +415,10 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
357
415
|
}
|
|
358
416
|
}
|
|
359
417
|
|
|
360
|
-
function currentDocumentArtifact(
|
|
361
|
-
project: ProjectState,
|
|
362
|
-
uri: string,
|
|
363
|
-
text: string,
|
|
364
|
-
): CachedDocument | undefined {
|
|
365
|
-
const cached = project.artifacts.getDocument(uri);
|
|
366
|
-
if (cached?.sourceFile.text === text) {
|
|
367
|
-
return cached;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (project.artifacts.update(uri, text, project.inputs, project.controlStack) === null) {
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
return project.artifacts.getDocument(uri);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
418
|
connection.onInitialize(async (params): Promise<InitializeResult> => {
|
|
377
|
-
rootPath = resolveRootPath(params
|
|
419
|
+
rootPath = resolveRootPath(params);
|
|
378
420
|
watchedConfigGlob = join(rootPath, '**', CONFIG_FILENAME);
|
|
379
|
-
|
|
380
|
-
clientSupportsSnippets = clientSupportsCompletionSnippets(params);
|
|
421
|
+
clientCapabilities = resolveClientCapabilities(params);
|
|
381
422
|
|
|
382
423
|
return {
|
|
383
424
|
capabilities: {
|
|
@@ -390,12 +431,24 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
390
431
|
range: true,
|
|
391
432
|
},
|
|
392
433
|
completionProvider: { triggerCharacters: ['.'] },
|
|
434
|
+
// Both flags reflect the current single-input implementation scope —
|
|
435
|
+
// not a property of PSL. Once the project symbol table merges multiple
|
|
436
|
+
// inputs, an edit in one file can change diagnostics in another and
|
|
437
|
+
// these must flip alongside that work.
|
|
438
|
+
...(clientCapabilities.pullDiagnostics
|
|
439
|
+
? {
|
|
440
|
+
diagnosticProvider: {
|
|
441
|
+
interFileDependencies: false,
|
|
442
|
+
workspaceDiagnostics: false,
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
393
446
|
},
|
|
394
447
|
};
|
|
395
448
|
});
|
|
396
449
|
|
|
397
450
|
connection.onInitialized(() => {
|
|
398
|
-
if (
|
|
451
|
+
if (clientCapabilities.watchedFilesRegistration) {
|
|
399
452
|
void connection
|
|
400
453
|
.sendRequest(RegistrationRequest.type, {
|
|
401
454
|
registrations: [
|
|
@@ -419,13 +472,29 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
419
472
|
params.changes.map((change) => filePathFromUri(change.uri)),
|
|
420
473
|
);
|
|
421
474
|
for (const configPath of changedConfigPaths) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
475
|
+
// Only live (or currently loading) projects are refreshed eagerly, so a
|
|
476
|
+
// config change cannot resurrect a project dropped when its last input
|
|
477
|
+
// closed; a config that newly gains an open input is still picked up
|
|
478
|
+
// lazily below through per-document rediscovery.
|
|
479
|
+
if (managedProjects.has(configPath)) {
|
|
480
|
+
try {
|
|
481
|
+
await refreshProject(configPath);
|
|
482
|
+
} catch {
|
|
483
|
+
stopManagingProject(configPath);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
427
486
|
}
|
|
428
|
-
|
|
487
|
+
if (!clientCapabilities.pullDiagnostics) {
|
|
488
|
+
await republishOpenDocumentsForConfig(configPath);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (
|
|
492
|
+
clientCapabilities.pullDiagnostics &&
|
|
493
|
+
clientCapabilities.diagnosticsRefresh &&
|
|
494
|
+
changedConfigPaths.size > 0 &&
|
|
495
|
+
!disposed
|
|
496
|
+
) {
|
|
497
|
+
void connection.languages.diagnostics.refresh().catch(() => undefined);
|
|
429
498
|
}
|
|
430
499
|
});
|
|
431
500
|
|
|
@@ -439,37 +508,54 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
439
508
|
semanticTokensForDocument(params.textDocument.uri, params.range),
|
|
440
509
|
);
|
|
441
510
|
|
|
442
|
-
connection.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
} catch {
|
|
447
|
-
return [];
|
|
511
|
+
connection.languages.diagnostics.on(async (params): Promise<DocumentDiagnosticReport> => {
|
|
512
|
+
const project = await resolveProjectForDocument(params.textDocument.uri);
|
|
513
|
+
if (project === undefined) {
|
|
514
|
+
return { kind: DocumentDiagnosticReportKind.Full, items: [] };
|
|
448
515
|
}
|
|
516
|
+
return buildDocumentDiagnosticReport(project, params.textDocument.uri);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
connection.onFoldingRanges(async (params): Promise<FoldingRange[]> => {
|
|
520
|
+
const project = await resolveProjectForDocument(params.textDocument.uri);
|
|
449
521
|
if (project === undefined) {
|
|
450
522
|
return [];
|
|
451
523
|
}
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
524
|
+
const artifacts = project.artifacts.document(params.textDocument.uri);
|
|
525
|
+
if (artifacts === undefined) {
|
|
454
526
|
return [];
|
|
455
527
|
}
|
|
456
|
-
return computeFoldingRanges(
|
|
528
|
+
return computeFoldingRanges(artifacts.document, artifacts.sourceFile);
|
|
457
529
|
});
|
|
458
530
|
|
|
459
531
|
documents.onDidOpen((event) => {
|
|
532
|
+
artifactsForDocument(event.document.uri)?.documentChanged(event.document.uri);
|
|
533
|
+
if (clientCapabilities.pullDiagnostics) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
460
536
|
publishSafely(event.document.uri);
|
|
461
537
|
});
|
|
462
538
|
documents.onDidChangeContent((event) => {
|
|
539
|
+
artifactsForDocument(event.document.uri)?.documentChanged(event.document.uri);
|
|
540
|
+
if (clientCapabilities.pullDiagnostics) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
463
543
|
publishSafely(event.document.uri);
|
|
464
544
|
});
|
|
465
545
|
documents.onDidClose((event) => {
|
|
466
546
|
const uri = event.document.uri;
|
|
467
547
|
const configPath = documentConfigPaths.get(uri);
|
|
548
|
+
artifactsForDocument(uri)?.documentClosed(uri);
|
|
549
|
+
documentConfigPaths.delete(uri);
|
|
550
|
+
// A live project always has at least one open input; when the last one
|
|
551
|
+
// closes the project is dropped, and a reopen re-resolves and reloads the
|
|
552
|
+
// config from scratch.
|
|
468
553
|
if (configPath !== undefined) {
|
|
469
|
-
|
|
554
|
+
dropProjectWithoutManagedDocuments(configPath);
|
|
555
|
+
}
|
|
556
|
+
if (!clientCapabilities.pullDiagnostics) {
|
|
557
|
+
sendDiagnostics({ uri, diagnostics: [] });
|
|
470
558
|
}
|
|
471
|
-
documentConfigPaths.delete(uri);
|
|
472
|
-
sendDiagnostics({ uri, diagnostics: [] });
|
|
473
559
|
});
|
|
474
560
|
|
|
475
561
|
documents.listen(connection);
|
|
@@ -477,7 +563,31 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
477
563
|
|
|
478
564
|
function artifactsForDocument(uri: string): ProjectArtifacts | undefined {
|
|
479
565
|
const configPath = documentConfigPaths.get(uri);
|
|
480
|
-
|
|
566
|
+
if (configPath === undefined) {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
const entry = managedProjects.get(configPath);
|
|
570
|
+
return entry?.status === 'loaded' ? entry.project.artifacts : undefined;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function hasManagedDocuments(configPath: string): boolean {
|
|
574
|
+
for (const managedConfigPath of documentConfigPaths.values()) {
|
|
575
|
+
if (managedConfigPath === configPath) {
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Deletes only loaded entries: an in-flight load settles through the
|
|
583
|
+
// association check in startProjectLoad and cleans up after itself.
|
|
584
|
+
function dropProjectWithoutManagedDocuments(configPath: string): void {
|
|
585
|
+
if (hasManagedDocuments(configPath)) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (managedProjects.get(configPath)?.status === 'loaded') {
|
|
589
|
+
managedProjects.delete(configPath);
|
|
590
|
+
}
|
|
481
591
|
}
|
|
482
592
|
|
|
483
593
|
return {
|
|
@@ -485,8 +595,10 @@ export function createServer(connection: Connection): LanguageServer {
|
|
|
485
595
|
disposed = true;
|
|
486
596
|
connection.dispose();
|
|
487
597
|
},
|
|
488
|
-
getDocumentAst: (uri) => artifactsForDocument(uri)?.
|
|
489
|
-
|
|
598
|
+
getDocumentAst: (uri) => artifactsForDocument(uri)?.document(uri),
|
|
599
|
+
// `| undefined` only because the uri may be unmanaged (closed, non-input,
|
|
600
|
+
// or projectless); a managed document's project always yields a symbolTable.
|
|
601
|
+
getProjectSymbolTable: (uri) => artifactsForDocument(uri)?.symbolTable(),
|
|
490
602
|
};
|
|
491
603
|
}
|
|
492
604
|
|
|
@@ -494,6 +606,16 @@ function emptySemanticTokens(): SemanticTokens {
|
|
|
494
606
|
return { data: [] };
|
|
495
607
|
}
|
|
496
608
|
|
|
609
|
+
function toDiagnostics(computed: readonly LspDiagnostic[]): Diagnostic[] {
|
|
610
|
+
return computed.map((diagnostic) => ({
|
|
611
|
+
range: diagnostic.range,
|
|
612
|
+
message: diagnostic.message,
|
|
613
|
+
code: diagnostic.code,
|
|
614
|
+
severity: toLspSeverity(diagnostic.severity),
|
|
615
|
+
source: 'prisma-next',
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
|
|
497
619
|
function toLspSeverity(severity: number): DiagnosticSeverity {
|
|
498
620
|
switch (severity) {
|
|
499
621
|
case ParseDiagnosticSeverity.Warning:
|
|
@@ -507,23 +629,43 @@ function toLspSeverity(severity: number): DiagnosticSeverity {
|
|
|
507
629
|
}
|
|
508
630
|
}
|
|
509
631
|
|
|
510
|
-
|
|
511
|
-
|
|
632
|
+
interface ResolvedClientCapabilities {
|
|
633
|
+
readonly watchedFilesRegistration: boolean;
|
|
634
|
+
readonly completionSnippets: boolean;
|
|
635
|
+
readonly pullDiagnostics: boolean;
|
|
636
|
+
readonly diagnosticsRefresh: boolean;
|
|
512
637
|
}
|
|
513
638
|
|
|
514
|
-
|
|
515
|
-
|
|
639
|
+
const noClientCapabilities: ResolvedClientCapabilities = {
|
|
640
|
+
watchedFilesRegistration: false,
|
|
641
|
+
completionSnippets: false,
|
|
642
|
+
pullDiagnostics: false,
|
|
643
|
+
diagnosticsRefresh: false,
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
function resolveClientCapabilities(params: InitializeParams): ResolvedClientCapabilities {
|
|
647
|
+
return {
|
|
648
|
+
watchedFilesRegistration:
|
|
649
|
+
params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true,
|
|
650
|
+
completionSnippets:
|
|
651
|
+
params.capabilities.textDocument?.completion?.completionItem?.snippetSupport === true,
|
|
652
|
+
pullDiagnostics: params.capabilities.textDocument?.diagnostic !== undefined,
|
|
653
|
+
diagnosticsRefresh: params.capabilities.workspace?.diagnostics?.refreshSupport === true,
|
|
654
|
+
};
|
|
516
655
|
}
|
|
517
656
|
|
|
518
|
-
function resolveRootPath(
|
|
519
|
-
|
|
520
|
-
rootPath
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
return fileURLToPath(
|
|
657
|
+
function resolveRootPath(params: InitializeParams): string {
|
|
658
|
+
// Single-root scope: the first workspace folder wins; multi-root workspaces
|
|
659
|
+
// are out of scope. `rootUri` / `rootPath` are the deprecated fallbacks.
|
|
660
|
+
const workspaceFolder = params.workspaceFolders?.[0];
|
|
661
|
+
if (workspaceFolder !== undefined) {
|
|
662
|
+
return fileURLToPath(workspaceFolder.uri);
|
|
663
|
+
}
|
|
664
|
+
if (params.rootUri) {
|
|
665
|
+
return fileURLToPath(params.rootUri);
|
|
524
666
|
}
|
|
525
|
-
if (rootPath) {
|
|
526
|
-
return rootPath;
|
|
667
|
+
if (params.rootPath) {
|
|
668
|
+
return params.rootPath;
|
|
527
669
|
}
|
|
528
670
|
return process.cwd();
|
|
529
671
|
}
|