@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.
@@ -2,11 +2,12 @@ import { join } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
3
  import { findNearestConfigPathForFile, loadConfig } from "@prisma-next/config-loader";
4
4
  import { format } from "@prisma-next/psl-parser/format";
5
- import { DiagnosticSeverity, DidChangeWatchedFilesNotification, FoldingRangeKind, RegistrationRequest, SemanticTokenModifiers, SemanticTokenTypes, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver";
5
+ import { CompletionItemKind, DiagnosticSeverity, DidChangeWatchedFilesNotification, FoldingRangeKind, InsertTextFormat, RegistrationRequest, SemanticTokenModifiers, SemanticTokenTypes, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver";
6
6
  import { TextDocument } from "vscode-languageserver-textdocument";
7
+ import { ArrayLiteralAst, AttributeArgListAst, BooleanLiteralExprAst, CompositeTypeDeclarationAst, FieldAttributeAst, FieldDeclarationAst, FunctionCallAst, GenericBlockDeclarationAst, IdentifierAst, KeyValuePairAst, ModelAttributeAst, ModelDeclarationAst, NamespaceDeclarationAst, NumberLiteralExprAst, ObjectLiteralExprAst, StringLiteralExprAst, TypesBlockAst, any, filterChildren, findChildToken, parse, skipTriviaToken } from "@prisma-next/psl-parser/syntax";
8
+ import { isAuthoringPslBlockDescriptor } from "@prisma-next/framework-components/authoring";
9
+ import { buildSymbolTable, findBlockDescriptor } from "@prisma-next/psl-parser";
7
10
  import { createControlStack } from "@prisma-next/framework-components/control";
8
- import { ArrayLiteralAst, BooleanLiteralExprAst, CompositeTypeDeclarationAst, FieldDeclarationAst, FunctionCallAst, IdentifierAst, ModelDeclarationAst, NamespaceDeclarationAst, NumberLiteralExprAst, ObjectLiteralExprAst, StringLiteralExprAst, TypesBlockAst, filterChildren, parse } from "@prisma-next/psl-parser/syntax";
9
- import { buildSymbolTable } from "@prisma-next/psl-parser";
10
11
  import { ProposedFeatures, createConnection } from "vscode-languageserver/node";
11
12
  //#region src/diagnostic-mapping.ts
12
13
  const ParseDiagnosticSeverity = {
@@ -24,6 +25,414 @@ function mapParseDiagnostics(diagnostics) {
24
25
  }));
25
26
  }
26
27
  //#endregion
