@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
|
@@ -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
|
+
}
|
package/src/semantic-tokens.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
626
|
-
includePrefix: boolean,
|
|
628
|
+
marker: SyntaxToken | undefined,
|
|
627
629
|
): PendingSemanticToken {
|
|
628
630
|
const range = rangeForIdentifier(identifier, 'decorator');
|
|
629
|
-
if (
|
|
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
|
-
|
|
635
|
+
marker.offset,
|
|
639
636
|
range.endOffset,
|
|
640
637
|
'decorator',
|
|
641
638
|
range.modifierBitset,
|