@oml/language 0.7.0

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 (91) hide show
  1. package/README.md +44 -0
  2. package/out/oml/generated/ast.d.ts +2109 -0
  3. package/out/oml/generated/ast.js +1807 -0
  4. package/out/oml/generated/ast.js.map +1 -0
  5. package/out/oml/generated/grammar.d.ts +6 -0
  6. package/out/oml/generated/grammar.js +6885 -0
  7. package/out/oml/generated/grammar.js.map +1 -0
  8. package/out/oml/generated/module.d.ts +13 -0
  9. package/out/oml/generated/module.js +21 -0
  10. package/out/oml/generated/module.js.map +1 -0
  11. package/out/oml/index.d.ts +17 -0
  12. package/out/oml/index.js +19 -0
  13. package/out/oml/index.js.map +1 -0
  14. package/out/oml/oml-candidates.d.ts +27 -0
  15. package/out/oml/oml-candidates.js +146 -0
  16. package/out/oml/oml-candidates.js.map +1 -0
  17. package/out/oml/oml-code-actions.d.ts +6 -0
  18. package/out/oml/oml-code-actions.js +79 -0
  19. package/out/oml/oml-code-actions.js.map +1 -0
  20. package/out/oml/oml-completion.d.ts +50 -0
  21. package/out/oml/oml-completion.js +188 -0
  22. package/out/oml/oml-completion.js.map +1 -0
  23. package/out/oml/oml-converter.d.ts +10 -0
  24. package/out/oml/oml-converter.js +62 -0
  25. package/out/oml/oml-converter.js.map +1 -0
  26. package/out/oml/oml-document-validator.d.ts +10 -0
  27. package/out/oml/oml-document-validator.js +31 -0
  28. package/out/oml/oml-document-validator.js.map +1 -0
  29. package/out/oml/oml-document.d.ts +9 -0
  30. package/out/oml/oml-document.js +18 -0
  31. package/out/oml/oml-document.js.map +1 -0
  32. package/out/oml/oml-edit.d.ts +72 -0
  33. package/out/oml/oml-edit.js +1155 -0
  34. package/out/oml/oml-edit.js.map +1 -0
  35. package/out/oml/oml-formatter.d.ts +22 -0
  36. package/out/oml/oml-formatter.js +357 -0
  37. package/out/oml/oml-formatter.js.map +1 -0
  38. package/out/oml/oml-hover.d.ts +13 -0
  39. package/out/oml/oml-hover.js +71 -0
  40. package/out/oml/oml-hover.js.map +1 -0
  41. package/out/oml/oml-index-manager.d.ts +10 -0
  42. package/out/oml/oml-index-manager.js +48 -0
  43. package/out/oml/oml-index-manager.js.map +1 -0
  44. package/out/oml/oml-index.d.ts +20 -0
  45. package/out/oml/oml-index.js +133 -0
  46. package/out/oml/oml-index.js.map +1 -0
  47. package/out/oml/oml-module.d.ts +42 -0
  48. package/out/oml/oml-module.js +76 -0
  49. package/out/oml/oml-module.js.map +1 -0
  50. package/out/oml/oml-rename.d.ts +14 -0
  51. package/out/oml/oml-rename.js +114 -0
  52. package/out/oml/oml-rename.js.map +1 -0
  53. package/out/oml/oml-scope.d.ts +30 -0
  54. package/out/oml/oml-scope.js +225 -0
  55. package/out/oml/oml-scope.js.map +1 -0
  56. package/out/oml/oml-serializer.d.ts +2 -0
  57. package/out/oml/oml-serializer.js +883 -0
  58. package/out/oml/oml-serializer.js.map +1 -0
  59. package/out/oml/oml-utils.d.ts +53 -0
  60. package/out/oml/oml-utils.js +241 -0
  61. package/out/oml/oml-utils.js.map +1 -0
  62. package/out/oml/oml-validator.d.ts +49 -0
  63. package/out/oml/oml-validator.js +668 -0
  64. package/out/oml/oml-validator.js.map +1 -0
  65. package/out/oml/oml-workspace.d.ts +23 -0
  66. package/out/oml/oml-workspace.js +68 -0
  67. package/out/oml/oml-workspace.js.map +1 -0
  68. package/package.json +50 -0
  69. package/src/oml/generated/ast.ts +2641 -0
  70. package/src/oml/generated/grammar.ts +6887 -0
  71. package/src/oml/generated/module.ts +25 -0
  72. package/src/oml/index.ts +19 -0
  73. package/src/oml/oml-candidates.ts +176 -0
  74. package/src/oml/oml-code-actions.ts +120 -0
  75. package/src/oml/oml-completion.ts +222 -0
  76. package/src/oml/oml-converter.ts +66 -0
  77. package/src/oml/oml-document-validator.ts +39 -0
  78. package/src/oml/oml-document.ts +24 -0
  79. package/src/oml/oml-edit.ts +1292 -0
  80. package/src/oml/oml-formatter.ts +390 -0
  81. package/src/oml/oml-hover.ts +93 -0
  82. package/src/oml/oml-index-manager.ts +56 -0
  83. package/src/oml/oml-index.ts +145 -0
  84. package/src/oml/oml-module.ts +105 -0
  85. package/src/oml/oml-rename.ts +140 -0
  86. package/src/oml/oml-scope.ts +279 -0
  87. package/src/oml/oml-serializer.ts +1080 -0
  88. package/src/oml/oml-utils.ts +294 -0
  89. package/src/oml/oml-validator.ts +725 -0
  90. package/src/oml/oml-workspace.ts +81 -0
  91. package/src/oml/oml.langium +594 -0
