@prisma-next/language-server 0.14.0-dev.21 → 0.14.0-dev.22

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 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. It speaks the Language Server Protocol over stdio and supports PSL parse diagnostics plus whole-document PSL formatting for the schema inputs declared in a project's `prisma-next.config.ts`. It is launched by the `prisma-next lsp` subcommand, so the editor features come from the project's own `@prisma-next` version (version-matched by construction).
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 and whole-document formatting 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. Hover, completion, navigation, range formatting, on-type formatting, 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.
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. **Preserved artifacts** — each project keeps the parse artifacts it produces:
23
- the AST per open document (keyed by URI) and one symbol table per project,
24
- rebuilt from the open configured input on each edit and dropped when the
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
- Free of any `vscode-languageserver` import; it returns plain shape objects
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
- preserves the per-URI ASTs and the single project symbol table across edits.
42
- - `server.ts` — `createServer(connection)` wires diagnostics and whole-document formatting handlers onto an injected connection.
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
@@ -20,6 +20,7 @@ declare function mapParseDiagnostics(diagnostics: readonly ParseDiagnostic[]): r
20
20
  interface CachedDocument {
21
21
  readonly document: DocumentAst;
22
22
  readonly sourceFile: SourceFile;
23
+ readonly text: string;
23
24
  }
24
25
  //#endregion
25
26
  //#region src/server.d.ts
@@ -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;;;UCqBhB,cAAA;EACf,OAAA;;AF7BF;;;EEkCE,cAAA,CAAe,GAAA,WAAc,cAAA;EAC7B,qBAAA,CAAsB,GAAA,WAAc,WAAW;AAAA;AAAA,iBAejC,YAAA,CAAa,UAAA,EAAY,UAAA,GAAa,cAAc;;;iBCjDpD,WAAA,IAAe,cAAc"}
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"}
@@ -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, text) {
518
+ async function publish(uri) {
181
519
  const project = await resolveProjectForDocument(uri);
182
520
  if (project === void 0) return;
183
- const currentDocument = documents.get(uri);
184
- if (currentDocument === void 0) {
521
+ const document = documents.get(uri);
522
+ if (document === void 0) {
185
523
  documentConfigPaths.delete(uri);
186
524
  return;
187
525
  }
188
- if (currentDocument.getText() !== text) return;
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) return resolveProject(knownConfigPath);
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, document.getText());
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, document.getText());
625
+ await publish(document.uri);
285
626
  }
286
627
  }
287
628
  }
288
- function publishSafely(uri, text) {
289
- publish(uri, text).catch((error) => {
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, event.document.getText());
730
+ publishSafely(event.document.uri);
367
731
  });
368
732
  documents.onDidChangeContent((event) => {
369
- publishSafely(event.document.uri, event.document.getText());
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;