treelsp 0.0.1

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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/dist/codegen/ast-types.d.ts +9 -0
  3. package/dist/codegen/grammar.d.ts +9 -0
  4. package/dist/codegen/highlights.d.ts +9 -0
  5. package/dist/codegen/index.d.ts +9 -0
  6. package/dist/codegen/index.js +983 -0
  7. package/dist/codegen/locals.d.ts +9 -0
  8. package/dist/codegen/server.d.ts +31 -0
  9. package/dist/defaults/completion.d.ts +18 -0
  10. package/dist/defaults/hover.d.ts +11 -0
  11. package/dist/defaults/index.d.ts +4 -0
  12. package/dist/defaults/symbols.d.ts +10 -0
  13. package/dist/defaults/validation.d.ts +19 -0
  14. package/dist/define.d.ts +25 -0
  15. package/dist/definition/grammar.d.ts +36 -0
  16. package/dist/definition/index.d.ts +33 -0
  17. package/dist/definition/lsp.d.ts +93 -0
  18. package/dist/definition/semantic.d.ts +80 -0
  19. package/dist/definition/validation.d.ts +62 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +148 -0
  22. package/dist/runtime/index.d.ts +19 -0
  23. package/dist/runtime/index.js +3 -0
  24. package/dist/runtime/lsp/completion.d.ts +24 -0
  25. package/dist/runtime/lsp/context.d.ts +59 -0
  26. package/dist/runtime/lsp/definition.d.ts +23 -0
  27. package/dist/runtime/lsp/diagnostics.d.ts +33 -0
  28. package/dist/runtime/lsp/documents.d.ts +55 -0
  29. package/dist/runtime/lsp/hover.d.ts +30 -0
  30. package/dist/runtime/lsp/references.d.ts +28 -0
  31. package/dist/runtime/lsp/rename.d.ts +50 -0
  32. package/dist/runtime/lsp/semantic-tokens.d.ts +32 -0
  33. package/dist/runtime/lsp/server.d.ts +52 -0
  34. package/dist/runtime/lsp/symbols.d.ts +36 -0
  35. package/dist/runtime/parser/index.d.ts +7 -0
  36. package/dist/runtime/parser/node.d.ts +185 -0
  37. package/dist/runtime/parser/tree.d.ts +153 -0
  38. package/dist/runtime/parser/wasm.d.ts +32 -0
  39. package/dist/runtime/scope/index.d.ts +7 -0
  40. package/dist/runtime/scope/resolver.d.ts +49 -0
  41. package/dist/runtime/scope/scope.d.ts +127 -0
  42. package/dist/runtime/scope/workspace.d.ts +104 -0
  43. package/dist/runtime-CnCx2QS4.js +1914 -0
  44. package/dist/server/index.d.ts +25 -0
  45. package/dist/server/index.js +230 -0
  46. package/package.json +64 -0