28
+ //#region src/completion-context.ts
29
+ const UNSUPPORTED = { kind: "unsupported" };
30
+ function classifyPslCompletionContext(input) {
31
+ const root = input.document.syntax;
32
+ const offset = input.sourceFile.offsetAt(input.position);
33
+ const at = root.tokenAtOffset(offset);
34
+ if (at.leftBiased()?.kind === "Comment") return UNSUPPORTED;
35
+ const edit = cursorIdentifier(at, offset);
36
+ const preceding = precedingToken(at, edit);
37
+ const precedingNode = preceding?.parent;
38
+ const replacementStartOffset = edit?.offset ?? offset;
39
+ const declarationKeywordContext = classifyDeclarationKeyword({
40
+ node: precedingNode,
41
+ offset,
42
+ replacementStartOffset
43
+ });
44
+ if (declarationKeywordContext !== void 0) return declarationKeywordContext;
45
+ const genericBlockContext = classifyGenericBlockParameter({
46
+ offset,
47
+ at,
48
+ precedingToken: preceding,
49
+ replacementStartOffset
50
+ });
51
+ if (genericBlockContext !== void 0) return genericBlockContext;
52
+ const field = fieldForTypeSlot(precedingNode);
53
+ if (field === void 0) return UNSUPPORTED;
54
+ if (field.syntax.findAncestor(any(ModelDeclarationAst.cast, CompositeTypeDeclarationAst.cast)) === void 0) return UNSUPPORTED;
55
+ return classifyModelFieldType({
56
+ field,
57
+ offset,
58
+ replacementStartOffset,
59
+ precedingToken: preceding
60
+ });
61
+ }
62
+ /**
63
+ * Locates the field whose type position the cursor occupies. The preceding token
64
+ * climbs to the field whether the cursor sits inside a present type (the type
65
+ * identifier's predecessor still belongs to the field) or in the empty type slot
66
+ * of a typeless field (whose trailing trivia lives in the enclosing block, so
67
+ * the nearest significant token to the left is the field's own name).
68
+ */
69
+ function fieldForTypeSlot(precedingNode) {
70
+ return precedingNode?.findAncestor(FieldDeclarationAst.cast);
71
+ }
72
+ function classifyModelFieldType(input) {
73
+ const fieldName = input.field.name();
74
+ if (fieldName === void 0) return UNSUPPORTED;
75
+ const fieldNameText = fieldName.name();
76
+ if (fieldNameText === void 0) return UNSUPPORTED;
77
+ if (fieldName.syntax.isInside(input.offset)) return UNSUPPORTED;
78
+ const typeAnnotation = input.field.typeAnnotation();
79
+ if (typeAnnotation === void 0) {
80
+ if (input.precedingToken !== void 0 && fieldName.syntax.isInside(input.precedingToken.offset)) return {
81
+ kind: "modelType",
82
+ offset: input.offset,
83
+ fieldName: fieldNameText,
84
+ replacementStartOffset: input.offset
85
+ };
86
+ return UNSUPPORTED;
87
+ }
88
+ if (typeAnnotation.syntax.isOutside(input.offset)) return UNSUPPORTED;
89
+ if (typeAnnotation.argList()?.syntax.isInside(input.offset)) return UNSUPPORTED;
90
+ const name = typeAnnotation.name();
91
+ if (name === void 0) return UNSUPPORTED;
92
+ if (name.syntax.isOutside(input.offset)) return UNSUPPORTED;
93
+ if (name.isOverQualified()) return UNSUPPORTED;
94
+ return classifyTypePosition(name, input.offset, fieldNameText, input.replacementStartOffset);
95
+ }
96
+ /**
97
+ * Builds the type-completion context for a qualified name. Roles are read
98
+ * straight off the separator-positional accessors: a populated namespace
99
+ * segment is a `.`-qualified name, a populated space segment is a `:`-qualified
100
+ * name, and the absence of both is a bare model type.
101
+ *
102
+ * Behaviour change: a `:`-qualified name with no `.` (e.g. `supabase:`,
103
+ * `supabase:U`) is a `spaceMember` position rather than falling through to bare
104
+ * model-type completions. A malformed leading-separator name (`:User`, `.User`)
105
+ * carries no populated segment and resolves to `modelType` rather than
106
+ * `unsupported`.
107
+ */
108
+ function classifyTypePosition(name, offset, fieldName, replacementStartOffset) {
109
+ const namespace = name.namespace()?.name();
110
+ if (namespace !== void 0 && namespace.length > 0) {
111
+ const namespaceSpace = name.space()?.name();
112
+ return {
113
+ kind: "namespaceMember",
114
+ offset,
115
+ fieldName,
116
+ replacementStartOffset,
117
+ namespace,
118
+ ...namespaceSpace !== void 0 && namespaceSpace.length > 0 ? { space: namespaceSpace } : {}
119
+ };
120
+ }
121
+ const space = name.space()?.name();
122
+ if (space !== void 0 && space.length > 0) return {
123
+ kind: "spaceMember",
124
+ offset,
125
+ fieldName,
126
+ replacementStartOffset,
127
+ space
128
+ };
129
+ return {
130
+ kind: "modelType",
131
+ offset,
132
+ fieldName,
133
+ replacementStartOffset
134
+ };
135
+ }
136
+ const declarationCast = any(ModelDeclarationAst.cast, CompositeTypeDeclarationAst.cast, TypesBlockAst.cast, GenericBlockDeclarationAst.cast, NamespaceDeclarationAst.cast);
137
+ function classifyDeclarationKeyword(input) {
138
+ const precedingDeclaration = input.node?.findAncestor(declarationCast);
139
+ const namespace = input.node?.findAncestor(NamespaceDeclarationAst.cast);
140
+ const inNamespaceBody = blockBodyContainsOffset(namespace, input.offset);
141
+ if (precedingDeclaration !== void 0 && !canCompleteDeclaration(precedingDeclaration, input.offset, inNamespaceBody)) return;
142
+ return {
143
+ kind: "declarationKeyword",
144
+ offset: input.offset,
145
+ scope: inNamespaceBody ? "namespace" : "document",
146
+ replacementStartOffset: input.replacementStartOffset
147
+ };
148
+ }
149
+ /**
150
+ * Whether a new declaration can begin at the cursor, given the nearest enclosing
151
+ * declaration. Allowed when that declaration is still nascent (only its keyword
152
+ * typed, no name or body yet), when it is a namespace whose body holds further
153
+ * declarations, or when the cursor sits past its closing `}`.
154
+ */
155
+ function canCompleteDeclaration(precedingDeclaration, offset, inNamespaceBody) {
156
+ if (precedingDeclaration.lbrace() === void 0 && (precedingDeclaration instanceof TypesBlockAst || precedingDeclaration.name() === void 0)) return true;
157
+ if (precedingDeclaration instanceof NamespaceDeclarationAst && inNamespaceBody) return true;
158
+ const rbrace = precedingDeclaration.rbrace();
159
+ return rbrace !== void 0 && offset >= rbrace.endOffset;
160
+ }
161
+ function blockBodyContainsOffset(block, offset) {
162
+ if (block === void 0) return false;
163
+ const lbrace = block.lbrace();
164
+ if (lbrace === void 0) return false;
165
+ const bodyStart = lbrace.endOffset;
166
+ const bodyEnd = block.rbrace()?.offset ?? block.syntax.endOffset;
167
+ return offset >= bodyStart && offset <= bodyEnd;
168
+ }
169
+ function classifyGenericBlockParameter(input) {
170
+ const node = input.at.leftBiased()?.parent;
171
+ const block = node?.findAncestor(GenericBlockDeclarationAst.cast);
172
+ if (block === void 0) return;
173
+ if (hasUnsupportedAncestor(node)) return UNSUPPORTED;
174
+ if (!blockBodyContainsOffset(block, input.offset)) return UNSUPPORTED;
175
+ if ((node?.findAncestor(FieldDeclarationAst.cast))?.syntax.isInside(input.offset)) return UNSUPPORTED;
176
+ const keyword = block.keyword()?.text;
177
+ if (keyword === void 0 || keyword.length === 0) return UNSUPPORTED;
178
+ if (input.precedingToken?.kind === "Equals") return {
179
+ kind: "genericBlockValue",
180
+ offset: input.offset,
181
+ blockKeyword: keyword,
182
+ replacementStartOffset: input.replacementStartOffset
183
+ };
184
+ const activePair = activeKeyValuePair(node, input.offset);
185
+ if (activePair !== void 0 && isAfterEquals(activePair, input.offset)) return UNSUPPORTED;
186
+ return {
187
+ kind: "genericBlockKey",
188
+ offset: input.offset,
189
+ blockKeyword: keyword,
190
+ replacementStartOffset: input.replacementStartOffset,
191
+ block
192
+ };
193
+ }
194
+ function activeKeyValuePair(node, offset) {
195
+ const pair = node?.findAncestor(KeyValuePairAst.cast);
196
+ if (pair === void 0 || pair.syntax.isOutside(offset)) return;
197
+ return pair;
198
+ }
199
+ function isAfterEquals(pair, offset) {
200
+ const equals = pair.equals();
201
+ return equals !== void 0 && offset > equals.offset;
202
+ }
203
+ function hasUnsupportedAncestor(node) {
204
+ return node?.findAncestor(any(AttributeArgListAst.cast, FieldAttributeAst.cast, ModelAttributeAst.cast)) !== void 0;
205
+ }
206
+ /** The significant token preceding the cursor — the in-progress edit identifier
207
+ * is skipped, so the result is the token the classifier anchors on. */
208
+ function precedingToken(at, edit) {
209
+ const start = edit !== void 0 ? edit.prevToken : at.leftBiased();
210
+ return start === void 0 ? void 0 : skipTriviaToken(start, "prev");
211
+ }
212
+ /** The identifier token the cursor is editing, if any. */
213
+ function cursorIdentifier(at, offset) {
214
+ const right = at.rightBiased();
215
+ if (right?.kind === "Ident" && offset < right.endOffset) return right;
216
+ const left = at.leftBiased();
217
+ if (left?.kind === "Ident" && left.endOffset === offset) return left;
218
+ }
219
+ //#endregion
220
+ //#region src/completion-provider.ts
221
+ const categoryOrder = {
222
+ configuredScalar: 0,
223
+ topLevelModel: 1,
224
+ topLevelCompositeType: 2,
225
+ scalar: 3,
226
+ typeAlias: 4,
227
+ namespace: 5,
228
+ namespaceModel: 6,
229
+ namespaceCompositeType: 7
230
+ };
231
+ const declarationKeywordCategoryOrder = {
232
+ native: 0,
233
+ genericBlock: 1
234
+ };
235
+ const nameSnippetPlaceholder = "${1:Name}";
236
+ const documentNativeDeclarationKeywords = [
237
+ nativeDeclarationKeyword("model", "model ", `model ${nameSnippetPlaceholder} {\n $0\n}`),
238
+ nativeDeclarationKeyword("type", "type ", `type ${nameSnippetPlaceholder} {\n $0\n}`),
239
+ nativeDeclarationKeyword("types", "types ", "types {\n $0\n}"),
240
+ nativeDeclarationKeyword("namespace", "namespace ", `namespace \${1:name} {\n $0\n}`)
241
+ ];
242
+ const namespaceNativeDeclarationKeywords = [nativeDeclarationKeyword("model", "model ", `model ${nameSnippetPlaceholder} {\n $0\n}`), nativeDeclarationKeyword("type", "type ", `type ${nameSnippetPlaceholder} {\n $0\n}`)];
243
+ function providePslCompletionItems(input) {
244
+ const { context } = input;
245
+ switch (context.kind) {
246
+ case "unsupported": return [];
247
+ case "spaceMember": return [];
248
+ case "genericBlockValue": return [];
249
+ case "declarationKeyword": return provideDeclarationKeywordCompletionItems(context, input.sourceFile, input.candidates, input.clientSupportsSnippets);
250
+ case "genericBlockKey": return provideGenericBlockKeyCompletionItems(context, input.sourceFile, input.candidates);
251
+ case "modelType": return provideModelTypeCompletionItems(context, input.sourceFile, input.candidates);
252
+ case "namespaceMember": return provideNamespaceMemberCompletionItems(context, input.sourceFile, input.candidates);
253
+ }
254
+ }
255
+ function provideDeclarationKeywordCompletionItems(context, sourceFile, source, clientSupportsSnippets) {
256
+ const replacementRange = {
257
+ start: sourceFile.positionAt(context.replacementStartOffset),
258
+ end: sourceFile.positionAt(context.offset)
259
+ };
260
+ return declarationKeywordCandidates(context.scope, source).map((candidate) => ({
261
+ label: candidate.label,
262
+ kind: candidate.kind,
263
+ detail: candidate.detail,
264
+ sortText: declarationKeywordSortText(candidate),
265
+ filterText: candidate.label,
266
+ ...clientSupportsSnippets ? { insertTextFormat: InsertTextFormat.Snippet } : {},
267
+ textEdit: {
268
+ range: replacementRange,
269
+ newText: clientSupportsSnippets ? candidate.snippetText : candidate.insertText
270
+ }
271
+ }));
272
+ }
273
+ function declarationKeywordCandidates(scope, source) {
274
+ return [...scope === "namespace" ? namespaceNativeDeclarationKeywords : documentNativeDeclarationKeywords, ...genericBlockDeclarationKeywordCandidates(source.pslBlockDescriptors)];
275
+ }
276
+ function nativeDeclarationKeyword(label, insertText, snippetText) {
277
+ return {
278
+ category: "native",
279
+ label,
280
+ insertText,
281
+ snippetText,
282
+ detail: "PSL declaration keyword",
283
+ kind: CompletionItemKind.Keyword
284
+ };
285
+ }
286
+ function genericBlockDeclarationKeywordCandidates(descriptors) {
287
+ return descriptorBlockKeywords(descriptors).map((keyword) => ({
288
+ category: "genericBlock",
289
+ label: keyword,
290
+ insertText: `${keyword} `,
291
+ snippetText: `${keyword} ${nameSnippetPlaceholder} {\n $0\n}`,
292
+ detail: "Generic block keyword",
293
+ kind: CompletionItemKind.Keyword
294
+ }));
295
+ }
296
+ function descriptorBlockKeywords(descriptors) {
297
+ const keywords = [];
298
+ collectDescriptorBlockKeywords(descriptors, keywords);
299
+ return sortedUnique(keywords);
300
+ }
301
+ function collectDescriptorBlockKeywords(descriptors, keywords) {
302
+ for (const value of Object.values(descriptors)) {
303
+ if (isAuthoringPslBlockDescriptor(value)) {
304
+ keywords.push(value.keyword);
305
+ continue;
306
+ }
307
+ collectDescriptorBlockKeywords(value, keywords);
308
+ }
309
+ }
310
+ function declarationKeywordSortText(candidate) {
311
+ return `${declarationKeywordCategoryOrder[candidate.category]}:${candidate.label}`;
312
+ }
313
+ function provideGenericBlockKeyCompletionItems(context, sourceFile, source) {
314
+ const descriptor = findBlockDescriptor(source.pslBlockDescriptors, context.blockKeyword);
315
+ if (descriptor === void 0) return [];
316
+ const existing = existingGenericBlockParameterNames(context.block, context.offset);
317
+ const replacementRange = {
318
+ start: sourceFile.positionAt(context.replacementStartOffset),
319
+ end: sourceFile.positionAt(context.offset)
320
+ };
321
+ return Object.keys(descriptor.parameters).filter((parameterName) => !existing.has(parameterName)).map((parameterName, index) => ({
322
+ label: parameterName,
323
+ kind: CompletionItemKind.Property,
324
+ detail: "Generic block parameter",
325
+ sortText: genericBlockParameterSortText(index, parameterName),
326
+ filterText: parameterName,
327
+ textEdit: {
328
+ range: replacementRange,
329
+ newText: parameterName
330
+ }
331
+ }));
332
+ }
333
+ function existingGenericBlockParameterNames(block, cursorOffset) {
334
+ const names = /* @__PURE__ */ new Set();
335
+ for (const entry of block.entries()) {
336
+ if (!entry.syntax.isOutside(cursorOffset)) continue;
337
+ const name = entry.key()?.name();
338
+ if (name !== void 0) names.add(name);
339
+ }
340
+ return names;
341
+ }
342
+ function provideModelTypeCompletionItems(context, sourceFile, source) {
343
+ return modelTypeCompletionItems(context, sourceFile, [
344
+ ...configuredScalarCandidates(source.scalarTypes),
345
+ ...topLevelSymbolCandidates(source.symbolTable),
346
+ ...allNamespaceCandidates(source.symbolTable)
347
+ ]);
348
+ }
349
+ function provideNamespaceMemberCompletionItems(context, sourceFile, source) {
350
+ if (context.space !== void 0) return [];
351
+ return modelTypeCompletionItems(context, sourceFile, namespaceCandidates(source.symbolTable?.topLevel.namespaces[context.namespace]));
352
+ }
353
+ function modelTypeCompletionItems(context, sourceFile, candidates) {
354
+ const replacementRange = {
355
+ start: sourceFile.positionAt(context.replacementStartOffset),
356
+ end: sourceFile.positionAt(context.offset)
357
+ };
358
+ return candidates.map((candidate) => ({
359
+ label: candidate.label,
360
+ kind: candidate.kind,
361
+ detail: candidate.detail,
362
+ sortText: sortText(candidate),
363
+ filterText: candidate.filterText,
364
+ textEdit: {
365
+ range: replacementRange,
366
+ newText: candidate.insertText
367
+ }
368
+ }));
369
+ }
370
+ function configuredScalarCandidates(scalarTypes) {
371
+ return sortedUnique(scalarTypes).map((name) => ({
372
+ category: "configuredScalar",
373
+ label: name,
374
+ insertText: name,
375
+ filterText: name,
376
+ detail: "Configured scalar type",
377
+ kind: CompletionItemKind.Keyword
378
+ }));
379
+ }
380
+ function topLevelSymbolCandidates(symbolTable) {
381
+ if (symbolTable === void 0) return [];
382
+ const { topLevel } = symbolTable;
383
+ return [
384
+ ...symbolCandidates(recordNames(topLevel.models), "topLevelModel", "Model", CompletionItemKind.Class),
385
+ ...symbolCandidates(recordNames(topLevel.compositeTypes), "topLevelCompositeType", "Composite type", CompletionItemKind.Struct),
386
+ ...symbolCandidates(recordNames(topLevel.scalars), "scalar", "Scalar type", CompletionItemKind.Unit),
387
+ ...symbolCandidates(recordNames(topLevel.typeAliases), "typeAlias", "Type alias", CompletionItemKind.Reference)
388
+ ];
389
+ }
390
+ function allNamespaceCandidates(symbolTable) {
391
+ if (symbolTable === void 0) return [];
392
+ return Object.values(symbolTable.topLevel.namespaces).sort((left, right) => compareNames(left.name, right.name)).map(namespaceQualifierCandidate);
393
+ }
394
+ function namespaceCandidates(namespace) {
395
+ if (namespace === void 0) return [];
396
+ return [...symbolCandidates(recordNames(namespace.models), "namespaceModel", `Model in namespace ${namespace.name}`, CompletionItemKind.Class), ...symbolCandidates(recordNames(namespace.compositeTypes), "namespaceCompositeType", `Composite type in namespace ${namespace.name}`, CompletionItemKind.Struct)];
397
+ }
398
+ function namespaceQualifierCandidate(namespace) {
399
+ return {
400
+ category: "namespace",
401
+ label: namespace.name,
402
+ insertText: namespace.name,
403
+ filterText: namespace.name,
404
+ detail: "Namespace",
405
+ kind: CompletionItemKind.Module
406
+ };
407
+ }
408
+ function symbolCandidates(names, category, detail, kind) {
409
+ return names.map((name) => ({
410
+ category,
411
+ label: name,
412
+ insertText: name,
413
+ filterText: name,
414
+ detail,
415
+ kind
416
+ }));
417
+ }
418
+ function recordNames(record) {
419
+ return Object.values(record).map((symbol) => symbol.name).sort(compareNames);
420
+ }
421
+ function sortedUnique(names) {
422
+ return [...new Set(names)].sort(compareNames);
423
+ }
424
+ function sortText(candidate) {
425
+ return `${categoryOrder[candidate.category]}:${candidate.label}`;
426
+ }
427
+ function genericBlockParameterSortText(index, label) {
428
+ return `${index.toString().padStart(4, "0")}:${label}`;
429
+ }
430
+ function compareNames(left, right) {
431
+ if (left < right) return -1;
432
+ if (left > right) return 1;
433
+ return 0;
434
+ }
435
+ //#endregion
27
436
  //#region src/schema-inputs.ts