@@ -0,0 +1,390 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import type { Formatter } from 'langium/lsp';
4
+ import type { LangiumDocument } from 'langium';
5
+ import type {
6
+ DocumentFormattingParams,
7
+ DocumentOnTypeFormattingOptions,
8
+ DocumentOnTypeFormattingParams,
9
+ DocumentRangeFormattingParams,
10
+ TextEdit,
11
+ } from 'vscode-languageserver-protocol';
12
+ import { TextEdit as VsTextEdit } from 'vscode-languageserver-protocol';
13
+ import { isOntology } from './generated/ast.js';
14
+ import { serializeOntology } from './oml-serializer.js';
15
+
16
+ export class OmlFormatter implements Formatter {
17
+ private readonly formattedTextCache = new Map<string, { version: number; text: string }>();
18
+
19
+ formatDocument(document: LangiumDocument, _params: DocumentFormattingParams): TextEdit[] {
20
+ const formatted = this.getFormattedText(document);
21
+ if (formatted === undefined) {
22
+ return [];
23
+ }
24
+ const textDocument = document.textDocument;
25
+ const end = textDocument.positionAt(textDocument.getText().length);
26
+ return [VsTextEdit.replace({ start: { line: 0, character: 0 }, end }, formatted)];
27
+ }
28
+
29
+ formatDocumentRange(document: LangiumDocument, params: DocumentRangeFormattingParams): TextEdit[] {
30
+ return this.formatRange(document, params.range);
31
+ }
32
+
33
+ formatDocumentOnType(document: LangiumDocument, params: DocumentOnTypeFormattingParams): TextEdit[] {
34
+ const textDocument = document.textDocument;
35
+ const text = textDocument.getText();
36
+ const lines = text.split('\n');
37
+ const line = Math.min(params.position.line, lines.length - 1);
38
+ if (line < 0) {
39
+ return [];
40
+ }
41
+ const currentLine = lines[line] ?? '';
42
+ const desiredIndent = this.computeOnTypeIndent(lines, line);
43
+ if (desiredIndent === undefined) {
44
+ return [];
45
+ }
46
+
47
+ const currentIndent = currentLine.match(/^\s*/)?.[0] ?? '';
48
+ if (currentIndent === desiredIndent) {
49
+ return [];
50
+ }
51
+
52
+ return [VsTextEdit.replace(
53
+ { start: { line, character: 0 }, end: { line, character: currentIndent.length } },
54
+ desiredIndent
55
+ )];
56
+ }
57
+
58
+ get formatOnTypeOptions(): DocumentOnTypeFormattingOptions | undefined {
59
+ return {
60
+ firstTriggerCharacter: '\n'
61
+ };
62
+ }
63
+
64
+ private getFormattedText(document: LangiumDocument): string | undefined {
65
+ const parseResult = document.parseResult;
66
+ if (!parseResult || parseResult.lexerErrors.length || parseResult.parserErrors.length) {
67
+ return undefined;
68
+ }
69
+ const uri = document.uri?.toString() ?? document.textDocument.uri;
70
+ const version = document.textDocument.version ?? 0;
71
+ const cached = this.formattedTextCache.get(uri);
72
+ if (cached && cached.version === version) {
73
+ return cached.text;
74
+ }
75
+ const root = parseResult.value;
76
+ if (!root || !isOntology(root)) {
77
+ return undefined;
78
+ }
79
+ const formatted = serializeOntology(root);
80
+ const text = this.mergeComments(document.textDocument.getText(), formatted);
81
+ this.formattedTextCache.set(uri, { version, text });
82
+ if (this.formattedTextCache.size > 200) {
83
+ this.formattedTextCache.clear();
84
+ }
85
+ return text;
86
+ }
87
+
88
+ private formatRange(document: LangiumDocument, range: { start: { line: number; character: number }; end: { line: number; character: number } }): TextEdit[] {
89
+ const formatted = this.getFormattedText(document);
90
+ if (formatted === undefined) {
91
+ return [];
92
+ }
93
+ const textDocument = document.textDocument;
94
+ const original = textDocument.getText();
95
+ const startOffset = textDocument.offsetAt(range.start);
96
+ const endOffset = textDocument.offsetAt(range.end);
97
+ const prefix = original.slice(0, startOffset);
98
+ const suffix = original.slice(endOffset);
99
+ if (!formatted.startsWith(prefix) || !formatted.endsWith(suffix)) {
100
+ const originalLines = original.split('\n');
101
+ const formattedLines = formatted.split('\n');
102
+ const startLine = Math.max(0, range.start.line);
103
+ const endLine = Math.min(range.end.line, originalLines.length - 1);
104
+ const commonPrefix = this.countCommonPrefixLines(originalLines, formattedLines);
105
+ const commonSuffix = this.countCommonSuffixLines(originalLines, formattedLines, commonPrefix);
106
+ const delta = formattedLines.length - originalLines.length;
107
+ let startLineFormatted = startLine;
108
+ let endLineFormatted = endLine;
109
+ if (startLine >= commonPrefix && endLine <= originalLines.length - commonSuffix - 1) {
110
+ startLineFormatted = startLine + delta;
111
+ endLineFormatted = endLine + delta;
112
+ }
113
+ if (
114
+ startLineFormatted < 0 ||
115
+ endLineFormatted < startLineFormatted ||
116
+ endLineFormatted >= formattedLines.length
117
+ ) {
118
+ return [];
119
+ }
120
+ const replacementLines = formattedLines.slice(startLineFormatted, endLineFormatted + 1);
121
+ const rangeForLines = {
122
+ start: { line: startLine, character: 0 },
123
+ end: { line: endLine, character: originalLines[endLine]?.length ?? 0 }
124
+ };
125
+ return [VsTextEdit.replace(rangeForLines, replacementLines.join('\n'))];
126
+ }
127
+ const replacement = formatted.slice(prefix.length, formatted.length - suffix.length);
128
+ return [VsTextEdit.replace(range, replacement)];
129
+ }
130
+
131
+ private mergeComments(original: string, formatted: string): string {
132
+ const originalLines = original.split('\n');
133
+ const formattedLines = formatted.split('\n');
134
+ const blocks = this.collectCommentBlocks(originalLines);
135
+ const inlineComments = this.collectInlineComments(originalLines);
136
+ const anchorLookup = this.buildAnchorIndex(formattedLines);
137
+ let insertOffset = 0;
138
+ for (const block of blocks) {
139
+ const anchorLine = block.anchorText
140
+ ? this.findAnchorLine(anchorLookup, block.anchorText, insertOffset)
141
+ : -1;
142
+ const insertAt = anchorLine === -1 ? formattedLines.length : anchorLine;
143
+ const insertLines = block.trailingBlankLines
144
+ ? [...block.lines, ...Array.from({ length: block.trailingBlankLines }, () => '')]
145
+ : block.lines;
146
+ formattedLines.splice(insertAt, 0, ...insertLines);
147
+ insertOffset = insertAt + insertLines.length;
148
+ }
149
+ if (inlineComments.size > 0) {
150
+ for (let i = 0; i < formattedLines.length; i += 1) {
151
+ const line = formattedLines[i];
152
+ if (this.findLineCommentIndex(line) !== -1) {
153
+ continue;
154
+ }
155
+ const code = line.trim();
156
+ const queue = inlineComments.get(code);
157
+ if (!queue || queue.length === 0) {
158
+ continue;
159
+ }
160
+ const suffix = queue.shift() ?? '';
161
+ formattedLines[i] = `${line}${suffix}`;
162
+ }
163
+ }
164
+ return formattedLines.join('\n');
165
+ }
166
+
167
+ private collectCommentBlocks(lines: string[]): Array<{ lines: string[]; anchorText?: string; trailingBlankLines: number }> {
168
+ const blocks: Array<{ lines: string[]; anchorText?: string; trailingBlankLines: number }> = [];
169
+ let index = 0;
170
+ let inBlock = false;
171
+ while (index < lines.length) {
172
+ const line = lines[index];
173
+ const trimmed = line.trim();
174
+ const isLineComment = trimmed.startsWith('//');
175
+ const isBlockStart = trimmed.startsWith('/*');
176
+ const isBlockContinue = inBlock || trimmed.startsWith('*');
177
+ if (isLineComment || isBlockStart || isBlockContinue) {
178
+ const blockLines: string[] = [];
179
+ while (index < lines.length) {
180
+ const current = lines[index];
181
+ const currentTrimmed = current.trim();
182
+ const currentLineComment = currentTrimmed.startsWith('//');
183
+ const currentBlockStart = currentTrimmed.startsWith('/*');
184
+ const currentBlockContinue = inBlock || currentTrimmed.startsWith('*');
185
+ if (!(currentLineComment || currentBlockStart || currentBlockContinue)) {
186
+ break;
187
+ }
188
+ blockLines.push(current);
189
+ if (currentTrimmed.includes('*/')) {
190
+ inBlock = false;
191
+ } else if (currentBlockStart) {
192
+ inBlock = true;
193
+ }
194
+ index += 1;
195
+ }
196
+ let anchorText: string | undefined;
197
+ let trailingBlankLines = 0;
198
+ let lookahead = index;
199
+ while (lookahead < lines.length) {
200
+ const next = lines[lookahead].trim();
201
+ if (!next) {
202
+ trailingBlankLines += 1;
203
+ lookahead += 1;
204
+ continue;
205
+ }
206
+ if (next && !next.startsWith('//') && !next.startsWith('/*') && !next.startsWith('*')) {
207
+ anchorText = next;
208
+ break;
209
+ }
210
+ lookahead += 1;
211
+ }
212
+ blocks.push({ lines: blockLines, anchorText, trailingBlankLines });
213
+ continue;
214
+ }
215
+ index += 1;
216
+ }
217
+ return blocks;
218
+ }
219
+
220
+ private collectInlineComments(lines: string[]): Map<string, string[]> {
221
+ const inline = new Map<string, string[]>();
222
+ for (const line of lines) {
223
+ const commentIndex = this.findLineCommentIndex(line);
224
+ if (commentIndex === -1) {
225
+ continue;
226
+ }
227
+ const codePart = line.slice(0, commentIndex);
228
+ const commentPart = line.slice(commentIndex);
229
+ if (!codePart.trim()) {
230
+ continue;
231
+ }
232
+ const key = codePart.trim();
233
+ const bucket = inline.get(key) ?? [];
234
+ bucket.push(` ${commentPart.trimStart()}`);
235
+ inline.set(key, bucket);
236
+ }
237
+ return inline;
238
+ }
239
+
240
+ private findLineCommentIndex(line: string): number {
241
+ let inSingle = false;
242
+ let inDouble = false;
243
+ let inTripleSingle = false;
244
+ let inTripleDouble = false;
245
+ let inAngle = false;
246
+ for (let i = 0; i < line.length - 1; i += 1) {
247
+ const ch = line[i];
248
+ const next = line[i + 1];
249
+ if (!inSingle && !inDouble && !inTripleSingle && !inTripleDouble && ch === '<') {
250
+ inAngle = true;
251
+ } else if (inAngle && ch === '>') {
252
+ inAngle = false;
253
+ }
254
+ if (!inAngle) {
255
+ if (!inSingle && !inDouble && ch === '\'' && next === '\'' && line[i + 2] === '\'') {
256
+ inTripleSingle = !inTripleSingle;
257
+ i += 2;
258
+ continue;
259
+ }
260
+ if (!inSingle && !inDouble && ch === '"' && next === '"' && line[i + 2] === '"') {
261
+ inTripleDouble = !inTripleDouble;
262
+ i += 2;
263
+ continue;
264
+ }
265
+ if (!inTripleSingle && !inTripleDouble && ch === '\'' && !inDouble) {
266
+ inSingle = !inSingle;
267
+ continue;
268
+ }
269
+ if (!inTripleSingle && !inTripleDouble && ch === '"' && !inSingle) {
270
+ inDouble = !inDouble;
271
+ continue;
272
+ }
273
+ }
274
+ if (!inSingle && !inDouble && !inTripleSingle && !inTripleDouble && !inAngle && ch === '/' && next === '/') {
275
+ return i;
276
+ }
277
+ }
278
+ return -1;
279
+ }
280
+
281
+ private buildAnchorIndex(lines: string[]): Map<string, { indices: number[]; cursor: number }> {
282
+ const index = new Map<string, { indices: number[]; cursor: number }>();
283
+ for (let i = 0; i < lines.length; i += 1) {
284
+ const key = lines[i].trim();
285
+ if (!key) {
286
+ continue;
287
+ }
288
+ let entry = index.get(key);
289
+ if (!entry) {
290
+ entry = { indices: [], cursor: 0 };
291
+ index.set(key, entry);
292
+ }
293
+ entry.indices.push(i);
294
+ }
295
+ return index;
296
+ }
297
+
298
+ private findAnchorLine(index: Map<string, { indices: number[]; cursor: number }>, anchorText: string, startIndex: number): number {
299
+ const entry = index.get(anchorText);
300
+ if (!entry) {
301
+ return -1;
302
+ }
303
+ while (entry.cursor < entry.indices.length && entry.indices[entry.cursor] < startIndex) {
304
+ entry.cursor += 1;
305
+ }
306
+ if (entry.cursor >= entry.indices.length) {
307
+ return -1;
308
+ }
309
+ const line = entry.indices[entry.cursor];
310
+ entry.cursor += 1;
311
+ return line;
312
+ }
313
+
314
+ private countCommonPrefixLines(originalLines: string[], formattedLines: string[]): number {
315
+ const limit = Math.min(originalLines.length, formattedLines.length);
316
+ let index = 0;
317
+ while (index < limit && originalLines[index] === formattedLines[index]) {
318
+ index += 1;
319
+ }
320
+ return index;
321
+ }
322
+
323
+ private countCommonSuffixLines(originalLines: string[], formattedLines: string[], prefixCount: number): number {
324
+ const originalLimit = originalLines.length - prefixCount;
325
+ const formattedLimit = formattedLines.length - prefixCount;
326
+ const limit = Math.min(originalLimit, formattedLimit);
327
+ let count = 0;
328
+ while (count < limit) {
329
+ const originalIndex = originalLines.length - 1 - count;
330
+ const formattedIndex = formattedLines.length - 1 - count;
331
+ if (originalLines[originalIndex] !== formattedLines[formattedIndex]) {
332
+ break;
333
+ }
334
+ count += 1;
335
+ }
336
+ return count;
337
+ }
338
+
339
+ private computeOnTypeIndent(lines: string[], line: number): string | undefined {
340
+ const currentLine = lines[line] ?? '';
341
+ const trimmedCurrent = currentLine.trim();
342
+ if (trimmedCurrent.startsWith('//') || trimmedCurrent.startsWith('/*') || trimmedCurrent.startsWith('*')) {
343
+ return undefined;
344
+ }
345
+ const indentUnit = this.detectIndentUnit(lines);
346
+
347
+ let previousIndex = line - 1;
348
+ while (previousIndex >= 0 && !(lines[previousIndex] ?? '').trim()) {
349
+ previousIndex -= 1;
350
+ }
351
+ if (previousIndex < 0) {
352
+ return '';
353
+ }
354
+
355
+ const previousLine = lines[previousIndex] ?? '';
356
+ const previousIndent = previousLine.match(/^\s*/)?.[0] ?? '';
357
+ const previousTrimmed = previousLine.trim();
358
+ let level = previousIndent.length;
359
+
360
+ if (previousTrimmed.endsWith('{')) {
361
+ level += indentUnit.length;
362
+ }
363
+ if (trimmedCurrent.startsWith('}')) {
364
+ level -= indentUnit.length;
365
+ }
366
+
367
+ level = Math.max(0, level);
368
+ if (indentUnit.startsWith('\t')) {
369
+ return '\t'.repeat(Math.floor(level / indentUnit.length));
370
+ }
371
+ return ' '.repeat(level);
372
+ }
373
+
374
+ private detectIndentUnit(lines: string[]): string {
375
+ for (const line of lines) {
376
+ if (!line.trim()) {
377
+ continue;
378
+ }
379
+ const indent = line.match(/^\s+/)?.[0];
380
+ if (!indent) {
381
+ continue;
382
+ }
383
+ if (indent.includes('\t')) {
384
+ return '\t';
385
+ }
386
+ return ' '.repeat(Math.min(indent.length, 4));
387
+ }
388
+ return ' ';
389
+ }
390
+ }
@@ -0,0 +1,93 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ /**
4
+ * OML Hover Provider
5
+ * Provides rich hover information for OML elements with type and namespace details
6
+ */
7
+
8
+ import type { AstNode, LangiumDocument, MaybePromise } from 'langium';
9
+ import { CstUtils } from 'langium';
10
+ import { AstNodeHoverProvider } from 'langium/lsp';
11
+ import type { LangiumServices } from 'langium/lsp';
12
+ import type { Hover, HoverParams } from 'vscode-languageserver-protocol';
13
+ import { MarkupKind } from 'vscode-languageserver-protocol';
14
+ import { findOwningOntologyNode, humanizeTypeName } from './oml-utils.js';
15
+
16
+ export class OmlHoverProvider extends AstNodeHoverProvider {
17
+
18
+ constructor(services: LangiumServices) {
19
+ super(services);
20
+ }
21
+
22
+ override async getHoverContent(document: LangiumDocument, params: HoverParams): Promise<Hover | undefined> {
23
+ const rootNode = document.parseResult?.value?.$cstNode;
24
+ if (rootNode) {
25
+ const offset = document.textDocument.offsetAt(params.position);
26
+ const cstNode = CstUtils.findDeclarationNodeAtOffset(rootNode, offset, this.grammarConfig.nameRegexp);
27
+ if (cstNode && cstNode.offset + cstNode.length > offset) {
28
+ const contents: string[] = [];
29
+ const targetNodes = this.references.findDeclarations(cstNode);
30
+ for (const targetNode of targetNodes) {
31
+ const content = await this.getAstNodeHoverContent(targetNode);
32
+ if (typeof content === 'string') {
33
+ contents.push(content);
34
+ }
35
+ }
36
+ if (contents.length > 0) {
37
+ // Override to use proper MarkupKind.Markdown instead of 'markdown' with language
38
+ return {
39
+ contents: {
40
+ kind: MarkupKind.Markdown,
41
+ value: contents.join('\n\n---\n\n')
42
+ }
43
+ };
44
+ }
45
+ }
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ protected getAstNodeHoverContent(node: AstNode): MaybePromise<string | undefined> {
51
+ // The node here is the AST node under the cursor
52
+ // Check if it has a name property (it's a definition)
53
+ if ((node as any).name) {
54
+ const elementType = node.$type || 'element';
55
+ const keyword = humanizeTypeName(elementType);
56
+ const name = (node as any).name;
57
+
58
+ let hoverText = `**${keyword}** \`${name}\``;
59
+
60
+ const ontology = findOwningOntologyNode(node);
61
+ if (ontology?.namespace) {
62
+ const namespace = ontology.namespace.replace(/^<|>$/g, '');
63
+ hoverText += `\n\nDefined in: \`${namespace}\``;
64
+ }
65
+
66
+ return hoverText;
67
+ }
68
+
69
+ // For references, we want to show info about the referenced element
70
+ const ref = (node as any).ref;
71
+
72
+ if (ref && ref.name) {
73
+ // Get the element type
74
+ const elementType = ref.$type || 'element';
75
+ const keyword = humanizeTypeName(elementType);
76
+
77
+ // Build hover text: "keyword name"
78
+ let hoverText = `**${keyword}** \`${ref.name}\``;
79
+
80
+ // Add namespace if available
81
+ const ontology = findOwningOntologyNode(ref);
82
+ if (ontology?.namespace) {
83
+ const namespace = ontology.namespace.replace(/^<|>$/g, '');
84
+ hoverText += `\n\nDefined in: \`${namespace}\``;
85
+ }
86
+
87
+ return hoverText;
88
+ }
89
+
90
+ return undefined;
91
+ }
92
+
93
+ }
@@ -0,0 +1,56 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import { AstUtils, DefaultIndexManager, URI, UriUtils, stream, type AstNode, type LangiumDocument, type LangiumSharedCoreServices, type ReferenceDescription } from 'langium';
4
+ import type { CancellationToken } from 'vscode-jsonrpc';
5
+ import { canonicalizeWorkspaceDocumentUri } from './oml-workspace.js';
6
+
7
+ export class OmlIndexManager extends DefaultIndexManager {
8
+ private readonly services: LangiumSharedCoreServices;
9
+
10
+ constructor(services: LangiumSharedCoreServices) {
11
+ super(services);
12
+ this.services = services;
13
+ }
14
+
15
+ override findAllReferences(targetNode: AstNode, astNodePath: string) {
16
+ const targetDocUri = this.canonicalizeUri(AstUtils.getDocument(targetNode).uri);
17
+ const result: ReferenceDescription[] = [];
18
+ this.referenceIndex.forEach(docRefs => {
19
+ docRefs.forEach(refDescr => {
20
+ if (UriUtils.equals(this.canonicalizeUri(refDescr.targetUri), targetDocUri) && refDescr.targetPath === astNodePath) {
21
+ result.push(refDescr);
22
+ }
23
+ });
24
+ });
25
+ return stream(result);
26
+ }
27
+
28
+ override async updateReferences(document: LangiumDocument, cancelToken?: CancellationToken): Promise<void> {
29
+ const services = this.serviceRegistry.getServices(document.uri);
30
+ const references = await services.workspace.ReferenceDescriptionProvider.createDescriptions(document, cancelToken);
31
+ const sourceUri = this.canonicalizeUri(document.uri);
32
+ this.referenceIndex.set(document.uri.toString(), references.map(ref => {
33
+ const targetUri = this.canonicalizeUri(ref.targetUri);
34
+ return {
35
+ ...ref,
36
+ sourceUri,
37
+ targetUri,
38
+ local: UriUtils.equals(sourceUri, targetUri),
39
+ };
40
+ }));
41
+ }
42
+
43
+ override isAffected(document: LangiumDocument, changedUris: Set<string>): boolean {
44
+ const references = this.referenceIndex.get(document.uri.toString());
45
+ if (!references) {
46
+ return false;
47
+ }
48
+ const canonicalChangedUris = new Set(Array.from(changedUris, uri => this.canonicalizeUri(uri).toString()));
49
+ return references.some(ref => !ref.local && canonicalChangedUris.has(this.canonicalizeUri(ref.targetUri).toString()));
50
+ }
51
+
52
+ private canonicalizeUri(uri: URI | string): URI {
53
+ const parsedUri = typeof uri === 'string' ? URI.parse(uri) : uri;
54
+ return canonicalizeWorkspaceDocumentUri(this.services.workspace.WorkspaceManager, parsedUri);
55
+ }
56
+ }
@@ -0,0 +1,145 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import { DocumentState, type LangiumDocument } from 'langium';
4
+ import type { LangiumSharedServices } from 'langium/lsp';
5
+ import { isOntology } from './generated/ast.js';
6
+ import { isTransientEditorDocumentUri } from './oml-utils.js';
7
+
8
+ const ontologyIndexByShared = new WeakMap<object, OntologyModelIndex>();
9
+
10
+ export function getOntologyModelIndex(shared: LangiumSharedServices): OntologyModelIndex {
11
+ const key = shared as object;
12
+ const existing = ontologyIndexByShared.get(key);
13
+ if (existing) {
14
+ return existing;
15
+ }
16
+ const created = new OntologyModelIndex(shared);
17
+ ontologyIndexByShared.set(key, created);
18
+ return created;
19
+ }
20
+
21
+ export class OntologyModelIndex {
22
+ private readonly ontologyIdentifierToModelUri = new Map<string, string>();
23
+ private readonly modelUriToOntologyIri = new Map<string, string>();
24
+ private readonly workspaceModelUrisByOntologyIri = new Map<string, Set<string>>();
25
+ private readonly workspaceOntologyIriByModelUri = new Map<string, string>();
26
+ private readonly workspaceNamespaceByModelUri = new Map<string, string>();
27
+
28
+ constructor(private readonly shared: LangiumSharedServices) {
29
+ this.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Parsed, (document) => {
30
+ this.indexDocument(document);
31
+ });
32
+ }
33
+
34
+ resolveModelUri(identifier: string): string | undefined {
35
+ this.ensureIndexedFromLoadedDocuments();
36
+ const normalized = this.normalizeNamespace(identifier);
37
+ if (!normalized) {
38
+ return undefined;
39
+ }
40
+ const ontologyIri = this.ontologyIriFromNamespace(normalized);
41
+ const candidates = this.workspaceModelUrisByOntologyIri.get(ontologyIri);
42
+ if (!candidates || candidates.size !== 1) {
43
+ return undefined;
44
+ }
45
+ return [...candidates][0];
46
+ }
47
+
48
+ resolveOntologyIri(modelUri: string): string | undefined {
49
+ this.ensureIndexedFromLoadedDocuments();
50
+ return this.modelUriToOntologyIri.get(modelUri);
51
+ }
52
+
53
+ removeModel(modelUri: string): void {
54
+ this.unindexModelUri(modelUri);
55
+ }
56
+
57
+ private ensureIndexedFromLoadedDocuments(): void {
58
+ const docs: any = this.shared.workspace.LangiumDocuments;
59
+ const all = docs.all ?? [];
60
+ const iterable: Iterable<LangiumDocument> = Array.isArray(all)
61
+ ? all
62
+ : (typeof all?.toArray === 'function' ? all.toArray() : all);
63
+ for (const document of iterable) {
64
+ this.indexDocument(document);
65
+ }
66
+ }
67
+
68
+ private indexDocument(document: LangiumDocument): void {
69
+ const modelUri = document.uri.toString();
70
+ this.unindexModelUri(modelUri);
71
+ const root = document.parseResult?.value;
72
+ if (!isOntology(root)) {
73
+ return;
74
+ }
75
+ const namespace = this.normalizeNamespace((root as any).namespace ?? '');
76
+ if (!namespace) {
77
+ return;
78
+ }
79
+ if (isTransientEditorDocumentUri(modelUri)) {
80
+ return;
81
+ }
82
+ const ontologyIri = this.ontologyIriFromNamespace(namespace);
83
+ const modelUris = this.workspaceModelUrisByOntologyIri.get(ontologyIri) ?? new Set<string>();
84
+ modelUris.add(modelUri);
85
+ this.workspaceModelUrisByOntologyIri.set(ontologyIri, modelUris);
86
+ this.workspaceOntologyIriByModelUri.set(modelUri, ontologyIri);
87
+ this.workspaceNamespaceByModelUri.set(modelUri, namespace);
88
+ this.modelUriToOntologyIri.set(modelUri, ontologyIri);
89
+ if (modelUris.size === 1) {
90
+ this.ontologyIdentifierToModelUri.set(namespace, modelUri);
91
+ this.ontologyIdentifierToModelUri.set(ontologyIri, modelUri);
92
+ return;
93
+ }
94
+ this.ontologyIdentifierToModelUri.delete(namespace);
95
+ this.ontologyIdentifierToModelUri.delete(ontologyIri);
96
+ }
97
+
98
+ private unindexModelUri(modelUri: string): void {
99
+ for (const [identifier, uri] of this.ontologyIdentifierToModelUri.entries()) {
100
+ if (uri === modelUri) {
101
+ this.ontologyIdentifierToModelUri.delete(identifier);
102
+ }
103
+ }
104
+ const ontologyIri = this.workspaceOntologyIriByModelUri.get(modelUri);
105
+ if (ontologyIri) {
106
+ const modelUris = this.workspaceModelUrisByOntologyIri.get(ontologyIri);
107
+ modelUris?.delete(modelUri);
108
+ if (!modelUris || modelUris.size === 0) {
109
+ this.workspaceModelUrisByOntologyIri.delete(ontologyIri);
110
+ } else if (modelUris.size === 1) {
111
+ const survivingModelUri = [...modelUris][0];
112
+ const survivingNamespace = this.workspaceNamespaceByModelUri.get(survivingModelUri);
113
+ this.ontologyIdentifierToModelUri.set(ontologyIri, survivingModelUri);
114
+ if (survivingNamespace) {
115
+ this.ontologyIdentifierToModelUri.set(survivingNamespace, survivingModelUri);
116
+ }
117
+ }
118
+ this.workspaceOntologyIriByModelUri.delete(modelUri);
119
+ this.workspaceNamespaceByModelUri.delete(modelUri);
120
+ }
121
+ this.modelUriToOntologyIri.delete(modelUri);
122
+ }
123
+
124
+ getDuplicateWorkspaceModelUris(identifier: string): string[] {
125
+ this.ensureIndexedFromLoadedDocuments();
126
+ const normalized = this.normalizeNamespace(identifier);
127
+ if (!normalized) {
128
+ return [];
129
+ }
130
+ const ontologyIri = this.ontologyIriFromNamespace(normalized);
131
+ const modelUris = this.workspaceModelUrisByOntologyIri.get(ontologyIri);
132
+ if (!modelUris || modelUris.size <= 1) {
133
+ return [];
134
+ }
135
+ return [...modelUris].sort();
136
+ }
137
+
138
+ private normalizeNamespace(namespace: string): string {
139
+ return namespace.replace(/^<|>$/g, '');
140
+ }
141
+
142
+ private ontologyIriFromNamespace(namespace: string): string {
143
+ return namespace.replace(/[\/#]+$/, '');
144
+ }
145
+ }