@prisma-next/language-server 0.14.0-dev.36 → 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.
- package/README.md +7 -4
- package/dist/exports/index.d.mts +1 -0
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +488 -25
- package/dist/exports/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/completion-context.ts +432 -0
- package/src/completion-provider.ts +454 -0
- package/src/semantic-tokens.ts +9 -12
- package/src/server.ts +98 -5
package/dist/exports/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
878
|
+
function rangeForDecoratorIdentifier(identifier, marker) {
|
|
469
879
|
const range = rangeForIdentifier(identifier, "decorator");
|
|
470
|
-
if (
|
|
471
|
-
|
|
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
|
-
|
|
946
|
+
sendDiagnostics({
|
|
529
947
|
uri,
|
|
530
948
|
diagnostics: []
|
|
531
949
|
});
|
|
532
950
|
return;
|
|
533
951
|
}
|
|
534
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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;
|