28
437
  function hasPslInputs(config) {
29
438
  const source = config.contract?.source;
@@ -337,13 +746,14 @@ function collectAttributes(attributes, source, tokens, namespace) {
337
746
  for (const attribute of attributes) collectAttribute(attribute, source, tokens, namespace);
338
747
  }
339
748
  function collectAttribute(attribute, source, tokens, namespace) {
340
- collectDecoratorName(attribute.name(), source.sourceFile.text, tokens);
749
+ const marker = findChildToken(attribute.syntax, "At") ?? findChildToken(attribute.syntax, "DoubleAt");
750
+ collectDecoratorName(attribute.name(), marker, tokens);
341
751
  collectAttributeArgList(attribute.argList(), source, tokens, namespace);
342
752
  }
343
- function collectDecoratorName(name, sourceText, tokens) {
753
+ function collectDecoratorName(name, marker, tokens) {
344
754
  if (name === void 0) return;
345
755
  const segments = identifierSegments(name);
346
- for (const [index, segment] of segments.entries()) tokens.push(rangeForDecoratorIdentifier(segment.identifier, sourceText, index === 0));
756
+ for (const [index, segment] of segments.entries()) tokens.push(rangeForDecoratorIdentifier(segment.identifier, index === 0 ? marker : void 0));
347
757
  }
348
758
  function collectAttributeArgList(argList, source, tokens, namespace) {
349
759
  if (argList === void 0) return;
@@ -465,12 +875,10 @@ function rangeForIdentifier(identifier, tokenType, modifierBitset = 0) {
465
875
  if (token === void 0) return createPendingSemanticToken(identifier.syntax.offset, identifier.syntax.offset, tokenType, modifierBitset);
466
876
  return pendingTokenForToken(token, tokenType, modifierBitset);
467
877
  }
468
- function rangeForDecoratorIdentifier(identifier, sourceText, includePrefix) {
878
+ function rangeForDecoratorIdentifier(identifier, marker) {
469
879
  const range = rangeForIdentifier(identifier, "decorator");
470
- if (!includePrefix) return range;
471
- let startOffset = range.startOffset;
472
- while (startOffset > 0 && sourceText.charAt(startOffset - 1) === "@") startOffset--;
473
- return createPendingSemanticToken(startOffset, range.endOffset, "decorator", range.modifierBitset);
880
+ if (marker === void 0 || marker.offset >= range.startOffset) return range;
881
+ return createPendingSemanticToken(marker.offset, range.endOffset, "decorator", range.modifierBitset);
474
882
  }
475
883
  function pendingTokenForToken(token, tokenType, modifierBitset = 0) {
476
884
  return createPendingSemanticToken(token.offset, token.offset + token.text.length, tokenType, modifierBitset);
@@ -515,6 +923,16 @@ function createServer(connection) {
515
923
  let rootPath = process.cwd();
516
924
  let watchedConfigGlob = join(rootPath, "**", CONFIG_FILENAME);
517
925
  let supportsWatchedFilesRegistration = false;
926
+ let clientSupportsSnippets = false;
927
+ let disposed = false;
928
+ function sendDiagnostics(params) {
929
+ if (disposed) return;
930
+ connection.sendDiagnostics(params);
931
+ }
932
+ function logWarn(message) {
933
+ if (disposed) return;
934
+ connection.console.warn(message);
935
+ }
518
936
  async function publish(uri) {
519
937
  const project = await resolveProjectForDocument(uri);
520
938
  if (project === void 0) return;
@@ -525,22 +943,21 @@ function createServer(connection) {
525
943
  }
526
944
  const computed = project.artifacts.update(uri, document.getText(), project.inputs, project.controlStack);
527
945
  if (computed === null) {
528
- connection.sendDiagnostics({
946
+ sendDiagnostics({
529
947
  uri,
530
948
  diagnostics: []
531
949
  });
532
950
  return;
533
951
  }
534
- const diagnostics = computed.map((diagnostic) => ({
535
- range: diagnostic.range,
536
- message: diagnostic.message,
537
- code: diagnostic.code,
538
- severity: toLspSeverity(diagnostic.severity),
539
- source: "prisma-next"
540
- }));
541
- connection.sendDiagnostics({
952
+ sendDiagnostics({
542
953
  uri,
543
- diagnostics
954
+ diagnostics: computed.map((diagnostic) => ({
955
+ range: diagnostic.range,
956
+ message: diagnostic.message,
957
+ code: diagnostic.code,
958
+ severity: toLspSeverity(diagnostic.severity),
959
+ source: "prisma-next"
960
+ }))
544
961
  });
545
962
  }
546
963
  async function resolveProjectForDocument(uri) {
@@ -606,7 +1023,7 @@ function createServer(connection) {
606
1023
  const hadProject = projects.delete(configPath);
607
1024
  for (const document of documents.all()) if (documentConfigPaths.get(document.uri) === configPath) {
608
1025
  documentConfigPaths.delete(document.uri);
609
- if (hadProject) connection.sendDiagnostics({
1026
+ if (hadProject) sendDiagnostics({
610
1027
  uri: document.uri,
611
1028
  diagnostics: []
612
1029
  });
@@ -628,6 +1045,7 @@ function createServer(connection) {
628
1045
  }
629
1046
  function publishSafely(uri) {
630
1047
  publish(uri).catch((error) => {
1048
+ if (disposed) return;
631
1049
  connection.console.error(error instanceof Error ? error.message : String(error));
632
1050
  });
633
1051
  }
@@ -676,10 +1094,49 @@ function createServer(connection) {
676
1094
  scalarTypes: project.controlStack.scalarTypes
677
1095
  }, range);
678
1096
  }
1097
+ async function completeDocument(uri, position) {
1098
+ const document = documents.get(uri);
1099
+ if (document === void 0) return [];
1100
+ let project;
1101
+ try {
1102
+ project = await resolveProjectForDocument(uri);
1103
+ } catch {
1104
+ return [];
1105
+ }
1106
+ if (project === void 0 || !project.inputs.includes(uri)) return [];
1107
+ const cached = currentDocumentArtifact(project, uri, document.getText());
1108
+ const symbolTable = project.artifacts.getSymbolTable();
1109
+ if (cached === void 0 || symbolTable === void 0) return [];
1110
+ try {
1111
+ return [...providePslCompletionItems({
1112
+ context: classifyPslCompletionContext({
1113
+ document: cached.document,
1114
+ sourceFile: cached.sourceFile,
1115
+ position
1116
+ }),
1117
+ sourceFile: cached.sourceFile,
1118
+ candidates: {
1119
+ scalarTypes: project.controlStack.scalarTypes,
1120
+ pslBlockDescriptors: project.controlStack.pslBlockDescriptors,
1121
+ symbolTable
1122
+ },
1123
+ clientSupportsSnippets
1124
+ })];
1125
+ } catch {
1126
+ return [];
1127
+ }
1128
+ }
1129
+ function currentDocumentArtifact(project, uri, text) {
1130
+ const cached = project.artifacts.getDocument(uri);
1131
+ if (cached?.sourceFile.text === text) return cached;
1132
+ if (project.artifacts.update(uri, text, project.inputs, project.controlStack) === null) return;
1133
+ return project.artifacts.getDocument(uri);
1134
+ }
679
1135
  connection.onInitialize(async (params) => {
680
1136
  rootPath = resolveRootPath(params.rootUri, params.rootPath);
681
1137
  watchedConfigGlob = join(rootPath, "**", CONFIG_FILENAME);
682
1138
  supportsWatchedFilesRegistration = clientSupportsWatchedFilesRegistration(params);
1139
+ clientSupportsSnippets = clientSupportsCompletionSnippets(params);
683
1140
  return { capabilities: {
684
1141
  textDocumentSync: TextDocumentSyncKind.Incremental,
685
1142
  documentFormattingProvider: true,
@@ -688,7 +1145,8 @@ function createServer(connection) {
688
1145
  legend: semanticTokensLegend,
689
1146
  full: true,
690
1147
  range: true
691
- }
1148
+ },
1149
+ completionProvider: { triggerCharacters: ["."] }
692
1150
  } };
693
1151
  });
694
1152
  connection.onInitialized(() => {
@@ -697,7 +1155,7 @@ function createServer(connection) {
697
1155
  method: DidChangeWatchedFilesNotification.type.method,
698
1156
  registerOptions: { watchers: [{ globPattern: watchedConfigGlob }] }
699
1157
  }] }).catch(() => void 0);
700
- else connection.console.warn("Client does not support dynamic file-watcher registration; Prisma Next config changes will not be picked up without a restart.");
1158
+ else logWarn("Client does not support dynamic file-watcher registration; Prisma Next config changes will not be picked up without a restart.");
701
1159
  });
702
1160
  connection.onDidChangeWatchedFiles(async (params) => {
703
1161
  const changedConfigPaths = configPathsFromWatchedChanges(params.changes.map((change) => filePathFromUri(change.uri)));
@@ -712,6 +1170,7 @@ function createServer(connection) {
712
1170
  }
713
1171
  });
714
1172
  connection.onDocumentFormatting((params) => formatDocument(params.textDocument.uri));
1173
+ connection.onCompletion((params) => completeDocument(params.textDocument.uri, params.position));
715
1174
  connection.languages.semanticTokens.on((params) => semanticTokensForDocument(params.textDocument.uri));
716
1175
  connection.languages.semanticTokens.onRange((params) => semanticTokensForDocument(params.textDocument.uri, params.range));
717
1176
  connection.onFoldingRanges(async (params) => {
@@ -737,7 +1196,7 @@ function createServer(connection) {
737
1196
  const configPath = documentConfigPaths.get(uri);
738
1197
  if (configPath !== void 0) projects.get(configPath)?.artifacts.remove(uri);
739
1198
  documentConfigPaths.delete(uri);
740
- connection.sendDiagnostics({
1199
+ sendDiagnostics({
741
1200
  uri,
742
1201
  diagnostics: []
743
1202
  });
@@ -750,6 +1209,7 @@ function createServer(connection) {
750
1209
  }
751
1210
  return {
752
1211
  dispose: () => {
1212
+ disposed = true;
753
1213
  connection.dispose();
754
1214
  },
755
1215
  getDocumentAst: (uri) => artifactsForDocument(uri)?.getDocument(uri),
@@ -770,6 +1230,9 @@ function toLspSeverity(severity) {
770
1230
  function clientSupportsWatchedFilesRegistration(params) {
771
1231
  return params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;
772
1232
  }
1233
+ function clientSupportsCompletionSnippets(params) {
1234
+ return params.capabilities.textDocument?.completion?.completionItem?.snippetSupport === true;
1235
+ }
773
1236
  function resolveRootPath(rootUri, rootPath) {
774
1237
  if (rootUri) return fileURLToPath(rootUri);
775
1238
  if (rootPath) return rootPath;