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

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.
@@ -0,0 +1,454 @@
1
+ import {
2
+ type AuthoringPslBlockDescriptorNamespace,
3
+ isAuthoringPslBlockDescriptor,
4
+ } from '@prisma-next/framework-components/authoring';
5
+ import {
6
+ findBlockDescriptor,
7
+ type NamespaceSymbol,
8
+ type SymbolTable,
9
+ } from '@prisma-next/psl-parser';
10
+ import type { GenericBlockDeclarationAst, SourceFile } from '@prisma-next/psl-parser/syntax';
11
+ import { type CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver';
12
+ import type {
13
+ DeclarationKeywordCompletionContext,
14
+ GenericBlockKeyCompletionContext,
15
+ ModelTypeCompletionContext,
16
+ NamespaceMemberCompletionContext,
17
+ PslCompletionContext,
18
+ } from './completion-context';
19
+
20
+ export interface PslCompletionCandidateSource {
21
+ readonly scalarTypes: readonly string[];
22
+ readonly pslBlockDescriptors: AuthoringPslBlockDescriptorNamespace;
23
+ readonly symbolTable?: SymbolTable;
24
+ }
25
+
26
+ export interface ProvidePslCompletionItemsInput {
27
+ readonly context: PslCompletionContext;
28
+ readonly sourceFile: SourceFile;
29
+ readonly candidates: PslCompletionCandidateSource;
30
+ readonly clientSupportsSnippets: boolean;
31
+ }
32
+
33
+ type DeclarationKeywordCompletionCandidateCategory = 'native' | 'genericBlock';
34
+
35
+ type ModelTypeCompletionCandidateCategory =
36
+ | 'configuredScalar'
37
+ | 'topLevelModel'
38
+ | 'topLevelCompositeType'
39
+ | 'scalar'
40
+ | 'typeAlias'
41
+ | 'namespace'
42
+ | 'namespaceModel'
43
+ | 'namespaceCompositeType';
44
+
45
+ interface DeclarationKeywordCompletionCandidate {
46
+ readonly category: DeclarationKeywordCompletionCandidateCategory;
47
+ readonly label: string;
48
+ readonly insertText: string;
49
+ readonly snippetText: string;
50
+ readonly detail: string;
51
+ readonly kind: CompletionItemKind;
52
+ }
53
+
54
+ interface ModelTypeCompletionCandidate {
55
+ readonly category: ModelTypeCompletionCandidateCategory;
56
+ readonly label: string;
57
+ readonly insertText: string;
58
+ readonly filterText: string;
59
+ readonly detail: string;
60
+ readonly kind: CompletionItemKind;
61
+ }
62
+
63
+ const categoryOrder: Record<ModelTypeCompletionCandidateCategory, number> = {
64
+ configuredScalar: 0,
65
+ topLevelModel: 1,
66
+ topLevelCompositeType: 2,
67
+ scalar: 3,
68
+ typeAlias: 4,
69
+ namespace: 5,
70
+ namespaceModel: 6,
71
+ namespaceCompositeType: 7,
72
+ };
73
+
74
+ const declarationKeywordCategoryOrder: Record<
75
+ DeclarationKeywordCompletionCandidateCategory,
76
+ number
77
+ > = {
78
+ native: 0,
79
+ genericBlock: 1,
80
+ };
81
+
82
+ const nameSnippetPlaceholder = '$' + '{1:Name}';
83
+ const namespaceSnippetPlaceholder = '$' + '{1:name}';
84
+
85
+ const documentNativeDeclarationKeywords: readonly DeclarationKeywordCompletionCandidate[] = [
86
+ nativeDeclarationKeyword('model', 'model ', `model ${nameSnippetPlaceholder} {\n $0\n}`),
87
+ nativeDeclarationKeyword('type', 'type ', `type ${nameSnippetPlaceholder} {\n $0\n}`),
88
+ nativeDeclarationKeyword('types', 'types ', 'types {\n $0\n}'),
89
+ nativeDeclarationKeyword(
90
+ 'namespace',
91
+ 'namespace ',
92
+ `namespace ${namespaceSnippetPlaceholder} {\n $0\n}`,
93
+ ),
94
+ ];
95
+
96
+ const namespaceNativeDeclarationKeywords: readonly DeclarationKeywordCompletionCandidate[] = [
97
+ nativeDeclarationKeyword('model', 'model ', `model ${nameSnippetPlaceholder} {\n $0\n}`),
98
+ nativeDeclarationKeyword('type', 'type ', `type ${nameSnippetPlaceholder} {\n $0\n}`),
99
+ ];
100
+
101
+ export function providePslCompletionItems(
102
+ input: ProvidePslCompletionItemsInput,
103
+ ): readonly CompletionItem[] {
104
+ const { context } = input;
105
+ switch (context.kind) {
106
+ case 'unsupported':
107
+ return [];
108
+ // Foreign contract-space members require the multi-input symbol table; no
109
+ // contract-space registry exists today, so this position yields nothing yet.
110
+ case 'spaceMember':
111
+ return [];
112
+ // Parameter-value completion (option allowed-values / ref scopes) is future
113
+ // work.
114
+ case 'genericBlockValue':
115
+ return [];
116
+ case 'declarationKeyword':
117
+ return provideDeclarationKeywordCompletionItems(
118
+ context,
119
+ input.sourceFile,
120
+ input.candidates,
121
+ input.clientSupportsSnippets,
122
+ );
123
+ case 'genericBlockKey':
124
+ return provideGenericBlockKeyCompletionItems(context, input.sourceFile, input.candidates);
125
+ case 'modelType':
126
+ return provideModelTypeCompletionItems(context, input.sourceFile, input.candidates);
127
+ case 'namespaceMember':
128
+ return provideNamespaceMemberCompletionItems(context, input.sourceFile, input.candidates);
129
+ }
130
+ }
131
+
132
+ function provideDeclarationKeywordCompletionItems(
133
+ context: DeclarationKeywordCompletionContext,
134
+ sourceFile: SourceFile,
135
+ source: PslCompletionCandidateSource,
136
+ clientSupportsSnippets: boolean,
137
+ ): readonly CompletionItem[] {
138
+ const replacementRange = {
139
+ start: sourceFile.positionAt(context.replacementStartOffset),
140
+ end: sourceFile.positionAt(context.offset),
141
+ };
142
+
143
+ return declarationKeywordCandidates(context.scope, source).map((candidate) => ({
144
+ label: candidate.label,
145
+ kind: candidate.kind,
146
+ detail: candidate.detail,
147
+ sortText: declarationKeywordSortText(candidate),
148
+ filterText: candidate.label,
149
+ ...(clientSupportsSnippets ? { insertTextFormat: InsertTextFormat.Snippet } : {}),
150
+ textEdit: {
151
+ range: replacementRange,
152
+ newText: clientSupportsSnippets ? candidate.snippetText : candidate.insertText,
153
+ },
154
+ }));
155
+ }
156
+
157
+ function declarationKeywordCandidates(
158
+ scope: DeclarationKeywordCompletionContext['scope'],
159
+ source: PslCompletionCandidateSource,
160
+ ): readonly DeclarationKeywordCompletionCandidate[] {
161
+ const nativeCandidates =
162
+ scope === 'namespace' ? namespaceNativeDeclarationKeywords : documentNativeDeclarationKeywords;
163
+ return [
164
+ ...nativeCandidates,
165
+ ...genericBlockDeclarationKeywordCandidates(source.pslBlockDescriptors),
166
+ ];
167
+ }
168
+
169
+ function nativeDeclarationKeyword(
170
+ label: string,
171
+ insertText: string,
172
+ snippetText: string,
173
+ ): DeclarationKeywordCompletionCandidate {
174
+ return {
175
+ category: 'native',
176
+ label,
177
+ insertText,
178
+ snippetText,
179
+ detail: 'PSL declaration keyword',
180
+ kind: CompletionItemKind.Keyword,
181
+ };
182
+ }
183
+
184
+ function genericBlockDeclarationKeywordCandidates(
185
+ descriptors: AuthoringPslBlockDescriptorNamespace,
186
+ ): readonly DeclarationKeywordCompletionCandidate[] {
187
+ return descriptorBlockKeywords(descriptors).map((keyword) => ({
188
+ category: 'genericBlock',
189
+ label: keyword,
190
+ insertText: `${keyword} `,
191
+ snippetText: `${keyword} ${nameSnippetPlaceholder} {\n $0\n}`,
192
+ detail: 'Generic block keyword',
193
+ kind: CompletionItemKind.Keyword,
194
+ }));
195
+ }
196
+
197
+ function descriptorBlockKeywords(
198
+ descriptors: AuthoringPslBlockDescriptorNamespace,
199
+ ): readonly string[] {
200
+ const keywords: string[] = [];
201
+ collectDescriptorBlockKeywords(descriptors, keywords);
202
+ return sortedUnique(keywords);
203
+ }
204
+
205
+ function collectDescriptorBlockKeywords(
206
+ descriptors: AuthoringPslBlockDescriptorNamespace,
207
+ keywords: string[],
208
+ ): void {
209
+ for (const value of Object.values(descriptors)) {
210
+ if (isAuthoringPslBlockDescriptor(value)) {
211
+ keywords.push(value.keyword);
212
+ continue;
213
+ }
214
+ collectDescriptorBlockKeywords(value, keywords);
215
+ }
216
+ }
217
+
218
+ function declarationKeywordSortText(candidate: DeclarationKeywordCompletionCandidate): string {
219
+ return `${declarationKeywordCategoryOrder[candidate.category]}:${candidate.label}`;
220
+ }
221
+
222
+ function provideGenericBlockKeyCompletionItems(
223
+ context: GenericBlockKeyCompletionContext,
224
+ sourceFile: SourceFile,
225
+ source: PslCompletionCandidateSource,
226
+ ): readonly CompletionItem[] {
227
+ const descriptor = findBlockDescriptor(source.pslBlockDescriptors, context.blockKeyword);
228
+ if (descriptor === undefined) {
229
+ return [];
230
+ }
231
+
232
+ const existing = existingGenericBlockParameterNames(context.block, context.offset);
233
+ const replacementRange = {
234
+ start: sourceFile.positionAt(context.replacementStartOffset),
235
+ end: sourceFile.positionAt(context.offset),
236
+ };
237
+
238
+ return Object.keys(descriptor.parameters)
239
+ .filter((parameterName) => !existing.has(parameterName))
240
+ .map((parameterName, index) => ({
241
+ label: parameterName,
242
+ kind: CompletionItemKind.Property,
243
+ detail: 'Generic block parameter',
244
+ sortText: genericBlockParameterSortText(index, parameterName),
245
+ filterText: parameterName,
246
+ textEdit: {
247
+ range: replacementRange,
248
+ newText: parameterName,
249
+ },
250
+ }));
251
+ }
252
+
253
+ function existingGenericBlockParameterNames(
254
+ block: GenericBlockDeclarationAst,
255
+ cursorOffset: number,
256
+ ): Set<string> {
257
+ const names = new Set<string>();
258
+ for (const entry of block.entries()) {
259
+ if (!entry.syntax.isOutside(cursorOffset)) {
260
+ continue;
261
+ }
262
+ const name = entry.key()?.name();
263
+ if (name !== undefined) {
264
+ names.add(name);
265
+ }
266
+ }
267
+ return names;
268
+ }
269
+
270
+ function provideModelTypeCompletionItems(
271
+ context: ModelTypeCompletionContext,
272
+ sourceFile: SourceFile,
273
+ source: PslCompletionCandidateSource,
274
+ ): readonly CompletionItem[] {
275
+ return modelTypeCompletionItems(context, sourceFile, [
276
+ ...configuredScalarCandidates(source.scalarTypes),
277
+ ...topLevelSymbolCandidates(source.symbolTable),
278
+ ...allNamespaceCandidates(source.symbolTable),
279
+ ]);
280
+ }
281
+
282
+ function provideNamespaceMemberCompletionItems(
283
+ context: NamespaceMemberCompletionContext,
284
+ sourceFile: SourceFile,
285
+ source: PslCompletionCandidateSource,
286
+ ): readonly CompletionItem[] {
287
+ // A foreign contract-space reference resolves against external symbols that no
288
+ // registry exposes yet; local namespace members must not stand in for them.
289
+ if (context.space !== undefined) {
290
+ return [];
291
+ }
292
+ return modelTypeCompletionItems(
293
+ context,
294
+ sourceFile,
295
+ namespaceCandidates(source.symbolTable?.topLevel.namespaces[context.namespace]),
296
+ );
297
+ }
298
+
299
+ function modelTypeCompletionItems(
300
+ context: ModelTypeCompletionContext | NamespaceMemberCompletionContext,
301
+ sourceFile: SourceFile,
302
+ candidates: readonly ModelTypeCompletionCandidate[],
303
+ ): readonly CompletionItem[] {
304
+ const replacementRange = {
305
+ start: sourceFile.positionAt(context.replacementStartOffset),
306
+ end: sourceFile.positionAt(context.offset),
307
+ };
308
+
309
+ return candidates.map((candidate) => ({
310
+ label: candidate.label,
311
+ kind: candidate.kind,
312
+ detail: candidate.detail,
313
+ sortText: sortText(candidate),
314
+ filterText: candidate.filterText,
315
+ textEdit: {
316
+ range: replacementRange,
317
+ newText: candidate.insertText,
318
+ },
319
+ }));
320
+ }
321
+
322
+ function configuredScalarCandidates(
323
+ scalarTypes: readonly string[],
324
+ ): readonly ModelTypeCompletionCandidate[] {
325
+ return sortedUnique(scalarTypes).map((name) => ({
326
+ category: 'configuredScalar',
327
+ label: name,
328
+ insertText: name,
329
+ filterText: name,
330
+ detail: 'Configured scalar type',
331
+ kind: CompletionItemKind.Keyword,
332
+ }));
333
+ }
334
+
335
+ function topLevelSymbolCandidates(
336
+ symbolTable: SymbolTable | undefined,
337
+ ): readonly ModelTypeCompletionCandidate[] {
338
+ if (symbolTable === undefined) {
339
+ return [];
340
+ }
341
+ const { topLevel } = symbolTable;
342
+ return [
343
+ ...symbolCandidates(
344
+ recordNames(topLevel.models),
345
+ 'topLevelModel',
346
+ 'Model',
347
+ CompletionItemKind.Class,
348
+ ),
349
+ ...symbolCandidates(
350
+ recordNames(topLevel.compositeTypes),
351
+ 'topLevelCompositeType',
352
+ 'Composite type',
353
+ CompletionItemKind.Struct,
354
+ ),
355
+ ...symbolCandidates(
356
+ recordNames(topLevel.scalars),
357
+ 'scalar',
358
+ 'Scalar type',
359
+ CompletionItemKind.Unit,
360
+ ),
361
+ ...symbolCandidates(
362
+ recordNames(topLevel.typeAliases),
363
+ 'typeAlias',
364
+ 'Type alias',
365
+ CompletionItemKind.Reference,
366
+ ),
367
+ ];
368
+ }
369
+
370
+ function allNamespaceCandidates(
371
+ symbolTable: SymbolTable | undefined,
372
+ ): readonly ModelTypeCompletionCandidate[] {
373
+ if (symbolTable === undefined) {
374
+ return [];
375
+ }
376
+ return Object.values(symbolTable.topLevel.namespaces)
377
+ .sort((left, right) => compareNames(left.name, right.name))
378
+ .map(namespaceQualifierCandidate);
379
+ }
380
+
381
+ function namespaceCandidates(
382
+ namespace: NamespaceSymbol | undefined,
383
+ ): readonly ModelTypeCompletionCandidate[] {
384
+ if (namespace === undefined) {
385
+ return [];
386
+ }
387
+ return [
388
+ ...symbolCandidates(
389
+ recordNames(namespace.models),
390
+ 'namespaceModel',
391
+ `Model in namespace ${namespace.name}`,
392
+ CompletionItemKind.Class,
393
+ ),
394
+ ...symbolCandidates(
395
+ recordNames(namespace.compositeTypes),
396
+ 'namespaceCompositeType',
397
+ `Composite type in namespace ${namespace.name}`,
398
+ CompletionItemKind.Struct,
399
+ ),
400
+ ];
401
+ }
402
+
403
+ function namespaceQualifierCandidate(namespace: NamespaceSymbol): ModelTypeCompletionCandidate {
404
+ return {
405
+ category: 'namespace',
406
+ label: namespace.name,
407
+ insertText: namespace.name,
408
+ filterText: namespace.name,
409
+ detail: 'Namespace',
410
+ kind: CompletionItemKind.Module,
411
+ };
412
+ }
413
+
414
+ function symbolCandidates(
415
+ names: readonly string[],
416
+ category: ModelTypeCompletionCandidateCategory,
417
+ detail: string,
418
+ kind: CompletionItemKind,
419
+ ): readonly ModelTypeCompletionCandidate[] {
420
+ return names.map((name) => ({
421
+ category,
422
+ label: name,
423
+ insertText: name,
424
+ filterText: name,
425
+ detail,
426
+ kind,
427
+ }));
428
+ }
429
+
430
+ function recordNames<T extends { readonly name: string }>(
431
+ record: Record<string, T>,
432
+ ): readonly string[] {
433
+ return Object.values(record)
434
+ .map((symbol) => symbol.name)
435
+ .sort(compareNames);
436
+ }
437
+
438
+ function sortedUnique(names: readonly string[]): readonly string[] {
439
+ return [...new Set(names)].sort(compareNames);
440
+ }
441
+
442
+ function sortText(candidate: ModelTypeCompletionCandidate): string {
443
+ return `${categoryOrder[candidate.category]}:${candidate.label}`;
444
+ }
445
+
446
+ function genericBlockParameterSortText(index: number, label: string): string {
447
+ return `${index.toString().padStart(4, '0')}:${label}`;
448
+ }
449
+
450
+ function compareNames(left: string, right: string): number {
451
+ if (left < right) return -1;
452
+ if (left > right) return 1;
453
+ return 0;
454
+ }
@@ -13,6 +13,7 @@ import {
13
13
  FieldDeclarationAst,
14
14
  FunctionCallAst,
15
15
  filterChildren,
16
+ findChildToken,
16
17
  type GenericBlockMemberAst,
17
18
  IdentifierAst,
18
19
  ModelDeclarationAst,
@@ -352,13 +353,15 @@ function collectAttribute(
352
353
  tokens: PendingSemanticToken[],
353
354
  namespace: string | undefined,
354
355
  ): void {
355
- collectDecoratorName(attribute.name(), source.sourceFile.text, tokens);
356
+ const marker =
357
+ findChildToken(attribute.syntax, 'At') ?? findChildToken(attribute.syntax, 'DoubleAt');
358
+ collectDecoratorName(attribute.name(), marker, tokens);
356
359
  collectAttributeArgList(attribute.argList(), source, tokens, namespace);
357
360
  }
358
361
 
359
362
  function collectDecoratorName(
360
363
  name: QualifiedNameAst | undefined,
361
- sourceText: string,
364
+ marker: SyntaxToken | undefined,
362
365
  tokens: PendingSemanticToken[],
363
366
  ): void {
364
367
  if (name === undefined) {
@@ -366,7 +369,7 @@ function collectDecoratorName(
366
369
  }
367
370
  const segments = identifierSegments(name);
368
371
  for (const [index, segment] of segments.entries()) {
369
- tokens.push(rangeForDecoratorIdentifier(segment.identifier, sourceText, index === 0));
372
+ tokens.push(rangeForDecoratorIdentifier(segment.identifier, index === 0 ? marker : undefined));
370
373
  }
371
374
  }
372
375
 
@@ -622,20 +625,14 @@ function rangeForIdentifier(
622
625
 
623
626
  function rangeForDecoratorIdentifier(
624
627
  identifier: IdentifierAst,
625
- sourceText: string,
626
- includePrefix: boolean,
628
+ marker: SyntaxToken | undefined,
627
629
  ): PendingSemanticToken {
628
630
  const range = rangeForIdentifier(identifier, 'decorator');
629
- if (!includePrefix) {
631
+ if (marker === undefined || marker.offset >= range.startOffset) {
630
632
  return range;
631
633
  }
632
-
633
- let startOffset = range.startOffset;
634
- while (startOffset > 0 && sourceText.charAt(startOffset - 1) === '@') {
635
- startOffset--;
636
- }
637
634
  return createPendingSemanticToken(
638
- startOffset,
635
+ marker.offset,
639
636
  range.endOffset,
640
637
  'decorator',
641
638
  range.modifierBitset,