@prisma-next/language-server 0.14.0-dev.21 → 0.14.0-dev.23
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 +29 -26
- package/dist/exports/index.d.mts +1 -0
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +383 -16
- package/dist/exports/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/project-artifacts.ts +2 -0
- package/src/semantic-tokens.ts +701 -0
- package/src/server.ts +70 -14
package/README.md
CHANGED
|
@@ -1,45 +1,48 @@
|
|
|
1
1
|
# @prisma-next/language-server
|
|
2
2
|
|
|
3
|
-
> **Internal package.** This package is an implementation detail of [`prisma-next`](https://www.npmjs.com/package/prisma-next)
|
|
4
|
-
> and is published only to support its runtime. Its API is unstable and may change
|
|
5
|
-
> without notice. Do not depend on this package directly; install `prisma-next` instead.
|
|
3
|
+
> **Internal package.** This package is an implementation detail of [`prisma-next`](https://www.npmjs.com/package/prisma-next) and is published only to support its runtime. Its API is unstable and may change without notice. Do not depend on this package directly; install `prisma-next` instead.
|
|
6
4
|
|
|
7
|
-
The Prisma Next language server
|
|
5
|
+
The Prisma Next language server speaks the Language Server Protocol over stdio for PSL schema inputs declared in a project's `prisma-next.config.ts`. It is launched by the `prisma-next lsp` subcommand, so editor features come from the project's own `@prisma-next` version and stay version-matched by construction.
|
|
8
6
|
|
|
9
7
|
## Scope
|
|
10
8
|
|
|
11
|
-
Supported capabilities are intentionally narrow: parse diagnostics
|
|
9
|
+
Supported capabilities are intentionally narrow: parse diagnostics, whole-document formatting, folding ranges, and full/range semantic tokens for configured PSL inputs. Formatting is only available for documents listed in `contract.source.inputs`, uses `@prisma-next/psl-parser/format`, and applies formatter options from the project's Prisma config `formatter` block. Semantic tokens use the standard LSP token taxonomy advertised by the server; they do not introduce Prisma-specific token names or a second parser. Hover, completion, navigation, range formatting, on-type formatting, semantic-token delta requests, and editor-extension work are out of scope. A server process can manage multiple projects under the workspace root, keyed by the config file each open document belongs to.
|
|
10
|
+
|
|
11
|
+
## Responsibilities
|
|
12
|
+
|
|
13
|
+
- Resolve workspace/project configuration for open PSL documents and keep managed projects aligned with config-file changes.
|
|
14
|
+
- Publish parse diagnostics and serve whole-document formatting, folding ranges, and full/range semantic tokens for configured PSL inputs.
|
|
15
|
+
- Preserve parser artifacts per project so editor features share the same AST, source-file, and symbol-table lifecycle instead of reparsing through feature-specific paths.
|
|
16
|
+
- Fail safely for unsupported documents, missing or closed buffers, config-load failures, malformed inputs, and oversized semantic-token requests.
|
|
17
|
+
|
|
18
|
+
## Dependencies
|
|
19
|
+
|
|
20
|
+
- `@prisma-next/config-loader` — discovers nearest config files and loads project configuration.
|
|
21
|
+
- `@prisma-next/psl-parser` — parses PSL, builds symbol tables, exposes syntax artifacts, and formats PSL text.
|
|
22
|
+
- `@prisma-next/framework-components` — supplies control-stack input types used when resolving project configuration.
|
|
23
|
+
- `@prisma-next/errors` and `@prisma-next/utils` — shared framework utilities used by parsing/config plumbing.
|
|
24
|
+
- `vscode-languageserver` and `vscode-languageserver-textdocument` — LSP connection, request/notification types, semantic-token/folding/formatting types, and incremental document management.
|
|
12
25
|
|
|
13
26
|
## How it works
|
|
14
27
|
|
|
15
|
-
1. **`initialize`** — resolves the workspace root from the client's `rootUri` and registers config-file watching when the client supports it. Configs are loaded when matching documents open or when watched config files change. If a config cannot be loaded, the server does not manage that project.
|
|
16
|
-
2. **Document sync** — text-document sync is **incremental**
|
|
17
|
-
(`TextDocumentSyncKind.Incremental`); the `TextDocuments` manager applies
|
|
18
|
-
incremental edits, and the server re-parses the full current buffer on each
|
|
19
|
-
change.
|
|
28
|
+
1. **`initialize`** — resolves the workspace root from the client's `rootUri` and registers config-file watching when the client supports it. The server advertises incremental text sync, whole-document formatting, folding ranges, and `semanticTokensProvider` with a stable standard-only legend, `full: true`, and `range: true`. Configs are loaded when matching documents open or when watched config files change. If a config cannot be loaded, the server does not manage that project.
|
|
29
|
+
2. **Document sync** — text-document sync is **incremental** (`TextDocumentSyncKind.Incremental`); the `TextDocuments` manager applies incremental edits, and the server re-parses the full current buffer on each change.
|
|
20
30
|
3. **Diagnostics** — on `didOpen` / `didChange` of a document whose URI is a configured PSL input, the server runs `@prisma-next/psl-parser`'s `parse()` (the CST path) and `buildSymbolTable()`, then publishes the merged, mapped diagnostics via `textDocument/publishDiagnostics`. A clean document publishes an empty array (clearing markers). Documents that are not configured inputs publish nothing.
|
|
21
31
|
4. **Formatting** — on `textDocument/formatting`, the server formats the current in-memory document text with `@prisma-next/psl-parser/format` when the document is a configured PSL input. It returns one whole-document edit when the formatted text differs, and returns no edits for missing or closed documents, unconfigured documents, already canonical text, malformed PSL, or invalid formatter options.
|
|
22
|
-
5. **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
document closes. They are exposed through `getDocumentAst` /
|
|
26
|
-
`getProjectSymbolTable` so future features (completion, semantic tokens) read
|
|
27
|
-
real stages instead of re-parsing. Filling the project table from several
|
|
28
|
-
inputs — and reading unopened inputs from disk — is deferred cross-file work.
|
|
32
|
+
5. **Folding ranges** — on `textDocument/foldingRange`, the server reads the preserved document AST for the configured input and returns foldable declaration/block ranges. Missing, unconfigured, or not-yet-parsed documents return an empty result.
|
|
33
|
+
6. **Semantic tokens** — on `textDocument/semanticTokens/full` and `textDocument/semanticTokens/range`, the server reads the current preserved `DocumentAst`, `SourceFile`, project `SymbolTable`, and control-stack scalar types from the same `ProjectArtifacts` lifecycle used by diagnostics. It classifies PSL keywords, declaration names, field/property names, type references, attributes, strings, numbers, booleans, and comments into standard token types/modifiers, then encodes them as LSP five-integer relative semantic-token data. The range request filters to intersecting tokens before encoding. Unconfigured, missing, closed, config-resolution-failed, oversized, or stale documents return `{ data: [] }` instead of throwing or reparsing through a semantic-token-specific path. Malformed PSL returns best-effort tokens from parser recovery when artifacts are available.
|
|
34
|
+
7. **Preserved artifacts** — each project keeps the parse artifacts it produces: the AST per open document (keyed by URI) and one symbol table per project, rebuilt from the open configured input on each edit and dropped when the document closes. Diagnostics populate these artifacts, and folding/semantic-token handlers read them instead of constructing independent parse/token caches. They are exposed through `getDocumentAst` / `getProjectSymbolTable` for future features. Filling the project table from several inputs — and reading unopened inputs from disk — is deferred cross-file work.
|
|
29
35
|
|
|
30
36
|
## Module layout
|
|
31
37
|
|
|
32
|
-
- `diagnostic-mapping.ts` — pure `ParseDiagnostic[] → LspDiagnostic[]` mapping.
|
|
33
|
-
|
|
34
|
-
(ranges pass through unchanged) so it stays reusable. The connection layer
|
|
35
|
-
adapts the numeric severity to the LSP enum.
|
|
36
|
-
- `schema-inputs.ts` — resolves the schema-input set (`SchemaInputSet`) from a
|
|
37
|
-
config and answers URI membership.
|
|
38
|
+
- `diagnostic-mapping.ts` — pure `ParseDiagnostic[] → LspDiagnostic[]` mapping. Free of any `vscode-languageserver` import; it returns plain shape objects (ranges pass through unchanged) so it stays reusable. The connection layer adapts the numeric severity to the LSP enum.
|
|
39
|
+
- `schema-inputs.ts` — resolves the schema-input set (`SchemaInputSet`) from a config and answers URI membership.
|
|
38
40
|
- `config-resolution.ts` — wraps `loadConfig` and resolves schema inputs, formatter options, and control-stack inputs for a config. A standalone async function so it can be re-run on a config change without rewiring the server.
|
|
39
41
|
- `document-diagnostics.ts` — `computeDocumentDiagnostics(uri, text, inputs, controlStack)`, the pure seam that parses, builds the symbol table, and returns the diagnostics plus the parse artifacts.
|
|
40
|
-
- `project-artifacts.ts` — `createProjectArtifacts()`, the per-project store that
|
|
41
|
-
|
|
42
|
-
- `
|
|
42
|
+
- `project-artifacts.ts` — `createProjectArtifacts()`, the per-project store that preserves the per-URI ASTs and the single project symbol table across edits.
|
|
43
|
+
- `folding-ranges.ts` — pure AST-to-LSP folding-range computation for declaration/block bodies.
|
|
44
|
+
- `semantic-tokens.ts` — pure PSL semantic-token collection, range filtering, multiline normalization, duplicate resolution, modifier bitset encoding, and LSP semantic-token data encoding.
|
|
45
|
+
- `server.ts` — `createServer(connection)` wires diagnostics, whole-document formatting, folding ranges, semantic-token handlers, config watching, and project-artifact access onto an injected connection.
|
|
43
46
|
- `start-server.ts` — `startServer()` creates a stdio connection and starts the server. This is what the CLI delegates to.
|
|
44
47
|
|
|
45
48
|
## Package Location
|
package/dist/exports/index.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/diagnostic-mapping.ts","../../src/project-artifacts.ts","../../src/server.ts","../../src/start-server.ts"],"mappings":";;;;cAEa,uBAAA;EAAA;;;;;UAOI,aAAA;EAAA,SACN,KAAA,EAAO,KAAK;EAAA,SACZ,OAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;AAAA;AAAA,iBAGK,mBAAA,CACd,WAAA,WAAsB,eAAA,cACZ,aAAa;;;UCXR,cAAA;EAAA,SACN,QAAA,EAAU,WAAA;EAAA,SACV,UAAA,EAAY,UAAU;AAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/diagnostic-mapping.ts","../../src/project-artifacts.ts","../../src/server.ts","../../src/start-server.ts"],"mappings":";;;;cAEa,uBAAA;EAAA;;;;;UAOI,aAAA;EAAA,SACN,KAAA,EAAO,KAAK;EAAA,SACZ,OAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;AAAA;AAAA,iBAGK,mBAAA,CACd,WAAA,WAAsB,eAAA,cACZ,aAAa;;;UCXR,cAAA;EAAA,SACN,QAAA,EAAU,WAAA;EAAA,SACV,UAAA,EAAY,UAAU;EAAA,SACtB,IAAA;AAAA;;;UCuBM,cAAA;EACf,OAAA;;AFhCF;;;EEqCE,cAAA,CAAe,GAAA,WAAc,cAAA;EAC7B,qBAAA,CAAsB,GAAA,WAAc,WAAW;AAAA;AAAA,iBAiBjC,YAAA,CAAa,UAAA,EAAY,UAAA,GAAa,cAAc;;;iBCtDpD,WAAA,IAAe,cAAc"}
|
package/dist/exports/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@ 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, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver";
|
|
5
|
+
import { DiagnosticSeverity, DidChangeWatchedFilesNotification, FoldingRangeKind, RegistrationRequest, SemanticTokenModifiers, SemanticTokenTypes, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver";
|
|
6
6
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
7
7
|
import { createControlStack } from "@prisma-next/framework-components/control";
|
|
8
|
-
import { NamespaceDeclarationAst, parse } from "@prisma-next/psl-parser/syntax";
|
|
8
|
+
import { ArrayLiteralAst, BooleanLiteralExprAst, CompositeTypeDeclarationAst, FieldDeclarationAst, FunctionCallAst, IdentifierAst, ModelDeclarationAst, NamespaceDeclarationAst, NumberLiteralExprAst, ObjectLiteralExprAst, StringLiteralExprAst, TypesBlockAst, filterChildren, parse } from "@prisma-next/psl-parser/syntax";
|
|
9
9
|
import { buildSymbolTable } from "@prisma-next/psl-parser";
|
|
10
10
|
import { ProposedFeatures, createConnection } from "vscode-languageserver/node";
|
|
11
11
|
//#region src/diagnostic-mapping.ts
|
|
@@ -157,7 +157,8 @@ function createProjectArtifacts() {
|
|
|
157
157
|
}
|
|
158
158
|
documents.set(uri, {
|
|
159
159
|
document: computed.document,
|
|
160
|
-
sourceFile: computed.sourceFile
|
|
160
|
+
sourceFile: computed.sourceFile,
|
|
161
|
+
text
|
|
161
162
|
});
|
|
162
163
|
symbolTable = computed.symbolTable;
|
|
163
164
|
return computed.diagnostics;
|
|
@@ -168,7 +169,344 @@ function createProjectArtifacts() {
|
|
|
168
169
|
};
|
|
169
170
|
}
|
|
170
171
|
//#endregion
|
|
172
|
+
//#region src/semantic-tokens.ts
|
|
173
|
+
const semanticTokenTypes = [
|
|
174
|
+
SemanticTokenTypes.keyword,
|
|
175
|
+
SemanticTokenTypes.namespace,
|
|
176
|
+
SemanticTokenTypes.class,
|
|
177
|
+
SemanticTokenTypes.struct,
|
|
178
|
+
SemanticTokenTypes.type,
|
|
179
|
+
SemanticTokenTypes.property,
|
|
180
|
+
SemanticTokenTypes.decorator,
|
|
181
|
+
SemanticTokenTypes.string,
|
|
182
|
+
SemanticTokenTypes.number,
|
|
183
|
+
SemanticTokenTypes.comment
|
|
184
|
+
];
|
|
185
|
+
const semanticTokenModifiers = [SemanticTokenModifiers.declaration, SemanticTokenModifiers.defaultLibrary];
|
|
186
|
+
const semanticTokenModifierIndexes = {
|
|
187
|
+
declaration: 0,
|
|
188
|
+
defaultLibrary: 1
|
|
189
|
+
};
|
|
190
|
+
const semanticTokenModifierBits = {
|
|
191
|
+
declaration: 1 << semanticTokenModifierIndexes.declaration,
|
|
192
|
+
defaultLibrary: 1 << semanticTokenModifierIndexes.defaultLibrary
|
|
193
|
+
};
|
|
194
|
+
const semanticTokensLegend = {
|
|
195
|
+
tokenTypes: [...semanticTokenTypes],
|
|
196
|
+
tokenModifiers: [...semanticTokenModifiers]
|
|
197
|
+
};
|
|
198
|
+
function buildSemanticTokens(source, range) {
|
|
199
|
+
const builder = new SemanticTokensBuilder(source.sourceFile, range);
|
|
200
|
+
for (const token of collectSemanticTokenEvents(source)) builder.add(token);
|
|
201
|
+
return builder.build();
|
|
202
|
+
}
|
|
203
|
+
function collectSemanticTokenEvents(source) {
|
|
204
|
+
const comments = collectCommentTokens(source.document);
|
|
205
|
+
const tokens = [];
|
|
206
|
+
collectDeclarations(source, tokens);
|
|
207
|
+
return mergeSourceOrderedTokens(comments, tokens);
|
|
208
|
+
}
|
|
209
|
+
var SemanticTokensBuilder = class {
|
|
210
|
+
#data = [];
|
|
211
|
+
#sourceFile;
|
|
212
|
+
#rangeOffsets;
|
|
213
|
+
#previousLine = 0;
|
|
214
|
+
#previousCharacter = 0;
|
|
215
|
+
#first = true;
|
|
216
|
+
constructor(sourceFile, range) {
|
|
217
|
+
this.#sourceFile = sourceFile;
|
|
218
|
+
if (range !== void 0) {
|
|
219
|
+
const startOffset = sourceFile.offsetAt(range.start);
|
|
220
|
+
const endOffset = sourceFile.offsetAt(range.end);
|
|
221
|
+
this.#rangeOffsets = {
|
|
222
|
+
lower: Math.min(startOffset, endOffset),
|
|
223
|
+
upper: Math.max(startOffset, endOffset)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
add(token) {
|
|
228
|
+
if (!this.#intersectsRange(token.startOffset, token.endOffset)) return;
|
|
229
|
+
if (token.splitMultiline) {
|
|
230
|
+
this.#addMultilineSplitToken(token);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.#encode(token.startOffset, token.endOffset, token.tokenTypeIndex, token.modifierBitset);
|
|
234
|
+
}
|
|
235
|
+
build() {
|
|
236
|
+
return { data: this.#data };
|
|
237
|
+
}
|
|
238
|
+
#intersectsRange(startOffset, endOffset) {
|
|
239
|
+
const rangeOffsets = this.#rangeOffsets;
|
|
240
|
+
return rangeOffsets === void 0 || startOffset < rangeOffsets.upper && endOffset > rangeOffsets.lower;
|
|
241
|
+
}
|
|
242
|
+
#addMultilineSplitToken(token) {
|
|
243
|
+
const start = this.#sourceFile.positionAt(token.startOffset);
|
|
244
|
+
const end = this.#sourceFile.positionAt(token.endOffset);
|
|
245
|
+
if (start.line === end.line) {
|
|
246
|
+
this.#encode(token.startOffset, token.endOffset, token.tokenTypeIndex, token.modifierBitset);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (let line = start.line; line <= end.line; line++) {
|
|
250
|
+
const startOffset = line === start.line ? token.startOffset : this.#sourceFile.lineStartOffset(line);
|
|
251
|
+
const endOffset = line === end.line ? token.endOffset : this.#sourceFile.lineEndOffset(line);
|
|
252
|
+
if (endOffset > startOffset && this.#intersectsRange(startOffset, endOffset)) this.#encode(startOffset, endOffset, token.tokenTypeIndex, token.modifierBitset);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
#encode(startOffset, endOffset, tokenTypeIndex, modifierBitset) {
|
|
256
|
+
const start = this.#sourceFile.positionAt(startOffset);
|
|
257
|
+
const deltaLine = this.#first ? start.line : start.line - this.#previousLine;
|
|
258
|
+
const deltaStart = this.#first || deltaLine !== 0 ? start.character : start.character - this.#previousCharacter;
|
|
259
|
+
this.#data.push(deltaLine, deltaStart, endOffset - startOffset, tokenTypeIndex, modifierBitset);
|
|
260
|
+
this.#previousLine = start.line;
|
|
261
|
+
this.#previousCharacter = start.character;
|
|
262
|
+
this.#first = false;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
function collectCommentTokens(document) {
|
|
266
|
+
const tokens = [];
|
|
267
|
+
for (const token of document.syntax.tokens()) if (token.kind === "Comment") tokens.push(pendingTokenForToken(token, "comment"));
|
|
268
|
+
return tokens;
|
|
269
|
+
}
|
|
270
|
+
function collectDeclarations(source, tokens) {
|
|
271
|
+
for (const declaration of source.document.declarations()) collectDeclaration(declaration, source, tokens, void 0);
|
|
272
|
+
}
|
|
273
|
+
function collectDeclaration(declaration, source, tokens, namespace) {
|
|
274
|
+
if (declaration instanceof ModelDeclarationAst) {
|
|
275
|
+
addToken(declaration.keyword(), "keyword", tokens);
|
|
276
|
+
addIdentifier(declaration.name(), "class", tokens, semanticTokenModifierBits.declaration);
|
|
277
|
+
collectBlockMembers(declaration.members(), source, tokens, namespace);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (declaration instanceof CompositeTypeDeclarationAst) {
|
|
281
|
+
addToken(declaration.keyword(), "keyword", tokens);
|
|
282
|
+
addIdentifier(declaration.name(), "struct", tokens, semanticTokenModifierBits.declaration);
|
|
283
|
+
collectBlockMembers(declaration.members(), source, tokens, namespace);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (declaration instanceof NamespaceDeclarationAst) {
|
|
287
|
+
addToken(declaration.keyword(), "keyword", tokens);
|
|
288
|
+
addIdentifier(declaration.name(), "namespace", tokens, semanticTokenModifierBits.declaration);
|
|
289
|
+
const nestedNamespace = declaration.name()?.name();
|
|
290
|
+
for (const nested of declaration.declarations()) collectDeclaration(nested, source, tokens, nestedNamespace);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (declaration instanceof TypesBlockAst) {
|
|
294
|
+
addToken(declaration.keyword(), "keyword", tokens);
|
|
295
|
+
for (const namedType of declaration.declarations()) collectNamedTypeDeclaration(namedType, source, tokens, namespace);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
addToken(declaration.keyword(), "keyword", tokens);
|
|
299
|
+
addIdentifier(declaration.name(), "type", tokens, semanticTokenModifierBits.declaration);
|
|
300
|
+
collectGenericBlockMembers(declaration.members(), source, tokens, namespace);
|
|
301
|
+
}
|
|
302
|
+
function collectNamedTypeDeclaration(declaration, source, tokens, namespace) {
|
|
303
|
+
addIdentifier(declaration.name(), "type", tokens, semanticTokenModifierBits.declaration);
|
|
304
|
+
collectTypeAnnotation(declaration.typeAnnotation(), source, tokens, namespace);
|
|
305
|
+
collectAttributes(declaration.attributes(), source, tokens, namespace);
|
|
306
|
+
}
|
|
307
|
+
function collectGenericBlockMembers(members, source, tokens, namespace) {
|
|
308
|
+
for (const member of members) {
|
|
309
|
+
if ("key" in member) {
|
|
310
|
+
addIdentifier(member.key(), "property", tokens);
|
|
311
|
+
collectExpression(member.value(), source, tokens, namespace);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
collectAttribute(member, source, tokens, namespace);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function collectBlockMembers(members, source, tokens, namespace) {
|
|
318
|
+
for (const member of members) {
|
|
319
|
+
if (member instanceof FieldDeclarationAst) {
|
|
320
|
+
collectField(member, source, tokens, namespace);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
collectAttribute(member, source, tokens, namespace);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function collectField(field, source, tokens, namespace) {
|
|
327
|
+
addIdentifier(field.name(), "property", tokens, semanticTokenModifierBits.declaration);
|
|
328
|
+
collectTypeAnnotation(field.typeAnnotation(), source, tokens, namespace);
|
|
329
|
+
collectAttributes(field.attributes(), source, tokens, namespace);
|
|
330
|
+
}
|
|
331
|
+
function collectTypeAnnotation(annotation, source, tokens, namespace) {
|
|
332
|
+
if (annotation === void 0) return;
|
|
333
|
+
collectTypeReference(annotation.name(), source, tokens, namespace);
|
|
334
|
+
collectAttributeArgList(annotation.argList(), source, tokens, namespace);
|
|
335
|
+
}
|
|
336
|
+
function collectAttributes(attributes, source, tokens, namespace) {
|
|
337
|
+
for (const attribute of attributes) collectAttribute(attribute, source, tokens, namespace);
|
|
338
|
+
}
|
|
339
|
+
function collectAttribute(attribute, source, tokens, namespace) {
|
|
340
|
+
collectDecoratorName(attribute.name(), source.sourceFile.text, tokens);
|
|
341
|
+
collectAttributeArgList(attribute.argList(), source, tokens, namespace);
|
|
342
|
+
}
|
|
343
|
+
function collectDecoratorName(name, sourceText, tokens) {
|
|
344
|
+
if (name === void 0) return;
|
|
345
|
+
const segments = identifierSegments(name);
|
|
346
|
+
for (const [index, segment] of segments.entries()) tokens.push(rangeForDecoratorIdentifier(segment.identifier, sourceText, index === 0));
|
|
347
|
+
}
|
|
348
|
+
function collectAttributeArgList(argList, source, tokens, namespace) {
|
|
349
|
+
if (argList === void 0) return;
|
|
350
|
+
for (const arg of argList.args()) collectAttributeArg(arg, source, tokens, namespace);
|
|
351
|
+
}
|
|
352
|
+
function collectAttributeArg(arg, source, tokens, namespace) {
|
|
353
|
+
const name = arg.name();
|
|
354
|
+
addIdentifier(name, "property", tokens);
|
|
355
|
+
collectExpression(arg.value(), source, tokens, namespace, expressionContextForAttributeArg(name));
|
|
356
|
+
}
|
|
357
|
+
function collectExpression(expression, source, tokens, namespace, context = {}) {
|
|
358
|
+
if (expression === void 0) return;
|
|
359
|
+
if (expression instanceof StringLiteralExprAst) {
|
|
360
|
+
addToken(expression.token(), "string", tokens);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (expression instanceof NumberLiteralExprAst) {
|
|
364
|
+
addToken(expression.token(), "number", tokens);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (expression instanceof BooleanLiteralExprAst) {
|
|
368
|
+
addToken(expression.token(), "keyword", tokens);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (expression instanceof FunctionCallAst) {
|
|
372
|
+
collectTypeReference(expression.name(), source, tokens, namespace);
|
|
373
|
+
for (const arg of expression.args()) collectAttributeArg(arg, source, tokens, namespace);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (expression instanceof ArrayLiteralAst) {
|
|
377
|
+
for (const element of expression.elements()) collectExpression(element, source, tokens, namespace, context);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (expression instanceof ObjectLiteralExprAst) {
|
|
381
|
+
for (const field of expression.fields()) {
|
|
382
|
+
addIdentifier(field.key(), "property", tokens);
|
|
383
|
+
collectExpression(field.value(), source, tokens, namespace);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
collectIdentifierExpression(expression, source, tokens, namespace, context);
|
|
388
|
+
}
|
|
389
|
+
function collectIdentifierExpression(identifier, source, tokens, namespace, context) {
|
|
390
|
+
const text = identifier.name();
|
|
391
|
+
if (text === void 0) return;
|
|
392
|
+
const bareIdentifierTokenType = context.bareIdentifierTokenType;
|
|
393
|
+
if (bareIdentifierTokenType !== void 0) {
|
|
394
|
+
tokens.push(rangeForIdentifier(identifier, bareIdentifierTokenType));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const classification = classifyTypeReference([text], source, namespace);
|
|
398
|
+
tokens.push(rangeForIdentifier(identifier, classification.tokenType, classification.modifierBitset));
|
|
399
|
+
}
|
|
400
|
+
function expressionContextForAttributeArg(name) {
|
|
401
|
+
const argName = name?.name();
|
|
402
|
+
return argName === "fields" || argName === "references" ? { bareIdentifierTokenType: "property" } : {};
|
|
403
|
+
}
|
|
404
|
+
function collectTypeReference(name, source, tokens, namespace) {
|
|
405
|
+
if (name === void 0) return;
|
|
406
|
+
const segments = identifierSegments(name);
|
|
407
|
+
if (segments.length === 0) return;
|
|
408
|
+
const path = segments.map((segment) => segment.text);
|
|
409
|
+
for (const segment of segments.slice(0, -1)) if (isKnownNamespace(segment.text, source.symbolTable)) tokens.push(rangeForIdentifier(segment.identifier, "namespace"));
|
|
410
|
+
const finalSegment = segments[segments.length - 1];
|
|
411
|
+
if (finalSegment === void 0) return;
|
|
412
|
+
const classification = classifyTypeReference(path, source, namespace);
|
|
413
|
+
tokens.push(rangeForIdentifier(finalSegment.identifier, classification.tokenType, classification.modifierBitset));
|
|
414
|
+
}
|
|
415
|
+
function classifyTypeReference(path, source, namespace) {
|
|
416
|
+
const name = path[path.length - 1];
|
|
417
|
+
if (name === void 0) return { tokenType: "type" };
|
|
418
|
+
const table = source.symbolTable;
|
|
419
|
+
const namespaceName = path.length > 1 ? path[path.length - 2] : namespace;
|
|
420
|
+
const namespaceScope = namespaceName !== void 0 ? table?.topLevel.namespaces[namespaceName] : void 0;
|
|
421
|
+
if (namespaceScope !== void 0) {
|
|
422
|
+
if (Object.hasOwn(namespaceScope.models, name)) return { tokenType: "class" };
|
|
423
|
+
if (Object.hasOwn(namespaceScope.compositeTypes, name)) return { tokenType: "struct" };
|
|
424
|
+
if (Object.hasOwn(namespaceScope.blocks, name)) return { tokenType: "type" };
|
|
425
|
+
}
|
|
426
|
+
if (table !== void 0) {
|
|
427
|
+
if (Object.hasOwn(table.topLevel.models, name)) return { tokenType: "class" };
|
|
428
|
+
if (Object.hasOwn(table.topLevel.compositeTypes, name)) return { tokenType: "struct" };
|
|
429
|
+
if (Object.hasOwn(table.topLevel.scalars, name)) return {
|
|
430
|
+
tokenType: "type",
|
|
431
|
+
modifierBitset: semanticTokenModifierBits.defaultLibrary
|
|
432
|
+
};
|
|
433
|
+
if (Object.hasOwn(table.topLevel.typeAliases, name) || Object.hasOwn(table.topLevel.blocks, name)) return { tokenType: "type" };
|
|
434
|
+
}
|
|
435
|
+
if (source.scalarTypes.includes(name)) return {
|
|
436
|
+
tokenType: "type",
|
|
437
|
+
modifierBitset: semanticTokenModifierBits.defaultLibrary
|
|
438
|
+
};
|
|
439
|
+
return { tokenType: "type" };
|
|
440
|
+
}
|
|
441
|
+
function isKnownNamespace(name, table) {
|
|
442
|
+
return table !== void 0 && Object.hasOwn(table.topLevel.namespaces, name);
|
|
443
|
+
}
|
|
444
|
+
function identifierSegments(name) {
|
|
445
|
+
const segments = [];
|
|
446
|
+
for (const identifier of filterChildren(name.syntax, IdentifierAst.cast)) {
|
|
447
|
+
const text = identifier.name();
|
|
448
|
+
if (text !== void 0) segments.push({
|
|
449
|
+
identifier,
|
|
450
|
+
text
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return segments;
|
|
454
|
+
}
|
|
455
|
+
function addIdentifier(identifier, tokenType, tokens, modifierBitset = 0) {
|
|
456
|
+
if (identifier === void 0) return;
|
|
457
|
+
tokens.push(rangeForIdentifier(identifier, tokenType, modifierBitset));
|
|
458
|
+
}
|
|
459
|
+
function addToken(token, tokenType, tokens, modifierBitset = 0) {
|
|
460
|
+
if (token === void 0) return;
|
|
461
|
+
tokens.push(pendingTokenForToken(token, tokenType, modifierBitset));
|
|
462
|
+
}
|
|
463
|
+
function rangeForIdentifier(identifier, tokenType, modifierBitset = 0) {
|
|
464
|
+
const token = identifier.token();
|
|
465
|
+
if (token === void 0) return createPendingSemanticToken(identifier.syntax.offset, identifier.syntax.offset, tokenType, modifierBitset);
|
|
466
|
+
return pendingTokenForToken(token, tokenType, modifierBitset);
|
|
467
|
+
}
|
|
468
|
+
function rangeForDecoratorIdentifier(identifier, sourceText, includePrefix) {
|
|
469
|
+
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);
|
|
474
|
+
}
|
|
475
|
+
function pendingTokenForToken(token, tokenType, modifierBitset = 0) {
|
|
476
|
+
return createPendingSemanticToken(token.offset, token.offset + token.text.length, tokenType, modifierBitset);
|
|
477
|
+
}
|
|
478
|
+
function mergeSourceOrderedTokens(left, right) {
|
|
479
|
+
const result = [];
|
|
480
|
+
let leftIndex = 0;
|
|
481
|
+
let rightIndex = 0;
|
|
482
|
+
while (leftIndex < left.length || rightIndex < right.length) {
|
|
483
|
+
const leftToken = left[leftIndex];
|
|
484
|
+
const rightToken = right[rightIndex];
|
|
485
|
+
if (leftToken !== void 0 && (rightToken === void 0 || leftToken.startOffset <= rightToken.startOffset)) {
|
|
486
|
+
result.push(leftToken);
|
|
487
|
+
leftIndex++;
|
|
488
|
+
} else if (rightToken !== void 0) {
|
|
489
|
+
result.push(rightToken);
|
|
490
|
+
rightIndex++;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
function createPendingSemanticToken(startOffset, endOffset, tokenType, modifierBitset = 0) {
|
|
496
|
+
return {
|
|
497
|
+
startOffset,
|
|
498
|
+
endOffset,
|
|
499
|
+
tokenTypeIndex: tokenTypeIndex(tokenType),
|
|
500
|
+
modifierBitset,
|
|
501
|
+
splitMultiline: tokenType === "string" || tokenType === "comment"
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function tokenTypeIndex(tokenType) {
|
|
505
|
+
return semanticTokenTypes.indexOf(tokenType);
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
171
508
|
//#region src/server.ts
|
|
509
|
+
const semanticTokenSourceLimit = 1e5;
|
|
172
510
|
function createServer(connection) {
|
|
173
511
|
const documents = new TextDocuments(TextDocument);
|
|
174
512
|
const projects = /* @__PURE__ */ new Map();
|
|
@@ -177,16 +515,15 @@ function createServer(connection) {
|
|
|
177
515
|
let rootPath = process.cwd();
|
|
178
516
|
let watchedConfigGlob = join(rootPath, "**", CONFIG_FILENAME);
|
|
179
517
|
let supportsWatchedFilesRegistration = false;
|
|
180
|
-
async function publish(uri
|
|
518
|
+
async function publish(uri) {
|
|
181
519
|
const project = await resolveProjectForDocument(uri);
|
|
182
520
|
if (project === void 0) return;
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
521
|
+
const document = documents.get(uri);
|
|
522
|
+
if (document === void 0) {
|
|
185
523
|
documentConfigPaths.delete(uri);
|
|
186
524
|
return;
|
|
187
525
|
}
|
|
188
|
-
|
|
189
|
-
const computed = project.artifacts.update(uri, text, project.inputs, project.controlStack);
|
|
526
|
+
const computed = project.artifacts.update(uri, document.getText(), project.inputs, project.controlStack);
|
|
190
527
|
if (computed === null) {
|
|
191
528
|
connection.sendDiagnostics({
|
|
192
529
|
uri,
|
|
@@ -208,7 +545,11 @@ function createServer(connection) {
|
|
|
208
545
|
}
|
|
209
546
|
async function resolveProjectForDocument(uri) {
|
|
210
547
|
const knownConfigPath = documentConfigPaths.get(uri);
|
|
211
|
-
if (knownConfigPath !== void 0)
|
|
548
|
+
if (knownConfigPath !== void 0) {
|
|
549
|
+
const project = await resolveProjectIfLoadable(knownConfigPath);
|
|
550
|
+
if (project === void 0) documentConfigPaths.delete(uri);
|
|
551
|
+
return project;
|
|
552
|
+
}
|
|
212
553
|
const filePath = filePathFromUri(uri);
|
|
213
554
|
if (filePath === void 0) return;
|
|
214
555
|
const configPath = await findNearestConfigPathForFile(filePath);
|
|
@@ -274,19 +615,19 @@ function createServer(connection) {
|
|
|
274
615
|
async function republishOpenDocumentsForConfig(configPath) {
|
|
275
616
|
for (const document of documents.all()) {
|
|
276
617
|
if (documentConfigPaths.get(document.uri) === configPath) {
|
|
277
|
-
await publish(document.uri
|
|
618
|
+
await publish(document.uri);
|
|
278
619
|
continue;
|
|
279
620
|
}
|
|
280
621
|
const filePath = filePathFromUri(document.uri);
|
|
281
622
|
if (filePath === void 0) continue;
|
|
282
623
|
if (await findNearestConfigPathForFile(filePath) === configPath) {
|
|
283
624
|
documentConfigPaths.set(document.uri, configPath);
|
|
284
|
-
await publish(document.uri
|
|
625
|
+
await publish(document.uri);
|
|
285
626
|
}
|
|
286
627
|
}
|
|
287
628
|
}
|
|
288
|
-
function publishSafely(uri
|
|
289
|
-
publish(uri
|
|
629
|
+
function publishSafely(uri) {
|
|
630
|
+
publish(uri).catch((error) => {
|
|
290
631
|
connection.console.error(error instanceof Error ? error.message : String(error));
|
|
291
632
|
});
|
|
292
633
|
}
|
|
@@ -319,6 +660,22 @@ function createServer(connection) {
|
|
|
319
660
|
newText: formatted
|
|
320
661
|
}];
|
|
321
662
|
}
|
|
663
|
+
async function semanticTokensForDocument(uri, range) {
|
|
664
|
+
const document = documents.get(uri);
|
|
665
|
+
if (document === void 0) return emptySemanticTokens();
|
|
666
|
+
const text = document.getText();
|
|
667
|
+
if (text.length > semanticTokenSourceLimit) return emptySemanticTokens();
|
|
668
|
+
const project = await resolveProjectForDocument(uri);
|
|
669
|
+
if (project === void 0) return emptySemanticTokens();
|
|
670
|
+
const cached = project.artifacts.getDocument(uri);
|
|
671
|
+
if (cached === void 0 || cached.text !== text) return emptySemanticTokens();
|
|
672
|
+
return buildSemanticTokens({
|
|
673
|
+
document: cached.document,
|
|
674
|
+
sourceFile: cached.sourceFile,
|
|
675
|
+
symbolTable: project.artifacts.getSymbolTable(),
|
|
676
|
+
scalarTypes: project.controlStack.scalarTypes
|
|
677
|
+
}, range);
|
|
678
|
+
}
|
|
322
679
|
connection.onInitialize(async (params) => {
|
|
323
680
|
rootPath = resolveRootPath(params.rootUri, params.rootPath);
|
|
324
681
|
watchedConfigGlob = join(rootPath, "**", CONFIG_FILENAME);
|
|
@@ -326,7 +683,12 @@ function createServer(connection) {
|
|
|
326
683
|
return { capabilities: {
|
|
327
684
|
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
328
685
|
documentFormattingProvider: true,
|
|
329
|
-
foldingRangeProvider: true
|
|
686
|
+
foldingRangeProvider: true,
|
|
687
|
+
semanticTokensProvider: {
|
|
688
|
+
legend: semanticTokensLegend,
|
|
689
|
+
full: true,
|
|
690
|
+
range: true
|
|
691
|
+
}
|
|
330
692
|
} };
|
|
331
693
|
});
|
|
332
694
|
connection.onInitialized(() => {
|
|
@@ -350,6 +712,8 @@ function createServer(connection) {
|
|
|
350
712
|
}
|
|
351
713
|
});
|
|
352
714
|
connection.onDocumentFormatting((params) => formatDocument(params.textDocument.uri));
|
|
715
|
+
connection.languages.semanticTokens.on((params) => semanticTokensForDocument(params.textDocument.uri));
|
|
716
|
+
connection.languages.semanticTokens.onRange((params) => semanticTokensForDocument(params.textDocument.uri, params.range));
|
|
353
717
|
connection.onFoldingRanges(async (params) => {
|
|
354
718
|
let project;
|
|
355
719
|
try {
|
|
@@ -363,10 +727,10 @@ function createServer(connection) {
|
|
|
363
727
|
return computeFoldingRanges(cached.document, cached.sourceFile);
|
|
364
728
|
});
|
|
365
729
|
documents.onDidOpen((event) => {
|
|
366
|
-
publishSafely(event.document.uri
|
|
730
|
+
publishSafely(event.document.uri);
|
|
367
731
|
});
|
|
368
732
|
documents.onDidChangeContent((event) => {
|
|
369
|
-
publishSafely(event.document.uri
|
|
733
|
+
publishSafely(event.document.uri);
|
|
370
734
|
});
|
|
371
735
|
documents.onDidClose((event) => {
|
|
372
736
|
const uri = event.document.uri;
|
|
@@ -392,6 +756,9 @@ function createServer(connection) {
|
|
|
392
756
|
getProjectSymbolTable: (uri) => artifactsForDocument(uri)?.getSymbolTable()
|
|
393
757
|
};
|
|
394
758
|
}
|
|
759
|
+
function emptySemanticTokens() {
|
|
760
|
+
return { data: [] };
|
|
761
|
+
}
|
|
395
762
|
function toLspSeverity(severity) {
|
|
396
763
|
switch (severity) {
|
|
397
764
|
case ParseDiagnosticSeverity.Warning: return DiagnosticSeverity.Warning;
|