@prisma-next/language-server 0.14.0-dev.37 → 0.14.0-dev.39

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
@@ -4,6 +4,7 @@ import { findNearestConfigPathForFile } from '@prisma-next/config-loader';
4
4
  import type { SymbolTable } from '@prisma-next/psl-parser';
5
5
  import { type FormatOptions, format } from '@prisma-next/psl-parser/format';
6
6
  import {
7
+ type CompletionItem,
7
8
  type Connection,
8
9
  type Diagnostic,
9
10
  DiagnosticSeverity,
@@ -11,6 +12,8 @@ import {
11
12
  type FoldingRange,
12
13
  type InitializeParams,
13
14
  type InitializeResult,
15
+ type Position,
16
+ type PublishDiagnosticsParams,
14
17
  type Range,
15
18
  RegistrationRequest,
16
19
  type SemanticTokens,
@@ -19,6 +22,8 @@ import {
19
22
  type TextEdit,
20
23
  } from 'vscode-languageserver';
21
24
  import { TextDocument } from 'vscode-languageserver-textdocument';
25
+ import { classifyPslCompletionContext } from './completion-context';
26
+ import { providePslCompletionItems } from './completion-provider';
22
27
  import { CONFIG_FILENAME, resolveConfigInputs } from './config-resolution';
23
28
  import { ParseDiagnosticSeverity } from './diagnostic-mapping';
24
29
  import { computeFoldingRanges } from './folding-ranges';
@@ -63,6 +68,22 @@ export function createServer(connection: Connection): LanguageServer {
63
68
  let rootPath = process.cwd();
64
69
  let watchedConfigGlob = join(rootPath, '**', CONFIG_FILENAME);
65
70
  let supportsWatchedFilesRegistration = false;
71
+ let clientSupportsSnippets = false;
72
+ let disposed = false;
73
+
74
+ function sendDiagnostics(params: PublishDiagnosticsParams): void {
75
+ if (disposed) {
76
+ return;
77
+ }
78
+ void connection.sendDiagnostics(params);
79
+ }
80
+
81
+ function logWarn(message: string): void {
82
+ if (disposed) {
83
+ return;
84
+ }
85
+ connection.console.warn(message);
86
+ }
66
87
 
67
88
  async function publish(uri: string): Promise<void> {
68
89
  const project = await resolveProjectForDocument(uri);
@@ -81,7 +102,7 @@ export function createServer(connection: Connection): LanguageServer {
81
102
  project.controlStack,
82
103
  );
83
104
  if (computed === null) {
84
- void connection.sendDiagnostics({ uri, diagnostics: [] });
105
+ sendDiagnostics({ uri, diagnostics: [] });
85
106
  return;
86
107
  }
87
108
  const diagnostics: Diagnostic[] = computed.map((diagnostic) => ({
@@ -91,7 +112,7 @@ export function createServer(connection: Connection): LanguageServer {
91
112
  severity: toLspSeverity(diagnostic.severity),
92
113
  source: 'prisma-next',
93
114
  }));
94
- void connection.sendDiagnostics({ uri, diagnostics });
115
+ sendDiagnostics({ uri, diagnostics });
95
116
  }
96
117
 
97
118
  async function resolveProjectForDocument(uri: string): Promise<ProjectState | undefined> {
@@ -191,7 +212,7 @@ export function createServer(connection: Connection): LanguageServer {
191
212
  if (documentConfigPaths.get(document.uri) === configPath) {
192
213
  documentConfigPaths.delete(document.uri);
193
214
  if (hadProject) {
194
- void connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
215
+ sendDiagnostics({ uri: document.uri, diagnostics: [] });
195
216
  }
196
217
  }
197
218
  }
@@ -219,6 +240,9 @@ export function createServer(connection: Connection): LanguageServer {
219
240
 
220
241
  function publishSafely(uri: string): void {
221
242
  void publish(uri).catch((error: unknown) => {
243
+ if (disposed) {
244
+ return;
245
+ }
222
246
  connection.console.error(error instanceof Error ? error.message : String(error));
223
247
  });
224
248
  }
@@ -288,10 +312,72 @@ export function createServer(connection: Connection): LanguageServer {
288
312
  return buildSemanticTokens(source, range);
289
313
  }
290
314
 
315
+ async function completeDocument(uri: string, position: Position): Promise<CompletionItem[]> {
316
+ const document = documents.get(uri);
317
+ if (document === undefined) {
318
+ return [];
319
+ }
320
+
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)) {
328
+ return [];
329
+ }
330
+
331
+ const cached = currentDocumentArtifact(project, uri, document.getText());
332
+ const symbolTable = project.artifacts.getSymbolTable();
333
+ if (cached === undefined || symbolTable === undefined) {
334
+ return [];
335
+ }
336
+
337
+ try {
338
+ const context = classifyPslCompletionContext({
339
+ document: cached.document,
340
+ sourceFile: cached.sourceFile,
341
+ position,
342
+ });
343
+ return [
344
+ ...providePslCompletionItems({
345
+ context,
346
+ sourceFile: cached.sourceFile,
347
+ candidates: {
348
+ scalarTypes: project.controlStack.scalarTypes,
349
+ pslBlockDescriptors: project.controlStack.pslBlockDescriptors,
350
+ symbolTable,
351
+ },
352
+ clientSupportsSnippets,
353
+ }),
354
+ ];
355
+ } catch {
356
+ return [];
357
+ }
358
+ }
359
+
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
+
291
376
  connection.onInitialize(async (params): Promise<InitializeResult> => {
292
377
  rootPath = resolveRootPath(params.rootUri, params.rootPath);
293
378
  watchedConfigGlob = join(rootPath, '**', CONFIG_FILENAME);
294
379
  supportsWatchedFilesRegistration = clientSupportsWatchedFilesRegistration(params);
380
+ clientSupportsSnippets = clientSupportsCompletionSnippets(params);
295
381
 
296
382
  return {
297
383
  capabilities: {
@@ -303,6 +389,7 @@ export function createServer(connection: Connection): LanguageServer {
303
389
  full: true,
304
390
  range: true,
305
391
  },
392
+ completionProvider: { triggerCharacters: ['.'] },
306
393
  },
307
394
  };
308
395
  });
@@ -321,7 +408,7 @@ export function createServer(connection: Connection): LanguageServer {
321
408
  })
322
409
  .catch(() => undefined);
323
410
  } else {
324
- connection.console.warn(
411
+ logWarn(
325
412
  'Client does not support dynamic file-watcher registration; Prisma Next config changes will not be picked up without a restart.',
326
413
  );
327
414
  }
@@ -343,6 +430,7 @@ export function createServer(connection: Connection): LanguageServer {
343
430
  });
344
431
 
345
432
  connection.onDocumentFormatting((params) => formatDocument(params.textDocument.uri));
433
+ connection.onCompletion((params) => completeDocument(params.textDocument.uri, params.position));
346
434
 
347
435
  connection.languages.semanticTokens.on((params) =>
348
436
  semanticTokensForDocument(params.textDocument.uri),
@@ -381,7 +469,7 @@ export function createServer(connection: Connection): LanguageServer {
381
469
  projects.get(configPath)?.artifacts.remove(uri);
382
470
  }
383
471
  documentConfigPaths.delete(uri);
384
- void connection.sendDiagnostics({ uri, diagnostics: [] });
472
+ sendDiagnostics({ uri, diagnostics: [] });
385
473
  });
386
474
 
387
475
  documents.listen(connection);
@@ -394,6 +482,7 @@ export function createServer(connection: Connection): LanguageServer {
394
482
 
395
483
  return {
396
484
  dispose: () => {
485
+ disposed = true;
397
486
  connection.dispose();
398
487
  },
399
488
  getDocumentAst: (uri) => artifactsForDocument(uri)?.getDocument(uri),
@@ -422,6 +511,10 @@ function clientSupportsWatchedFilesRegistration(params: InitializeParams): boole
422
511
  return params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;
423
512
  }
424
513
 
514
+ function clientSupportsCompletionSnippets(params: InitializeParams): boolean {
515
+ return params.capabilities.textDocument?.completion?.completionItem?.snippetSupport === true;
516
+ }
517
+
425
518
  function resolveRootPath(
426
519
  rootUri: string | null | undefined,
427
520
  rootPath: string | null | undefined,