@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/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): CachedDocument | undefined;
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 projects = new Map<string, ProjectState>();
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 supportsWatchedFilesRegistration = false;
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 computed = project.artifacts.update(
99
- uri,
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
- const diagnostics: Diagnostic[] = computed.map((diagnostic) => ({
109
- range: diagnostic.range,
110
- message: diagnostic.message,
111
- code: diagnostic.code,
112
- severity: toLspSeverity(diagnostic.severity),
113
- source: 'prisma-next',
114
- }));
115
- sendDiagnostics({ uri, diagnostics });
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
- const configPath = await findNearestConfigPathForFile(filePath);
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 existing = projects.get(configPath);
157
- if (existing !== undefined) {
158
- return existing;
196
+ const entry = managedProjects.get(configPath);
197
+ if (entry === undefined) {
198
+ return startProjectLoad(configPath);
159
199
  }
160
- const existingLoad = projectLoads.get(configPath);
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 queueProjectLoad(configPath);
204
+ return startProjectLoad(configPath);
169
205
  }
170
206
 
171
- function queueProjectLoad(configPath: string): Promise<ProjectState> {
172
- const previousLoad = projectLoads.get(configPath) ?? Promise.resolve();
173
- const load = previousLoad
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
- .finally(() => {
177
- if (projectLoads.get(configPath) === load) {
178
- projectLoads.delete(configPath);
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
- projectLoads.set(configPath, load);
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
- // Preserve open-document ASTs across config reloads; the project symbol table
188
- // refreshes on the next publish against the new stack.
189
- const artifacts = projects.get(configPath)?.artifacts ?? createProjectArtifacts();
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 hadProject = projects.delete(configPath);
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
- let project: ProjectState | undefined;
257
- try {
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 cached = project.artifacts.getDocument(uri);
302
- if (cached === undefined || cached.text !== text) {
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: cached.document,
308
- sourceFile: cached.sourceFile,
309
- symbolTable: project.artifacts.getSymbolTable(),
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
- let project: ProjectState | undefined;
322
- try {
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 cached = currentDocumentArtifact(project, uri, document.getText());
332
- const symbolTable = project.artifacts.getSymbolTable();
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: cached.document,
340
- sourceFile: cached.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: cached.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.rootUri, params.rootPath);
419
+ rootPath = resolveRootPath(params);
378
420
  watchedConfigGlob = join(rootPath, '**', CONFIG_FILENAME);
379
- supportsWatchedFilesRegistration = clientSupportsWatchedFilesRegistration(params);
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 (supportsWatchedFilesRegistration) {
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
- try {
423
- await refreshProject(configPath);
424
- } catch {
425
- stopManagingProject(configPath);
426
- continue;
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
- await republishOpenDocumentsForConfig(configPath);
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.onFoldingRanges(async (params): Promise<FoldingRange[]> => {
443
- let project: ProjectState | undefined;
444
- try {
445
- project = await resolveProjectForDocument(params.textDocument.uri);
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 cached = project.artifacts.getDocument(params.textDocument.uri);
453
- if (cached === undefined) {
524
+ const artifacts = project.artifacts.document(params.textDocument.uri);
525
+ if (artifacts === undefined) {
454
526
  return [];
455
527
  }
456
- return computeFoldingRanges(cached.document, cached.sourceFile);
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
- projects.get(configPath)?.artifacts.remove(uri);
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
- return configPath === undefined ? undefined : projects.get(configPath)?.artifacts;
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)?.getDocument(uri),
489
- getProjectSymbolTable: (uri) => artifactsForDocument(uri)?.getSymbolTable(),
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
- function clientSupportsWatchedFilesRegistration(params: InitializeParams): boolean {
511
- return params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;
632
+ interface ResolvedClientCapabilities {
633
+ readonly watchedFilesRegistration: boolean;
634
+ readonly completionSnippets: boolean;
635
+ readonly pullDiagnostics: boolean;
636
+ readonly diagnosticsRefresh: boolean;
512
637
  }
513
638
 
514
- function clientSupportsCompletionSnippets(params: InitializeParams): boolean {
515
- return params.capabilities.textDocument?.completion?.completionItem?.snippetSupport === true;
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
- rootUri: string | null | undefined,
520
- rootPath: string | null | undefined,
521
- ): string {
522
- if (rootUri) {
523
- return fileURLToPath(rootUri);
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
  }