@@ -0,0 +1,1914 @@
1
+ import Parser from "web-tree-sitter";
2
+
3
+ //#region src/runtime/parser/node.ts
4
+ /**
5
+ * AST node wrapper providing a friendly API over Tree-sitter's SyntaxNode
6
+ *
7
+ * This class matches the usage patterns from DESIGN.md:
8
+ * - node.field('name') - access named fields
9
+ * - node.fields('params') - access repeated fields
10
+ * - node.field('type')?.text - optional chaining
11
+ * - node.hasChild('export') - check field existence
12
+ */
13
+ var ASTNode = class ASTNode {
14
+ /**
15
+ * Wrapped Tree-sitter SyntaxNode
16
+ * Kept private to force API usage
17
+ */
18
+ syntaxNode;
19
+ /**
20
+ * Source text provider (optional - for efficient text access)
21
+ * If not provided, uses syntaxNode.text which is slower
22
+ */
23
+ sourceProvider;
24
+ constructor(syntaxNode, sourceProvider) {
25
+ this.syntaxNode = syntaxNode;
26
+ this.sourceProvider = sourceProvider;
27
+ }
28
+ /**
29
+ * Internal accessor for other runtime modules
30
+ * Not part of the public API — prefixed with underscore
31
+ * @internal
32
+ */
33
+ get _syntaxNode() {
34
+ return this.syntaxNode;
35
+ }
36
+ /**
37
+ * Get single child by field name
38
+ * Returns null if field doesn't exist or has no value
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const name = node.field('name');
43
+ * if (name) {
44
+ * console.log(name.text);
45
+ * }
46
+ * ```
47
+ */
48
+ field(name) {
49
+ const child = this.syntaxNode.childForFieldName(name);
50
+ if (!child) return null;
51
+ return new ASTNode(child, this.sourceProvider);
52
+ }
53
+ /**
54
+ * Get all children for a field (handles repeated fields)
55
+ * Returns empty array if field doesn't exist
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const params = node.fields('params');
60
+ * for (const param of params) {
61
+ * console.log(param.field('name')?.text);
62
+ * }
63
+ * ```
64
+ */
65
+ fields(name) {
66
+ const children = this.syntaxNode.childrenForFieldName(name);
67
+ return children.map((child) => new ASTNode(child, this.sourceProvider));
68
+ }
69
+ /**
70
+ * Alias for field() - matches Tree-sitter API
71
+ * Provided for users familiar with Tree-sitter
72
+ */
73
+ childForFieldName(name) {
74
+ return this.field(name);
75
+ }
76
+ /**
77
+ * Check if node has a child with given field name
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const visibility = node.hasChild('export') ? 'public' : 'private';
82
+ * ```
83
+ */
84
+ hasChild(name) {
85
+ return this.syntaxNode.childForFieldName(name) !== null;
86
+ }
87
+ /**
88
+ * Stable node identity (from Tree-sitter SyntaxNode.id)
89
+ * Safe to use as Map key — same tree node always returns same id
90
+ */
91
+ get id() {
92
+ return this.syntaxNode.id;
93
+ }
94
+ /**
95
+ * Node type from grammar (e.g., 'variable_decl', 'identifier')
96
+ */
97
+ get type() {
98
+ return this.syntaxNode.type;
99
+ }
100
+ /**
101
+ * Source text for this node
102
+ * Uses sourceProvider if available for better performance
103
+ */
104
+ get text() {
105
+ if (this.sourceProvider) {
106
+ const source = this.sourceProvider();
107
+ return source.slice(this.syntaxNode.startIndex, this.syntaxNode.endIndex);
108
+ }
109
+ return this.syntaxNode.text;
110
+ }
111
+ /**
112
+ * Is this an error node? (parsing failed)
113
+ */
114
+ get isError() {
115
+ return this.syntaxNode.isError || this.syntaxNode.hasError;
116
+ }
117
+ /**
118
+ * Is this a missing node? (expected but not found)
119
+ */
120
+ get isMissing() {
121
+ return this.syntaxNode.isMissing;
122
+ }
123
+ /**
124
+ * Is this a named node? (appears in the grammar)
125
+ */
126
+ get isNamed() {
127
+ return this.syntaxNode.isNamed;
128
+ }
129
+ /**
130
+ * Parent node (null for root)
131
+ */
132
+ get parent() {
133
+ const parentNode = this.syntaxNode.parent;
134
+ if (!parentNode) return null;
135
+ return new ASTNode(parentNode, this.sourceProvider);
136
+ }
137
+ /**
138
+ * All children (including unnamed nodes like punctuation)
139
+ */
140
+ get children() {
141
+ return this.syntaxNode.children.map((child) => new ASTNode(child, this.sourceProvider));
142
+ }
143
+ /**
144
+ * Named children only (grammar rules, not literals)
145
+ */
146
+ get namedChildren() {
147
+ return this.syntaxNode.namedChildren.map((child) => new ASTNode(child, this.sourceProvider));
148
+ }
149
+ /**
150
+ * Get child by index
151
+ */
152
+ child(index) {
153
+ const child = this.syntaxNode.child(index);
154
+ if (!child) return null;
155
+ return new ASTNode(child, this.sourceProvider);
156
+ }
157
+ /**
158
+ * Get named child by index
159
+ */
160
+ namedChild(index) {
161
+ const child = this.syntaxNode.namedChild(index);
162
+ if (!child) return null;
163
+ return new ASTNode(child, this.sourceProvider);
164
+ }
165
+ /**
166
+ * Number of children
167
+ */
168
+ get childCount() {
169
+ return this.syntaxNode.childCount;
170
+ }
171
+ /**
172
+ * Number of named children
173
+ */
174
+ get namedChildCount() {
175
+ return this.syntaxNode.namedChildCount;
176
+ }
177
+ /**
178
+ * Convert Tree-sitter Point to LSP Position
179
+ */
180
+ toPosition(point) {
181
+ return {
182
+ line: point.row,
183
+ character: point.column
184
+ };
185
+ }
186
+ /**
187
+ * Start position (LSP format: 0-based line and character)
188
+ */
189
+ get startPosition() {
190
+ return this.toPosition(this.syntaxNode.startPosition);
191
+ }
192
+ /**
193
+ * End position (LSP format: 0-based line and character)
194
+ */
195
+ get endPosition() {
196
+ return this.toPosition(this.syntaxNode.endPosition);
197
+ }
198
+ /**
199
+ * Start byte offset in source
200
+ */
201
+ get startIndex() {
202
+ return this.syntaxNode.startIndex;
203
+ }
204
+ /**
205
+ * End byte offset in source
206
+ */
207
+ get endIndex() {
208
+ return this.syntaxNode.endIndex;
209
+ }
210
+ /**
211
+ * Find smallest descendant at byte offset
212
+ */
213
+ descendantForIndex(startIndex, endIndex) {
214
+ const node = endIndex !== void 0 ? this.syntaxNode.descendantForIndex(startIndex, endIndex) : this.syntaxNode.descendantForIndex(startIndex);
215
+ return new ASTNode(node, this.sourceProvider);
216
+ }
217
+ /**
218
+ * Find smallest descendant at position
219
+ */
220
+ descendantForPosition(startPosition, endPosition) {
221
+ const start = {
222
+ row: startPosition.line,
223
+ column: startPosition.character
224
+ };
225
+ const node = endPosition ? this.syntaxNode.descendantForPosition(start, {
226
+ row: endPosition.line,
227
+ column: endPosition.character
228
+ }) : this.syntaxNode.descendantForPosition(start);
229
+ return new ASTNode(node, this.sourceProvider);
230
+ }
231
+ /**
232
+ * Get all descendants of specific type(s)
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const vars = root.descendantsOfType('variable_decl');
237
+ * const allRefs = root.descendantsOfType(['identifier', 'name_ref']);
238
+ * ```
239
+ */
240
+ descendantsOfType(types, startPosition, endPosition) {
241
+ const start = startPosition ? {
242
+ row: startPosition.line,
243
+ column: startPosition.character
244
+ } : void 0;
245
+ const end = endPosition ? {
246
+ row: endPosition.line,
247
+ column: endPosition.character
248
+ } : void 0;
249
+ const nodes = this.syntaxNode.descendantsOfType(types, start, end);
250
+ return nodes.map((node) => new ASTNode(node, this.sourceProvider));
251
+ }
252
+ /**
253
+ * String representation (for debugging)
254
+ */
255
+ toString() {
256
+ return this.syntaxNode.toString();
257
+ }
258
+ /**
259
+ * Get S-expression representation of the syntax tree
260
+ */
261
+ toSExpression() {
262
+ return this.syntaxNode.toString();
263
+ }
264
+ };
265
+
266
+ //#endregion
267
+ //#region src/runtime/parser/wasm.ts
268
+ /**
269
+ * Global state - Parser.init() must be called exactly once
270
+ */
271
+ const state = {
272
+ initialized: false,
273
+ initPromise: null
274
+ };
275
+ /**
276
+ * Language cache - reuse loaded languages
277
+ */
278
+ const languageCache = {};
279
+ /**
280
+ * Detect runtime platform
281
+ */
282
+ function detectPlatform() {
283
+ const global = globalThis;
284
+ if (typeof global.window !== "undefined" && typeof global.document !== "undefined") return "browser";
285
+ if (typeof process !== "undefined" && process.versions?.node) return "node";
286
+ return "node";
287
+ }
288
+ /**
289
+ * Ensure Parser is initialized
290
+ * Safe to call multiple times - initialization happens only once
291
+ */
292
+ async function ensureInitialized() {
293
+ if (state.initialized) return;
294
+ if (state.initPromise) {
295
+ await state.initPromise;
296
+ return;
297
+ }
298
+ state.initPromise = (async () => {
299
+ const platform = detectPlatform();
300
+ if (platform === "browser") await Parser.init();
301
+ else await Parser.init();
302
+ state.initialized = true;
303
+ })();
304
+ await state.initPromise;
305
+ }
306
+ /**
307
+ * Load a Tree-sitter language from WASM file
308
+ * Languages are cached to avoid redundant loading
309
+ *
310
+ * @param wasmPath Path to grammar.wasm file
311
+ * @returns Tree-sitter Language instance
312
+ */
313
+ async function loadLanguage(wasmPath) {
314
+ await ensureInitialized();
315
+ const cached = languageCache[wasmPath];
316
+ if (cached) return cached;
317
+ try {
318
+ const language = await Parser.Language.load(wasmPath);
319
+ languageCache[wasmPath] = language;
320
+ return language;
321
+ } catch (error) {
322
+ throw new Error(`Failed to load Tree-sitter grammar from ${wasmPath}. Ensure the WASM file exists and is accessible. Run 'treelsp build' to generate the grammar. Original error: ${error instanceof Error ? error.message : String(error)}`);
323
+ }
324
+ }
325
+ /**
326
+ * Create a new Tree-sitter parser with the specified language
327
+ * This is the main entry point for creating parsers
328
+ *
329
+ * @param wasmPath Path to grammar.wasm file
330
+ * @returns Configured Parser instance ready to use
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const parser = await createParser('./generated/grammar.wasm');
335
+ * const tree = parser.parse('let x = 42;');
336
+ * ```
337
+ */
338
+ async function createParser(wasmPath) {
339
+ const language = await loadLanguage(wasmPath);
340
+ const parser = new Parser();
341
+ parser.setLanguage(language);
342
+ return parser;
343
+ }
344
+ /**
345
+ * Preload Parser initialization (optional)
346
+ * Useful for reducing latency of the first parse
347
+ */
348
+ async function preloadParser() {
349
+ await ensureInitialized();
350
+ }
351
+
352
+ //#endregion
353
+ //#region src/runtime/parser/tree.ts
354
+ /**
355
+ * Document state with incremental parsing
356
+ *
357
+ * V1 strategy (DESIGN.md lines 16-24):
358
+ * - Keystroke → Tree-sitter incremental CST reparse → full AST rebuild → full scope rebuild
359
+ * - Tree-sitter's CST reparse is incremental automatically (via oldTree)
360
+ * - AST and scope are full recompute (simple and correct)
361
+ * - DocumentState.update() owns the boundary, designed for v2 incremental upgrade
362
+ *
363
+ * Memory management:
364
+ * - Tree-sitter uses WASM memory that must be explicitly freed
365
+ * - Call dispose() when done with the document
366
+ */
367
+ var DocumentState = class {
368
+ /**
369
+ * Document metadata
370
+ */
371
+ metadata;
372
+ /**
373
+ * Current source text
374
+ */
375
+ sourceText;
376
+ /**
377
+ * Tree-sitter parser (owns the Language)
378
+ */
379
+ parser;
380
+ /**
381
+ * Current parse tree (kept for incremental parsing)
382
+ * Must be deleted when replaced to free WASM memory
383
+ */
384
+ tree = null;
385
+ /**
386
+ * Root AST node (cached after parse)
387
+ */
388
+ rootNode = null;
389
+ constructor(parser, metadata, initialText) {
390
+ this.parser = parser;
391
+ this.metadata = metadata;
392
+ this.sourceText = initialText;
393
+ this.reparse();
394
+ }
395
+ /**
396
+ * Parse or reparse the document
397
+ *
398
+ * V1 strategy: Always parse from scratch (no incremental CST reuse).
399
+ * Tree-sitter's incremental parsing requires calling tree.edit() with
400
+ * precise byte-offset edit info before passing the old tree. Without it,
401
+ * tree-sitter reuses old nodes at wrong positions, producing garbled ASTs.
402
+ * V2 will add proper edit tracking to enable incremental parsing.
403
+ */
404
+ reparse() {
405
+ const newTree = this.parser.parse(this.sourceText);
406
+ if (this.tree) this.tree.delete();
407
+ this.tree = newTree;
408
+ this.rootNode = new ASTNode(newTree.rootNode, () => this.sourceText);
409
+ }
410
+ /**
411
+ * Get root AST node
412
+ */
413
+ get root() {
414
+ if (!this.rootNode) throw new Error("Document has not been parsed");
415
+ return this.rootNode;
416
+ }
417
+ /**
418
+ * Update document with new text (full replacement)
419
+ *
420
+ * V1 API: Simple full text replacement
421
+ * Tree-sitter does incremental CST reparse automatically via oldTree
422
+ * We rebuild the full AST wrapper (simple and correct for v1)
423
+ *
424
+ * @param newText New source text
425
+ * @param newVersion Document version after update (optional, will auto-increment)
426
+ */
427
+ update(newText, newVersion) {
428
+ this.sourceText = newText;
429
+ if (newVersion !== void 0) this.metadata.version = newVersion;
430
+ else this.metadata.version++;
431
+ this.reparse();
432
+ }
433
+ /**
434
+ * Get current source text
435
+ */
436
+ get text() {
437
+ return this.sourceText;
438
+ }
439
+ /**
440
+ * Get document URI
441
+ */
442
+ get uri() {
443
+ return this.metadata.uri;
444
+ }
445
+ /**
446
+ * Get document version
447
+ */
448
+ get version() {
449
+ return this.metadata.version;
450
+ }
451
+ /**
452
+ * Get language ID
453
+ */
454
+ get languageId() {
455
+ return this.metadata.languageId;
456
+ }
457
+ /**
458
+ * Check if document has parse errors
459
+ * Tree-sitter is error-tolerant - always returns a tree
460
+ * Check this to detect ERROR nodes
461
+ */
462
+ get hasErrors() {
463
+ return this.root.isError;
464
+ }
465
+ /**
466
+ * Dispose resources (free WASM memory)
467
+ * Call this when done with the document
468
+ *
469
+ * @example
470
+ * ```typescript
471
+ * const doc = await createDocumentState(...);
472
+ * try {
473
+ * // Use document
474
+ * } finally {
475
+ * doc.dispose();
476
+ * }
477
+ * ```
478
+ */
479
+ dispose() {
480
+ if (this.tree) {
481
+ this.tree.delete();
482
+ this.tree = null;
483
+ }
484
+ this.rootNode = null;
485
+ }
486
+ };
487
+ /**
488
+ * Create a new document state
489
+ *
490
+ * @param wasmPath Path to grammar WASM file (e.g., './generated/grammar.wasm')
491
+ * @param metadata Document metadata
492
+ * @param initialText Initial document text
493
+ * @returns Promise<DocumentState>
494
+ *
495
+ * @example
496
+ * ```typescript
497
+ * const doc = await createDocumentState(
498
+ * './generated/grammar.wasm',
499
+ * { uri: 'file:///test.mylang', version: 1, languageId: 'mylang' },
500
+ * 'let x = 42;'
501
+ * );
502
+ *
503
+ * const root = doc.root;
504
+ * console.log(root.type); // 'program'
505
+ *
506
+ * doc.update('let y = 99;');
507
+ * console.log(doc.version); // 2
508
+ *
509
+ * doc.dispose(); // Free WASM memory
510
+ * ```
511
+ */
512
+ async function createDocumentState(wasmPath, metadata, initialText) {
513
+ const parser = await createParser(wasmPath);
514
+ return new DocumentState(parser, metadata, initialText);
515
+ }
516
+
517
+ //#endregion
518
+ //#region src/runtime/scope/scope.ts
519
+ /**
520
+ * Scope - manages declarations and name resolution
521
+ *
522
+ * Supports three scope kinds:
523
+ * - 'global': Root scope, one per document
524
+ * - 'lexical': Standard block scope, inherits from parent
525
+ * - 'isolated': No access to parent scope (module boundaries)
526
+ *
527
+ * Visibility:
528
+ * - 'public': Visible to parent and cross-file
529
+ * - 'private': Visible only within this scope
530
+ */
531
+ var Scope = class {
532
+ /**
533
+ * Scope kind
534
+ */
535
+ kind;
536
+ /**
537
+ * AST node that owns this scope (null for global scope)
538
+ */
539
+ node;
540
+ /**
541
+ * Parent scope (null for global scope)
542
+ */
543
+ parent;
544
+ /**
545
+ * Declarations in this scope
546
+ * Maps name → array of declarations (for declaration merging in v2)
547
+ */
548
+ declarations = /* @__PURE__ */ new Map();
549
+ /**
550
+ * Child scopes
551
+ */
552
+ children = [];
553
+ constructor(kind, node, parent) {
554
+ this.kind = kind;
555
+ this.node = node;
556
+ this.parent = parent;
557
+ if (parent) parent.children.push(this);
558
+ }
559
+ /**
560
+ * Add a declaration to this scope
561
+ *
562
+ * V1: Single declarations per name (declaration merging in v2)
563
+ * If name already exists, adds to array (for future merging support)
564
+ */
565
+ declare(name, node, declaredBy, visibility) {
566
+ const decl = {
567
+ node,
568
+ name,
569
+ visibility,
570
+ declaredBy
571
+ };
572
+ const existing = this.declarations.get(name);
573
+ if (existing) existing.push(decl);
574
+ else this.declarations.set(name, [decl]);
575
+ return decl;
576
+ }
577
+ /**
578
+ * Look up a name in this scope only (does not search parent)
579
+ *
580
+ * @param name Name to look up
581
+ * @param declaredBy Optional filter by declaration type(s)
582
+ * @param visibility Optional filter by visibility
583
+ * @returns First matching declaration or null
584
+ */
585
+ lookupLocal(name, declaredBy, visibility) {
586
+ const declarations = this.declarations.get(name);
587
+ if (!declarations) return null;
588
+ let filtered = declarations;
589
+ if (declaredBy) {
590
+ const types = Array.isArray(declaredBy) ? declaredBy : [declaredBy];
591
+ filtered = declarations.filter((d) => types.includes(d.declaredBy));
592
+ }
593
+ if (visibility) filtered = filtered.filter((d) => d.visibility === visibility);
594
+ return filtered[0] ?? null;
595
+ }
596
+ /**
597
+ * Look up a name in this scope and parent scopes
598
+ *
599
+ * Respects scope kinds:
600
+ * - 'global': No parent to search
601
+ * - 'lexical': Searches parent recursively
602
+ * - 'isolated': Does not search parent
603
+ *
604
+ * @param name Name to look up
605
+ * @param declaredBy Optional filter by declaration type(s)
606
+ * @returns First matching declaration or null
607
+ */
608
+ lookup(name, declaredBy) {
609
+ const local = this.lookupLocal(name, declaredBy);
610
+ if (local) return local;
611
+ if (this.kind === "isolated") return null;
612
+ if (this.parent) return this.parent.lookup(name, declaredBy);
613
+ return null;
614
+ }
615
+ /**
616
+ * Get all declarations in this scope
617
+ *
618
+ * @param options Filtering options
619
+ * @returns Array of declarations
620
+ */
621
+ allDeclarations(options) {
622
+ const all = [];
623
+ for (const declarations of this.declarations.values()) all.push(...declarations);
624
+ let filtered = all;
625
+ if (options?.visibility) filtered = filtered.filter((d) => d.visibility === options.visibility);
626
+ if (options?.declaredBy) {
627
+ const types = Array.isArray(options.declaredBy) ? options.declaredBy : [options.declaredBy];
628
+ filtered = filtered.filter((d) => types.includes(d.declaredBy));
629
+ }
630
+ return filtered;
631
+ }
632
+ /**
633
+ * Check if a name is declared in this scope (local only)
634
+ */
635
+ isDeclared(name, declaredBy) {
636
+ return this.lookupLocal(name, declaredBy) !== null;
637
+ }
638
+ /**
639
+ * Get the global scope (root of scope tree)
640
+ */
641
+ global() {
642
+ let scope = this;
643
+ while (scope.parent) scope = scope.parent;
644
+ return scope;
645
+ }
646
+ /**
647
+ * Get all child scopes
648
+ */
649
+ getChildren() {
650
+ return [...this.children];
651
+ }
652
+ /**
653
+ * Get all descendant scopes (recursive)
654
+ */
655
+ descendants() {
656
+ const result = [];
657
+ for (const child of this.children) {
658
+ result.push(child);
659
+ result.push(...child.descendants());
660
+ }
661
+ return result;
662
+ }
663
+ /**
664
+ * Debug string representation
665
+ */
666
+ toString() {
667
+ const nodeType = this.node?.type ?? "document";
668
+ const declCount = Array.from(this.declarations.values()).flat().length;
669
+ return `Scope(${this.kind}, ${nodeType}, ${declCount} decls)`;
670
+ }
671
+ };
672
+
673
+ //#endregion
674
+ //#region src/runtime/scope/resolver.ts
675
+ /**
676
+ * Build scope tree for a document
677
+ *
678
+ * V1 strategy: Full rebuild on every document change
679
+ * - Walk AST to create scope tree
680
+ * - Register all declarations
681
+ * - Register all references
682
+ * - Resolve references to declarations
683
+ *
684
+ * @param document The document to analyze
685
+ * @param semantic Semantic definition from defineLanguage()
686
+ * @returns DocumentScope with scope tree and resolution data
687
+ */
688
+ function buildScopes(document, semantic) {
689
+ const root = document.root;
690
+ const nodeScopes = /* @__PURE__ */ new Map();
691
+ const references = [];
692
+ const declaredNodeIds = /* @__PURE__ */ new Set();
693
+ const context = {
694
+ scopeOf: (node) => {
695
+ let current = node;
696
+ while (current) {
697
+ const scope = nodeScopes.get(current.id);
698
+ if (scope) return scope;
699
+ current = current.parent;
700
+ }
701
+ return null;
702
+ },
703
+ resolveModule: () => null,
704
+ getReferences: () => references,
705
+ getDeclarations: () => {
706
+ const all = [];
707
+ for (const scope of nodeScopes.values()) all.push(...scope.allDeclarations());
708
+ return all;
709
+ }
710
+ };
711
+ const globalScope = new Scope("global", root, null);
712
+ nodeScopes.set(root.id, globalScope);
713
+ walkTree(root, globalScope, semantic, nodeScopes, references, context, declaredNodeIds);
714
+ for (const ref of references) ref.resolved = resolveReference(ref, context);
715
+ const declarations = context.getDeclarations();
716
+ return {
717
+ root: globalScope,
718
+ nodeScopes,
719
+ references,
720
+ declarations
721
+ };
722
+ }
723
+ /**
724
+ * Walk AST tree to build scopes and register declarations/references
725
+ */
726
+ function walkTree(node, currentScope, semantic, nodeScopes, references, context, declaredNodeIds) {
727
+ const rule = semantic[node.type];
728
+ let nodeScope = currentScope;
729
+ if (rule?.scope) {
730
+ nodeScope = new Scope(rule.scope, node, currentScope);
731
+ nodeScopes.set(node.id, nodeScope);
732
+ }
733
+ if (rule) {
734
+ processDeclarations(node, rule, nodeScope, semantic, nodeScopes, context, declaredNodeIds);
735
+ processReferences(node, rule, nodeScope, references, context, declaredNodeIds);
736
+ }
737
+ for (const child of node.namedChildren) walkTree(child, nodeScope, semantic, nodeScopes, references, context, declaredNodeIds);
738
+ }
739
+ /**
740
+ * Process declarations for a node
741
+ */
742
+ function processDeclarations(node, rule, _scope, _semantic, nodeScopes, context, declaredNodeIds) {
743
+ if (!rule.declares) return;
744
+ const descriptor = rule.declares;
745
+ const nameNode = node.field(descriptor.field);
746
+ if (!nameNode) return;
747
+ declaredNodeIds.add(nameNode.id);
748
+ const name = nameNode.text;
749
+ const visibility = getVisibility(node, descriptor);
750
+ const targetScope = getTargetScope(node, descriptor.scope, nodeScopes);
751
+ if (!targetScope) return;
752
+ const strategy = descriptor.strategy ?? "if-not-declared";
753
+ if (strategy === "if-not-declared") {
754
+ if (targetScope.isDeclared(name)) return;
755
+ }
756
+ if (descriptor.resolve) {
757
+ const resolved = descriptor.resolve({
758
+ node,
759
+ name,
760
+ text: name
761
+ }, context);
762
+ if (!resolved) return;
763
+ }
764
+ targetScope.declare(name, nameNode, node.type, visibility);
765
+ }
766
+ /**
767
+ * Process references for a node
768
+ */
769
+ function processReferences(node, rule, _scope, references, context, declaredNodeIds) {
770
+ if (!rule.references) return;
771
+ const descriptor = rule.references;
772
+ const nameNode = node.field(descriptor.field) ?? node;
773
+ if (declaredNodeIds.has(nameNode.id)) return;
774
+ const name = nameNode.text;
775
+ const to = Array.isArray(descriptor.to) ? descriptor.to : [descriptor.to];
776
+ const ref = {
777
+ node: nameNode,
778
+ name,
779
+ to,
780
+ resolved: null
781
+ };
782
+ if (descriptor.resolve) {
783
+ const resolved = descriptor.resolve({
784
+ node: nameNode,
785
+ name,
786
+ text: name
787
+ }, context);
788
+ if (resolved) ref.resolved = resolved;
789
+ }
790
+ references.push(ref);
791
+ }
792
+ /**
793
+ * Resolve a reference to its declaration
794
+ */
795
+ function resolveReference(ref, context) {
796
+ if (ref.resolved) return ref.resolved;
797
+ const scope = context.scopeOf(ref.node);
798
+ if (!scope) return null;
799
+ const decl = scope.lookup(ref.name, ref.to);
800
+ return decl;
801
+ }
802
+ /**
803
+ * Get visibility for a declaration
804
+ */
805
+ function getVisibility(node, descriptor) {
806
+ if (!descriptor.visibility) return "private";
807
+ if (typeof descriptor.visibility === "function") return descriptor.visibility(node);
808
+ return descriptor.visibility;
809
+ }
810
+ /**
811
+ * Get target scope for a declaration
812
+ */
813
+ function getTargetScope(node, target, nodeScopes) {
814
+ if (target === "global") {
815
+ let current = node;
816
+ while (current) {
817
+ const scope = nodeScopes.get(current.id);
818
+ if (scope?.kind === "global") return scope;
819
+ current = current.parent;
820
+ }
821
+ return null;
822
+ }
823
+ if (target === "enclosing") {
824
+ let current = node.parent;
825
+ while (current) {
826
+ const scope = nodeScopes.get(current.id);
827
+ if (scope) return scope;
828
+ current = current.parent;
829
+ }
830
+ return null;
831
+ }
832
+ if (target === "local") {
833
+ let current = node.parent;
834
+ while (current) {
835
+ const scope = nodeScopes.get(current.id);
836
+ if (scope) return scope;
837
+ current = current.parent;
838
+ }
839
+ return null;
840
+ }
841
+ return null;
842
+ }
843
+
844
+ //#endregion
845
+ //#region src/runtime/scope/workspace.ts
846
+ /**
847
+ * Workspace - manages multiple documents and cross-file resolution
848
+ *
849
+ * V1: Basic cross-file index for public declarations
850
+ * - Stores DocumentScope for each file
851
+ * - Provides lookup by URI
852
+ * - Exports public declarations for cross-file resolution
853
+ *
854
+ * V2: Will add module resolution, import tracking, circular dependency detection
855
+ */
856
+ var Workspace = class {
857
+ /**
858
+ * Semantic definition (shared across all documents)
859
+ */
860
+ semantic;
861
+ /**
862
+ * Documents indexed by URI
863
+ */
864
+ documents = /* @__PURE__ */ new Map();
865
+ /**
866
+ * Index of public declarations by name (for cross-file lookup)
867
+ * Maps name → array of declarations from all files
868
+ */
869
+ publicDeclarations = /* @__PURE__ */ new Map();
870
+ constructor(semantic) {
871
+ this.semantic = semantic;
872
+ }
873
+ /**
874
+ * Add or update a document in the workspace
875
+ *
876
+ * @param document The document to add/update
877
+ * @returns The DocumentScope for this document
878
+ */
879
+ addDocument(document) {
880
+ const scope = buildScopes(document, this.semantic);
881
+ this.documents.set(document.uri, {
882
+ document,
883
+ scope
884
+ });
885
+ this.rebuildPublicIndex();
886
+ return scope;
887
+ }
888
+ /**
889
+ * Remove a document from the workspace
890
+ */
891
+ removeDocument(uri) {
892
+ this.documents.delete(uri);
893
+ this.rebuildPublicIndex();
894
+ }
895
+ /**
896
+ * Get a document by URI
897
+ */
898
+ getDocument(uri) {
899
+ return this.documents.get(uri) ?? null;
900
+ }
901
+ /**
902
+ * Get all documents in the workspace
903
+ */
904
+ getAllDocuments() {
905
+ return Array.from(this.documents.values());
906
+ }
907
+ /**
908
+ * Look up a public declaration by name across all files
909
+ *
910
+ * @param name The name to look up
911
+ * @param declaredBy Optional filter by declaration type(s)
912
+ * @returns Array of matching declarations from all files
913
+ */
914
+ lookupPublic(name, declaredBy) {
915
+ const declarations = this.publicDeclarations.get(name);
916
+ if (!declarations) return [];
917
+ if (declaredBy) {
918
+ const types = Array.isArray(declaredBy) ? declaredBy : [declaredBy];
919
+ return declarations.filter((d) => types.includes(d.declaredBy));
920
+ }
921
+ return declarations;
922
+ }
923
+ /**
924
+ * Get all public declarations in the workspace
925
+ */
926
+ getAllPublicDeclarations() {
927
+ const all = [];
928
+ for (const declarations of this.publicDeclarations.values()) all.push(...declarations);
929
+ return all;
930
+ }
931
+ /**
932
+ * Rebuild the public declarations index
933
+ * Called after any document is added/removed/updated
934
+ */
935
+ rebuildPublicIndex() {
936
+ this.publicDeclarations.clear();
937
+ for (const { scope } of this.documents.values()) {
938
+ const publicDecls = scope.root.allDeclarations({ visibility: "public" });
939
+ for (const decl of publicDecls) {
940
+ const existing = this.publicDeclarations.get(decl.name);
941
+ if (existing) existing.push(decl);
942
+ else this.publicDeclarations.set(decl.name, [decl]);
943
+ }
944
+ }
945
+ }
946
+ /**
947
+ * Find circular imports (placeholder for v2)
948
+ *
949
+ * V1: Not implemented
950
+ * V2: Will track import graph and detect cycles
951
+ *
952
+ * @returns Array of circular import chains
953
+ */
954
+ findCircularImports() {
955
+ return [];
956
+ }
957
+ /**
958
+ * Get workspace statistics (for debugging)
959
+ */
960
+ getStats() {
961
+ let totalDecls = 0;
962
+ let totalRefs = 0;
963
+ for (const { scope } of this.documents.values()) {
964
+ totalDecls += scope.declarations.length;
965
+ totalRefs += scope.references.length;
966
+ }
967
+ return {
968
+ documentCount: this.documents.size,
969
+ publicDeclarationCount: this.getAllPublicDeclarations().length,
970
+ totalDeclarationCount: totalDecls,
971
+ totalReferenceCount: totalRefs
972
+ };
973
+ }
974
+ /**
975
+ * Clear all documents from the workspace
976
+ */
977
+ clear() {
978
+ this.documents.clear();
979
+ this.publicDeclarations.clear();
980
+ }
981
+ };
982
+
983
+ //#endregion
984
+ //#region src/runtime/lsp/documents.ts
985
+ /**
986
+ * Document manager for LSP
987
+ *
988
+ * Manages the lifecycle of open documents:
989
+ * - Tracks open/change/close events
990
+ * - Rebuilds scopes on document changes
991
+ * - Maintains workspace index
992
+ */
993
+ var DocumentManager = class {
994
+ workspace;
995
+ constructor(semantic) {
996
+ this.workspace = new Workspace(semantic);
997
+ }
998
+ /**
999
+ * Open or update a document
1000
+ *
1001
+ * @param document The document state (already parsed)
1002
+ * @returns The computed scope for this document
1003
+ */
1004
+ open(document) {
1005
+ return this.workspace.addDocument(document);
1006
+ }
1007
+ /**
1008
+ * Handle a document change
1009
+ * The caller should have already called document.update() with new text
1010
+ *
1011
+ * @param document The updated document state
1012
+ * @returns The recomputed scope
1013
+ */
1014
+ change(document) {
1015
+ return this.workspace.addDocument(document);
1016
+ }
1017
+ /**
1018
+ * Close a document
1019
+ */
1020
+ close(uri) {
1021
+ this.workspace.removeDocument(uri);
1022
+ }
1023
+ /**
1024
+ * Get document by URI
1025
+ */
1026
+ get(uri) {
1027
+ return this.workspace.getDocument(uri);
1028
+ }
1029
+ /**
1030
+ * Get the workspace instance
1031
+ */
1032
+ getWorkspace() {
1033
+ return this.workspace;
1034
+ }
1035
+ /**
1036
+ * Get all open documents
1037
+ */
1038
+ getAllDocuments() {
1039
+ return this.workspace.getAllDocuments();
1040
+ }
1041
+ /**
1042
+ * Clear all documents
1043
+ */
1044
+ clear() {
1045
+ this.workspace.clear();
1046
+ }
1047
+ };
1048
+
1049
+ //#endregion
1050
+ //#region src/runtime/lsp/context.ts
1051
+ /**
1052
+ * Create LspContext for handler use
1053
+ */
1054
+ function createLspContext(docScope, workspace, document, _semantic) {
1055
+ return {
1056
+ resolve(node) {
1057
+ const ref = findReferenceForNode(node, docScope);
1058
+ return ref?.resolved?.node ?? null;
1059
+ },
1060
+ typeOf(_node) {
1061
+ return null;
1062
+ },
1063
+ scopeOf(node) {
1064
+ return findScopeForNode(node, docScope);
1065
+ },
1066
+ document,
1067
+ workspace
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Find the smallest named node at a position
1072
+ *
1073
+ * When the cursor is at the exclusive end of a token (e.g., right edge of an
1074
+ * identifier), tree-sitter returns the parent node because that position falls
1075
+ * in whitespace. In that case, look one character back to find the token the
1076
+ * user likely intended.
1077
+ *
1078
+ * Similarly, when the cursor lands on an anonymous node (punctuation like ";",
1079
+ * "+", "="), look one character back to find the named token the user likely
1080
+ * intended. This handles the common case of clicking at the boundary between
1081
+ * an identifier and adjacent punctuation.
1082
+ */
1083
+ function findNodeAtPosition(root, position) {
1084
+ const node = root.descendantForPosition(position);
1085
+ if (position.character > 0 && (node.namedChildCount > 0 || !node.isNamed)) {
1086
+ const prev = root.descendantForPosition({
1087
+ line: position.line,
1088
+ character: position.character - 1
1089
+ });
1090
+ if (prev.namedChildCount === 0 && prev.isNamed) return prev;
1091
+ }
1092
+ return node;
1093
+ }
1094
+ /**
1095
+ * Find the reference entry for a node (by position match)
1096
+ */
1097
+ function findReferenceForNode(node, docScope) {
1098
+ const startLine = node.startPosition.line;
1099
+ const startChar = node.startPosition.character;
1100
+ const endLine = node.endPosition.line;
1101
+ const endChar = node.endPosition.character;
1102
+ for (const ref of docScope.references) {
1103
+ const refStart = ref.node.startPosition;
1104
+ const refEnd = ref.node.endPosition;
1105
+ if (refStart.line === startLine && refStart.character === startChar && refEnd.line === endLine && refEnd.character === endChar) return ref;
1106
+ }
1107
+ return null;
1108
+ }
1109
+ /**
1110
+ * Find the declaration entry for a node (by position match)
1111
+ */
1112
+ function findDeclarationForNode(node, docScope) {
1113
+ const startLine = node.startPosition.line;
1114
+ const startChar = node.startPosition.character;
1115
+ for (const decl of docScope.declarations) {
1116
+ const declStart = decl.node.startPosition;
1117
+ if (declStart.line === startLine && declStart.character === startChar) return decl;
1118
+ }
1119
+ return null;
1120
+ }
1121
+ /**
1122
+ * Find the scope that contains a node
1123
+ * Walks up the parent chain checking nodeScopes map
1124
+ */
1125
+ function findScopeForNode(node, docScope) {
1126
+ let current = node;
1127
+ while (current) {
1128
+ const scope = docScope.nodeScopes.get(current.id);
1129
+ if (scope) return scope;
1130
+ current = current.parent;
1131
+ }
1132
+ return docScope.root;
1133
+ }
1134
+ /**
1135
+ * Convert an ASTNode to an LSP-style range
1136
+ */
1137
+ function nodeToRange(node) {
1138
+ return {
1139
+ start: node.startPosition,
1140
+ end: node.endPosition
1141
+ };
1142
+ }
1143
+
1144
+ //#endregion
1145
+ //#region src/runtime/lsp/diagnostics.ts
1146
+ /**
1147
+ * Compute all diagnostics for a document
1148
+ */
1149
+ function computeDiagnostics(document, docScope, semantic, lsp, validation, workspace) {
1150
+ const diagnostics = [];
1151
+ collectParseErrors(document.root, diagnostics);
1152
+ collectUnresolvedReferences(docScope, semantic, lsp, diagnostics);
1153
+ if (validation) collectValidationDiagnostics(document, docScope, semantic, validation, workspace, diagnostics);
1154
+ return diagnostics;
1155
+ }
1156
+ /**
1157
+ * Walk AST to find ERROR and MISSING nodes
1158
+ */
1159
+ function collectParseErrors(node, diagnostics) {
1160
+ if (node.isMissing) {
1161
+ diagnostics.push({
1162
+ range: {
1163
+ start: node.startPosition,
1164
+ end: node.endPosition
1165
+ },
1166
+ severity: "error",
1167
+ message: `Missing ${node.type}`,
1168
+ code: "missing-node",
1169
+ source: "treelsp"
1170
+ });
1171
+ return;
1172
+ }
1173
+ if (node.isError) {
1174
+ const hasErrorChild = node.children.some((c) => c.isError || c.isMissing);
1175
+ if (!hasErrorChild) diagnostics.push({
1176
+ range: {
1177
+ start: node.startPosition,
1178
+ end: node.endPosition
1179
+ },
1180
+ severity: "error",
1181
+ message: "Syntax error",
1182
+ code: "syntax-error",
1183
+ source: "treelsp"
1184
+ });
1185
+ }
1186
+ for (const child of node.children) collectParseErrors(child, diagnostics);
1187
+ }
1188
+ /**
1189
+ * Report unresolved references based on semantic onUnresolved policy
1190
+ */
1191
+ function collectUnresolvedReferences(docScope, semantic, lsp, diagnostics) {
1192
+ for (const ref of docScope.references) {
1193
+ if (ref.resolved) continue;
1194
+ const refNode = ref.node;
1195
+ let parentNode = refNode.parent;
1196
+ let policy = "error";
1197
+ let optional = false;
1198
+ while (parentNode) {
1199
+ const rule = semantic[parentNode.type];
1200
+ if (rule?.references) {
1201
+ policy = rule.references.onUnresolved ?? "error";
1202
+ optional = rule.references.optional ?? false;
1203
+ break;
1204
+ }
1205
+ parentNode = parentNode.parent;
1206
+ }
1207
+ if (optional || policy === "ignore") continue;
1208
+ const severity = policy === "warning" ? "warning" : "error";
1209
+ let message = `Cannot find name '${ref.name}'`;
1210
+ if (lsp?.$unresolved) {
1211
+ const ctx = {
1212
+ resolve: () => null,
1213
+ typeOf: () => null,
1214
+ scopeOf: () => docScope.root,
1215
+ document: null,
1216
+ workspace: null
1217
+ };
1218
+ const custom = lsp.$unresolved(refNode, ctx);
1219
+ if (custom) message = custom;
1220
+ }
1221
+ diagnostics.push({
1222
+ range: {
1223
+ start: refNode.startPosition,
1224
+ end: refNode.endPosition
1225
+ },
1226
+ severity,
1227
+ message,
1228
+ code: "unresolved-reference",
1229
+ source: "treelsp"
1230
+ });
1231
+ }
1232
+ }
1233
+ /**
1234
+ * Run custom validators and collect their diagnostics
1235
+ */
1236
+ function collectValidationDiagnostics(document, docScope, semantic, validation, workspace, diagnostics) {
1237
+ const lspContext = createLspContext(docScope, workspace ?? {}, document, semantic);
1238
+ function createValidationContext(node) {
1239
+ return {
1240
+ error(target, message, options) {
1241
+ addValidationDiagnostic("error", target, message, options, diagnostics);
1242
+ },
1243
+ warning(target, message, options) {
1244
+ addValidationDiagnostic("warning", target, message, options, diagnostics);
1245
+ },
1246
+ info(target, message, options) {
1247
+ addValidationDiagnostic("info", target, message, options, diagnostics);
1248
+ },
1249
+ hint(target, message, options) {
1250
+ addValidationDiagnostic("hint", target, message, options, diagnostics);
1251
+ },
1252
+ resolve: (n) => lspContext.resolve(n),
1253
+ scopeOf: (n) => lspContext.scopeOf(n),
1254
+ declarationsOf(_target) {
1255
+ const scope = lspContext.scopeOf(node);
1256
+ return scope.allDeclarations();
1257
+ },
1258
+ referencesTo(_target) {
1259
+ return docScope.references.filter((r) => r.resolved?.node === _target);
1260
+ },
1261
+ document,
1262
+ workspace: workspace ?? null
1263
+ };
1264
+ }
1265
+ walkForValidation(document.root, validation, createValidationContext);
1266
+ }
1267
+ /**
1268
+ * Walk AST and run matching validators
1269
+ */
1270
+ function walkForValidation(node, validation, createCtx) {
1271
+ const validators = validation[node.type];
1272
+ if (validators) {
1273
+ const ctx = createCtx(node);
1274
+ const fns = Array.isArray(validators) ? validators : [validators];
1275
+ for (const fn of fns) fn(node, ctx);
1276
+ }
1277
+ for (const child of node.namedChildren) walkForValidation(child, validation, createCtx);
1278
+ }
1279
+ /**
1280
+ * Add a validation diagnostic to the collection
1281
+ */
1282
+ function addValidationDiagnostic(severity, node, message, options, diagnostics) {
1283
+ const target = options?.at ?? node;
1284
+ const diag = {
1285
+ range: {
1286
+ start: target.startPosition,
1287
+ end: target.endPosition
1288
+ },
1289
+ severity,
1290
+ message,
1291
+ source: "treelsp"
1292
+ };
1293
+ if (options?.code) diag.code = options.code;
1294
+ diagnostics.push(diag);
1295
+ }
1296
+
1297
+ //#endregion
1298
+ //#region src/runtime/lsp/hover.ts
1299
+ /**
1300
+ * Provide hover information at position
1301
+ *
1302
+ * Strategy:
1303
+ * 1. Find node at position
1304
+ * 2. If node is a reference → resolve to declaration → use declaration's hover
1305
+ * 3. If node is a declaration → use its hover directly
1306
+ * 4. Fall back to default hover (type + name)
1307
+ */
1308
+ function provideHover(document, position, docScope, semantic, lsp, workspace) {
1309
+ const node = findNodeAtPosition(document.root, position);
1310
+ if (node.type === document.root.type) return null;
1311
+ const ctx = createLspContext(docScope, workspace ?? {}, document, semantic);
1312
+ const ref = findReferenceForNode(node, docScope);
1313
+ if (ref) {
1314
+ if (!ref.resolved) return null;
1315
+ const declNode = ref.resolved.node;
1316
+ const declParent = declNode.parent;
1317
+ const declType = ref.resolved.declaredBy;
1318
+ const hoverHandler = lsp?.[declType]?.hover;
1319
+ if (hoverHandler && declParent) {
1320
+ const contents = hoverHandler(declParent, ctx);
1321
+ if (contents) return {
1322
+ contents,
1323
+ range: {
1324
+ start: node.startPosition,
1325
+ end: node.endPosition
1326
+ }
1327
+ };
1328
+ }
1329
+ return {
1330
+ contents: `**${declType}** \`${ref.resolved.name}\``,
1331
+ range: {
1332
+ start: node.startPosition,
1333
+ end: node.endPosition
1334
+ }
1335
+ };
1336
+ }
1337
+ const decl = findDeclarationForNode(node, docScope);
1338
+ if (decl) {
1339
+ const declParent = node.parent;
1340
+ const hoverHandler = lsp?.[decl.declaredBy]?.hover;
1341
+ if (hoverHandler && declParent) {
1342
+ const contents = hoverHandler(declParent, ctx);
1343
+ if (contents) return {
1344
+ contents,
1345
+ range: {
1346
+ start: node.startPosition,
1347
+ end: node.endPosition
1348
+ }
1349
+ };
1350
+ }
1351
+ return {
1352
+ contents: `**${decl.declaredBy}** \`${decl.name}\``,
1353
+ range: {
1354
+ start: node.startPosition,
1355
+ end: node.endPosition
1356
+ }
1357
+ };
1358
+ }
1359
+ return null;
1360
+ }
1361
+
1362
+ //#endregion
1363
+ //#region src/runtime/lsp/definition.ts
1364
+ /**
1365
+ * Provide definition location for a reference
1366
+ *
1367
+ * Fully automatic: find reference at position → return declaration location
1368
+ */
1369
+ function provideDefinition(document, position, docScope) {
1370
+ const node = findNodeAtPosition(document.root, position);
1371
+ const ref = findReferenceForNode(node, docScope);
1372
+ if (!ref?.resolved) return null;
1373
+ return {
1374
+ uri: document.uri,
1375
+ range: nodeToRange(ref.resolved.node)
1376
+ };
1377
+ }
1378
+
1379
+ //#endregion
1380
+ //#region src/runtime/lsp/references.ts
1381
+ /**
1382
+ * Find all references to the symbol at position
1383
+ *
1384
+ * Strategy:
1385
+ * 1. Find node at position
1386
+ * 2. If reference → resolve to declaration
1387
+ * 3. If declaration → use directly
1388
+ * 4. Scan all documents in workspace for references to that declaration
1389
+ */
1390
+ function provideReferences(document, position, docScope, workspace) {
1391
+ const node = findNodeAtPosition(document.root, position);
1392
+ let targetDecl = null;
1393
+ const ref = findReferenceForNode(node, docScope);
1394
+ if (ref?.resolved) targetDecl = ref.resolved;
1395
+ if (!targetDecl) targetDecl = findDeclarationForNode(node, docScope);
1396
+ if (!targetDecl) return [];
1397
+ const locations = [];
1398
+ if (workspace) for (const wsDoc of workspace.getAllDocuments()) collectReferencesInDocument(wsDoc.document.uri, wsDoc.scope, targetDecl, locations);
1399
+ else collectReferencesInDocument(document.uri, docScope, targetDecl, locations);
1400
+ return locations;
1401
+ }
1402
+ /**
1403
+ * Collect all references to a declaration within a single document
1404
+ */
1405
+ function collectReferencesInDocument(uri, docScope, targetDecl, locations) {
1406
+ for (const ref of docScope.references) {
1407
+ if (!ref.resolved) continue;
1408
+ if (ref.resolved.name === targetDecl.name && ref.resolved.declaredBy === targetDecl.declaredBy) locations.push({
1409
+ uri,
1410
+ range: nodeToRange(ref.node)
1411
+ });
1412
+ }
1413
+ }
1414
+
1415
+ //#endregion
1416
+ //#region src/runtime/lsp/completion.ts
1417
+ /**
1418
+ * LSP CompletionItemKind mapping
1419
+ */
1420
+ const COMPLETION_KIND_MAP = {
1421
+ Text: 1,
1422
+ Method: 2,
1423
+ Function: 3,
1424
+ Constructor: 4,
1425
+ Field: 5,
1426
+ Variable: 6,
1427
+ Class: 7,
1428
+ Interface: 8,
1429
+ Module: 9,
1430
+ Property: 10,
1431
+ Enum: 13,
1432
+ Keyword: 14,
1433
+ Snippet: 15,
1434
+ Constant: 21
1435
+ };
1436
+ /**
1437
+ * Provide completions at position
1438
+ *
1439
+ * Sources:
1440
+ * 1. Scope-based: all declarations visible from current scope
1441
+ * 2. Keywords: from $keywords config
1442
+ * 3. Custom: from per-rule complete() handlers
1443
+ */
1444
+ function provideCompletion(document, position, docScope, semantic, lsp, workspace) {
1445
+ const node = findNodeAtPosition(document.root, position);
1446
+ const ctx = createLspContext(docScope, workspace ?? {}, document, semantic);
1447
+ const scopeItems = getScopeCompletions(node, docScope, lsp);
1448
+ const keywordItems = getKeywordCompletions(lsp);
1449
+ const customResult = getCustomCompletions(node, ctx, lsp);
1450
+ if (customResult?.replace) return customResult.items;
1451
+ const allItems = [...scopeItems, ...keywordItems];
1452
+ if (customResult) allItems.push(...customResult.items);
1453
+ return deduplicateCompletions(allItems);
1454
+ }
1455
+ /**
1456
+ * Get completions from all declarations visible in scope
1457
+ */
1458
+ function getScopeCompletions(node, docScope, lsp) {
1459
+ const scope = findScopeForNode(node, docScope);
1460
+ const items = [];
1461
+ const seen = /* @__PURE__ */ new Set();
1462
+ let currentScope = scope;
1463
+ while (currentScope) {
1464
+ for (const decl of currentScope.allDeclarations()) {
1465
+ if (seen.has(decl.name)) continue;
1466
+ seen.add(decl.name);
1467
+ const lspRule = lsp?.[decl.declaredBy];
1468
+ const kind = lspRule?.completionKind;
1469
+ const item = {
1470
+ label: decl.name,
1471
+ detail: decl.declaredBy
1472
+ };
1473
+ if (kind) item.kind = kind;
1474
+ items.push(item);
1475
+ }
1476
+ if (currentScope.kind === "isolated") break;
1477
+ currentScope = currentScope.parent;
1478
+ }
1479
+ return items;
1480
+ }
1481
+ /**
1482
+ * Get keyword completions from $keywords config
1483
+ */
1484
+ function getKeywordCompletions(lsp) {
1485
+ const keywords = lsp?.$keywords;
1486
+ if (!keywords) return [];
1487
+ return Object.entries(keywords).map(([keyword, descriptor]) => {
1488
+ const item = {
1489
+ label: keyword,
1490
+ kind: "Keyword"
1491
+ };
1492
+ if (descriptor.detail) item.detail = descriptor.detail;
1493
+ if (descriptor.documentation) item.documentation = descriptor.documentation;
1494
+ return item;
1495
+ });
1496
+ }
1497
+ /**
1498
+ * Get custom completions from per-rule complete() handler
1499
+ */
1500
+ function getCustomCompletions(node, ctx, lsp) {
1501
+ if (!lsp) return null;
1502
+ let current = node;
1503
+ while (current) {
1504
+ const handler = lsp[current.type]?.complete;
1505
+ if (handler) {
1506
+ const result = handler(current, ctx);
1507
+ if (Array.isArray(result)) return { items: result };
1508
+ return result;
1509
+ }
1510
+ current = current.parent;
1511
+ }
1512
+ return null;
1513
+ }
1514
+ /**
1515
+ * Remove duplicate completion items (by label)
1516
+ */
1517
+ function deduplicateCompletions(items) {
1518
+ const seen = /* @__PURE__ */ new Set();
1519
+ const result = [];
1520
+ for (const item of items) if (!seen.has(item.label)) {
1521
+ seen.add(item.label);
1522
+ result.push(item);
1523
+ }
1524
+ return result;
1525
+ }
1526
+
1527
+ //#endregion
1528
+ //#region src/runtime/lsp/rename.ts
1529
+ /**
1530
+ * Check if rename is possible at position and return the symbol range
1531
+ *
1532
+ * Returns the range of the symbol and its current name as placeholder,
1533
+ * or null if the position is not on a renameable symbol.
1534
+ */
1535
+ function prepareRename(document, position, docScope) {
1536
+ const node = findNodeAtPosition(document.root, position);
1537
+ const ref = findReferenceForNode(node, docScope);
1538
+ if (ref?.resolved) return {
1539
+ range: nodeToRange(node),
1540
+ placeholder: ref.name
1541
+ };
1542
+ const decl = findDeclarationForNode(node, docScope);
1543
+ if (decl) return {
1544
+ range: nodeToRange(node),
1545
+ placeholder: decl.name
1546
+ };
1547
+ return null;
1548
+ }
1549
+ /**
1550
+ * Provide rename edits for symbol at position
1551
+ *
1552
+ * Strategy:
1553
+ * 1. Find declaration (directly or via reference resolution)
1554
+ * 2. Find all references across workspace
1555
+ * 3. Return edits for declaration + all references
1556
+ */
1557
+ function provideRename(document, position, newName, docScope, workspace) {
1558
+ const node = findNodeAtPosition(document.root, position);
1559
+ let targetDecl = null;
1560
+ const ref = findReferenceForNode(node, docScope);
1561
+ if (ref?.resolved) targetDecl = ref.resolved;
1562
+ if (!targetDecl) targetDecl = findDeclarationForNode(node, docScope);
1563
+ if (!targetDecl) return null;
1564
+ const changes = {};
1565
+ function addEdit(uri, targetNode) {
1566
+ const edits = changes[uri] ?? (changes[uri] = []);
1567
+ edits.push({
1568
+ range: nodeToRange(targetNode),
1569
+ newText: newName
1570
+ });
1571
+ }
1572
+ if (workspace) for (const wsDoc of workspace.getAllDocuments()) {
1573
+ const uri = wsDoc.document.uri;
1574
+ for (const decl of wsDoc.scope.declarations) if (decl.name === targetDecl.name && decl.declaredBy === targetDecl.declaredBy) addEdit(uri, decl.node);
1575
+ for (const r of wsDoc.scope.references) if (r.resolved && r.resolved.name === targetDecl.name && r.resolved.declaredBy === targetDecl.declaredBy) addEdit(uri, r.node);
1576
+ }
1577
+ else {
1578
+ addEdit(document.uri, targetDecl.node);
1579
+ for (const r of docScope.references) if (r.resolved && r.resolved.name === targetDecl.name && r.resolved.declaredBy === targetDecl.declaredBy) addEdit(document.uri, r.node);
1580
+ }
1581
+ return { changes };
1582
+ }
1583
+
1584
+ //#endregion
1585
+ //#region src/runtime/lsp/symbols.ts
1586
+ /**
1587
+ * LSP SymbolKind mapping
1588
+ */
1589
+ const SYMBOL_KIND_MAP = {
1590
+ File: 1,
1591
+ Module: 2,
1592
+ Namespace: 3,
1593
+ Package: 4,
1594
+ Class: 5,
1595
+ Method: 6,
1596
+ Property: 7,
1597
+ Field: 8,
1598
+ Constructor: 9,
1599
+ Enum: 10,
1600
+ Interface: 11,
1601
+ Function: 12,
1602
+ Variable: 13,
1603
+ Constant: 14,
1604
+ String: 15,
1605
+ Number: 16,
1606
+ Boolean: 17,
1607
+ Array: 18
1608
+ };
1609
+ /**
1610
+ * Provide document symbols
1611
+ *
1612
+ * Walks all declarations and builds symbol entries for those
1613
+ * with a symbol descriptor defined in the LSP config
1614
+ */
1615
+ function provideSymbols(docScope, lsp) {
1616
+ if (!lsp) return [];
1617
+ const symbols = [];
1618
+ for (const decl of docScope.declarations) {
1619
+ const lspRule = lsp[decl.declaredBy];
1620
+ if (!lspRule?.symbol) continue;
1621
+ const descriptor = lspRule.symbol;
1622
+ const declParent = decl.node.parent;
1623
+ let name;
1624
+ if (typeof descriptor.label === "function") name = declParent ? descriptor.label(declParent) : decl.name;
1625
+ else name = descriptor.label;
1626
+ let detail;
1627
+ if (typeof descriptor.detail === "function") detail = declParent ? descriptor.detail(declParent) : void 0;
1628
+ else detail = descriptor.detail;
1629
+ const rangeNode = declParent ?? decl.node;
1630
+ const sym = {
1631
+ name,
1632
+ kind: descriptor.kind,
1633
+ kindNumber: SYMBOL_KIND_MAP[descriptor.kind] ?? 13,
1634
+ range: {
1635
+ start: rangeNode.startPosition,
1636
+ end: rangeNode.endPosition
1637
+ },
1638
+ selectionRange: {
1639
+ start: decl.node.startPosition,
1640
+ end: decl.node.endPosition
1641
+ }
1642
+ };
1643
+ if (detail) sym.detail = detail;
1644
+ symbols.push(sym);
1645
+ }
1646
+ return symbols;
1647
+ }
1648
+
1649
+ //#endregion
1650
+ //#region src/runtime/lsp/semantic-tokens.ts
1651
+ /**
1652
+ * Standard LSP semantic token types
1653
+ * Index in this array = tokenType value in the encoded data
1654
+ */
1655
+ const SEMANTIC_TOKEN_TYPES = [
1656
+ "namespace",
1657
+ "type",
1658
+ "class",
1659
+ "enum",
1660
+ "interface",
1661
+ "struct",
1662
+ "typeParameter",
1663
+ "parameter",
1664
+ "variable",
1665
+ "property",
1666
+ "enumMember",
1667
+ "event",
1668
+ "function",
1669
+ "method",
1670
+ "macro",
1671
+ "keyword",
1672
+ "modifier",
1673
+ "comment",
1674
+ "string",
1675
+ "number",
1676
+ "regexp",
1677
+ "operator",
1678
+ "decorator"
1679
+ ];
1680
+ /**
1681
+ * Standard LSP semantic token modifiers
1682
+ * Encoded as bit flags: modifier at index N has value 2^N
1683
+ */
1684
+ const SEMANTIC_TOKEN_MODIFIERS = [
1685
+ "declaration",
1686
+ "definition",
1687
+ "readonly",
1688
+ "static",
1689
+ "deprecated",
1690
+ "abstract",
1691
+ "async",
1692
+ "modification",
1693
+ "documentation",
1694
+ "defaultLibrary"
1695
+ ];
1696
+ /** Index lookup for token types */
1697
+ const TOKEN_TYPE_INDEX = {};
1698
+ for (let i = 0; i < SEMANTIC_TOKEN_TYPES.length; i++) TOKEN_TYPE_INDEX[SEMANTIC_TOKEN_TYPES[i]] = i;
1699
+ /** Mapping from CompletionKind to semantic token type name */
1700
+ const COMPLETION_KIND_TO_TOKEN_TYPE = {
1701
+ Variable: "variable",
1702
+ Function: "function",
1703
+ Method: "method",
1704
+ Class: "class",
1705
+ Interface: "interface",
1706
+ Module: "namespace",
1707
+ Enum: "enum",
1708
+ Constant: "variable",
1709
+ Property: "property",
1710
+ Field: "property",
1711
+ Constructor: "function"
1712
+ };
1713
+ const BRACKETS = new Set([
1714
+ "(",
1715
+ ")",
1716
+ "{",
1717
+ "}",
1718
+ "[",
1719
+ "]"
1720
+ ]);
1721
+ const DELIMITERS = new Set([
1722
+ ";",
1723
+ ",",
1724
+ ".",
1725
+ ":"
1726
+ ]);
1727
+ /**
1728
+ * Classify a named token rule by its name (heuristic)
1729
+ */
1730
+ function classifyTokenRuleName(ruleName) {
1731
+ const lower = ruleName.toLowerCase();
1732
+ if (lower.includes("comment")) return "comment";
1733
+ if (lower.includes("string")) return "string";
1734
+ if (lower.includes("number") || lower.includes("integer") || lower.includes("float")) return "number";
1735
+ if (lower.includes("bool")) return "boolean";
1736
+ return null;
1737
+ }
1738
+ /**
1739
+ * Compute semantic tokens for a full document
1740
+ *
1741
+ * Walks the AST and classifies each leaf node into an LSP semantic token type.
1742
+ * Uses the semantic and LSP definitions to determine token types for
1743
+ * declarations and references. Falls back to heuristics for other tokens.
1744
+ */
1745
+ function provideSemanticTokensFull(document, docScope, semantic, lsp) {
1746
+ const tokens = [];
1747
+ const declNodeTokenType = /* @__PURE__ */ new Map();
1748
+ const refNodeTokenType = /* @__PURE__ */ new Map();
1749
+ for (const decl of docScope.declarations) {
1750
+ const lspRule = lsp?.[decl.declaredBy];
1751
+ const kind = lspRule?.completionKind;
1752
+ const typeName = kind ? COMPLETION_KIND_TO_TOKEN_TYPE[kind] ?? "variable" : "variable";
1753
+ const typeIndex = TOKEN_TYPE_INDEX[typeName] ?? TOKEN_TYPE_INDEX["variable"];
1754
+ declNodeTokenType.set(decl.node.id, typeIndex);
1755
+ }
1756
+ for (const ref of docScope.references) if (ref.resolved) {
1757
+ const lspRule = lsp?.[ref.resolved.declaredBy];
1758
+ const kind = lspRule?.completionKind;
1759
+ const typeName = kind ? COMPLETION_KIND_TO_TOKEN_TYPE[kind] ?? "variable" : "variable";
1760
+ const typeIndex = TOKEN_TYPE_INDEX[typeName] ?? TOKEN_TYPE_INDEX["variable"];
1761
+ refNodeTokenType.set(ref.node.id, typeIndex);
1762
+ }
1763
+ walkForTokens(document.root, tokens, declNodeTokenType, refNodeTokenType, semantic);
1764
+ tokens.sort((a, b) => a.line - b.line || a.character - b.character);
1765
+ const data = [];
1766
+ let prevLine = 0;
1767
+ let prevChar = 0;
1768
+ for (const token of tokens) {
1769
+ const deltaLine = token.line - prevLine;
1770
+ const deltaChar = deltaLine === 0 ? token.character - prevChar : token.character;
1771
+ data.push(deltaLine, deltaChar, token.length, token.tokenType, token.modifiers);
1772
+ prevLine = token.line;
1773
+ prevChar = token.character;
1774
+ }
1775
+ return { data };
1776
+ }
1777
+ /**
1778
+ * Walk the AST to collect semantic tokens
1779
+ */
1780
+ function walkForTokens(node, tokens, declNodeTokenType, refNodeTokenType, semantic) {
1781
+ if (node.childCount === 0) {
1782
+ const token = classifyLeaf(node, declNodeTokenType, refNodeTokenType, semantic);
1783
+ if (token) tokens.push(token);
1784
+ return;
1785
+ }
1786
+ for (const child of node.children) walkForTokens(child, tokens, declNodeTokenType, refNodeTokenType, semantic);
1787
+ }
1788
+ /**
1789
+ * Classify a leaf node into a semantic token
1790
+ */
1791
+ function classifyLeaf(node, declNodeTokenType, refNodeTokenType, semantic) {
1792
+ const start = node.startPosition;
1793
+ const length = node.endIndex - node.startIndex;
1794
+ if (length <= 0) return null;
1795
+ const declType = declNodeTokenType.get(node.id);
1796
+ if (declType !== void 0) return {
1797
+ line: start.line,
1798
+ character: start.character,
1799
+ length,
1800
+ tokenType: declType,
1801
+ modifiers: 1
1802
+ };
1803
+ const refType = refNodeTokenType.get(node.id);
1804
+ if (refType !== void 0) return {
1805
+ line: start.line,
1806
+ character: start.character,
1807
+ length,
1808
+ tokenType: refType,
1809
+ modifiers: 0
1810
+ };
1811
+ if (!node.isNamed) {
1812
+ const text = node.text;
1813
+ if (/^[a-zA-Z_]+$/.test(text)) return {
1814
+ line: start.line,
1815
+ character: start.character,
1816
+ length,
1817
+ tokenType: TOKEN_TYPE_INDEX["keyword"],
1818
+ modifiers: 0
1819
+ };
1820
+ if (BRACKETS.has(text) || DELIMITERS.has(text)) return null;
1821
+ return {
1822
+ line: start.line,
1823
+ character: start.character,
1824
+ length,
1825
+ tokenType: TOKEN_TYPE_INDEX["operator"],
1826
+ modifiers: 0
1827
+ };
1828
+ }
1829
+ const tokenClass = classifyTokenRuleName(node.type);
1830
+ if (tokenClass) {
1831
+ const typeIndex = TOKEN_TYPE_INDEX[tokenClass];
1832
+ if (typeIndex !== void 0) return {
1833
+ line: start.line,
1834
+ character: start.character,
1835
+ length,
1836
+ tokenType: typeIndex,
1837
+ modifiers: 0
1838
+ };
1839
+ }
1840
+ const semRule = semantic[node.type];
1841
+ if (semRule?.references) return {
1842
+ line: start.line,
1843
+ character: start.character,
1844
+ length,
1845
+ tokenType: TOKEN_TYPE_INDEX["variable"],
1846
+ modifiers: 0
1847
+ };
1848
+ return null;
1849
+ }
1850
+
1851
+ //#endregion
1852
+ //#region src/runtime/lsp/server.ts
1853
+ /**
1854
+ * Create language service from language definition
1855
+ *
1856
+ * @param definition The language definition from defineLanguage()
1857
+ * @returns LanguageService with all LSP handler methods
1858
+ */
1859
+ function createServer(definition) {
1860
+ const semantic = definition.semantic ?? {};
1861
+ const lsp = definition.lsp;
1862
+ const validation = definition.validation;
1863
+ const documents = new DocumentManager(semantic);
1864
+ function getDocScope(document) {
1865
+ const wsDoc = documents.get(document.uri);
1866
+ if (wsDoc) {
1867
+ if (wsDoc.document !== document) return documents.change(document);
1868
+ return wsDoc.scope;
1869
+ }
1870
+ return documents.open(document);
1871
+ }
1872
+ return {
1873
+ documents,
1874
+ computeDiagnostics(document) {
1875
+ const docScope = getDocScope(document);
1876
+ return computeDiagnostics(document, docScope, semantic, lsp, validation, documents.getWorkspace());
1877
+ },
1878
+ provideHover(document, position) {
1879
+ const docScope = getDocScope(document);
1880
+ return provideHover(document, position, docScope, semantic, lsp, documents.getWorkspace());
1881
+ },
1882
+ provideDefinition(document, position) {
1883
+ const docScope = getDocScope(document);
1884
+ return provideDefinition(document, position, docScope);
1885
+ },
1886
+ provideReferences(document, position) {
1887
+ const docScope = getDocScope(document);
1888
+ return provideReferences(document, position, docScope, documents.getWorkspace());
1889
+ },
1890
+ provideCompletion(document, position) {
1891
+ const docScope = getDocScope(document);
1892
+ return provideCompletion(document, position, docScope, semantic, lsp, documents.getWorkspace());
1893
+ },
1894
+ prepareRename(document, position) {
1895
+ const docScope = getDocScope(document);
1896
+ return prepareRename(document, position, docScope);
1897
+ },
1898
+ provideRename(document, position, newName) {
1899
+ const docScope = getDocScope(document);
1900
+ return provideRename(document, position, newName, docScope, documents.getWorkspace());
1901
+ },
1902
+ provideSymbols(document) {
1903
+ const docScope = getDocScope(document);
1904
+ return provideSymbols(docScope, lsp);
1905
+ },
1906
+ provideSemanticTokensFull(document) {
1907
+ const docScope = getDocScope(document);
1908
+ return provideSemanticTokensFull(document, docScope, semantic, lsp);
1909
+ }
1910
+ };
1911
+ }
1912
+
1913
+ //#endregion
1914
+ export { COMPLETION_KIND_MAP, DocumentManager, DocumentState, SEMANTIC_TOKEN_MODIFIERS, SEMANTIC_TOKEN_TYPES, Scope, Workspace, buildScopes, createDocumentState, createParser, createServer, loadLanguage, preloadParser };