@oml/language 0.12.0 → 0.14.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 (41) hide show
  1. package/out/oml/index.d.ts +3 -1
  2. package/out/oml/index.js +19 -1
  3. package/out/oml/index.js.map +1 -1
  4. package/out/oml/oml-candidates.d.ts +3 -3
  5. package/out/oml/oml-candidates.js +1 -1
  6. package/out/oml/oml-candidates.js.map +1 -1
  7. package/out/oml/oml-diagram.d.ts +17 -0
  8. package/out/oml/oml-diagram.js +1549 -0
  9. package/out/oml/oml-diagram.js.map +1 -0
  10. package/out/oml/oml-document.d.ts +5 -0
  11. package/out/oml/oml-document.js +12 -1
  12. package/out/oml/oml-document.js.map +1 -1
  13. package/out/oml/oml-index.d.ts +2 -1
  14. package/out/oml/oml-index.js +90 -9
  15. package/out/oml/oml-index.js.map +1 -1
  16. package/out/oml/oml-scope.js +4 -3
  17. package/out/oml/oml-scope.js.map +1 -1
  18. package/out/oml/oml-search.d.ts +24 -0
  19. package/out/oml/oml-search.js +95 -0
  20. package/out/oml/oml-search.js.map +1 -0
  21. package/out/oml/{oml-edit.d.ts → oml-update.d.ts} +2 -0
  22. package/out/oml/{oml-edit.js → oml-update.js} +270 -67
  23. package/out/oml/oml-update.js.map +1 -0
  24. package/out/oml/oml-utils.d.ts +1 -1
  25. package/out/oml/oml-utils.js +3 -4
  26. package/out/oml/oml-utils.js.map +1 -1
  27. package/out/oml/oml-validator.d.ts +1 -0
  28. package/out/oml/oml-validator.js +18 -3
  29. package/out/oml/oml-validator.js.map +1 -1
  30. package/package.json +4 -2
  31. package/src/oml/index.ts +32 -1
  32. package/src/oml/oml-candidates.ts +4 -4
  33. package/src/oml/oml-diagram.ts +1708 -0
  34. package/src/oml/oml-document.ts +13 -1
  35. package/src/oml/oml-index.ts +87 -9
  36. package/src/oml/oml-scope.ts +4 -3
  37. package/src/oml/oml-search.ts +132 -0
  38. package/src/oml/{oml-edit.ts → oml-update.ts} +317 -66
  39. package/src/oml/oml-utils.ts +3 -4
  40. package/src/oml/oml-validator.ts +18 -3
  41. package/out/oml/oml-edit.js.map +0 -1
@@ -5,6 +5,7 @@ import { DefaultDocumentUpdateHandler } from 'langium/lsp';
5
5
  import type { LangiumSharedServices } from 'langium/lsp';
6
6
  import { FileChangeType, type DidChangeWatchedFilesParams } from 'vscode-languageserver';
7
7
  import { getOntologyModelIndex } from './oml-index.js';
8
+ import { isIgnoredByOmlLsDocumentUri } from './oml-utils.js';
8
9
 
9
10
  export class OmlDocumentUpdateHandler extends DefaultDocumentUpdateHandler {
10
11
  private readonly services: LangiumSharedServices;
@@ -16,7 +17,18 @@ export class OmlDocumentUpdateHandler extends DefaultDocumentUpdateHandler {
16
17
 
17
18
  override didChangeWatchedFiles(params: DidChangeWatchedFilesParams): void {
18
19
  this.cleanDeletedOntologyDocuments(params);
19
- super.didChangeWatchedFiles(params);
20
+ const filtered = params.changes.filter((change) => !isIgnoredByOmlLsDocumentUri(change.uri));
21
+ if (filtered.length === 0) {
22
+ return;
23
+ }
24
+ super.didChangeWatchedFiles({ changes: filtered });
25
+ }
26
+
27
+ override didChangeContent(change: { document: { uri: string } }): void {
28
+ if (isIgnoredByOmlLsDocumentUri(change.document.uri)) {
29
+ return;
30
+ }
31
+ super.didChangeContent(change as any);
20
32
  }
21
33
 
22
34
  private cleanDeletedOntologyDocuments(params: DidChangeWatchedFilesParams): void {
@@ -3,7 +3,7 @@
3
3
  import { DocumentState, URI, type LangiumDocument } from 'langium';
4
4
  import type { LangiumSharedServices } from 'langium/lsp';
5
5
  import { isAspect, isConcept, isConceptInstance, isDescription, isOntology, isRelationEntity, isRelationInstance, isScalar } from './generated/ast.js';
6
- import { collectOntologyMembers, getIriForNode, isTransientEditorDocumentUri } from './oml-utils.js';
6
+ import { collectOntologyMembers, getIriForNode } from './oml-utils.js';
7
7
 
8
8
  const OML_LABEL_IRI = 'http://opencaesar.io/oml#label';
9
9
 
@@ -39,18 +39,99 @@ export class OmlIndex {
39
39
  });
40
40
  }
41
41
 
42
- resolveModelUri(identifier: string): string | undefined {
42
+ resolveModelUri(identifier: string, referencingDocumentUri?: string): string | undefined {
43
43
  this.ensureIndexedFromLoadedDocuments();
44
44
  const normalized = this.normalizeNamespace(identifier);
45
45
  if (!normalized) {
46
46
  return undefined;
47
47
  }
48
48
  const ontologyIri = this.ontologyIriFromNamespace(normalized);
49
- const candidates = this.workspaceModelUrisByOntologyIri.get(ontologyIri);
50
- if (!candidates || candidates.size !== 1) {
49
+ const indexedCandidates = this.workspaceModelUrisByOntologyIri.get(ontologyIri);
50
+ if (!indexedCandidates || indexedCandidates.size === 0) {
51
51
  return undefined;
52
52
  }
53
- return [...candidates][0];
53
+ const candidates = new Set<string>(indexedCandidates);
54
+ let reference: URI | undefined;
55
+ if (referencingDocumentUri) {
56
+ try {
57
+ reference = URI.parse(referencingDocumentUri);
58
+ } catch {
59
+ reference = undefined;
60
+ }
61
+ if (reference && candidates.size > 0) {
62
+ for (const candidate of [...candidates]) {
63
+ const synthesized = this.synthesizeCandidateForReferencingScheme(candidate, reference);
64
+ if (synthesized) {
65
+ candidates.add(synthesized);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ const candidateModelUris = [...candidates].sort();
71
+ if (candidateModelUris.length === 1) {
72
+ return candidateModelUris[0];
73
+ }
74
+ if (!referencingDocumentUri) {
75
+ return undefined;
76
+ }
77
+ if (!reference) {
78
+ return undefined;
79
+ }
80
+ const sameSchemeCandidates = candidateModelUris.filter((candidate) => {
81
+ try {
82
+ return URI.parse(candidate).scheme === reference.scheme;
83
+ } catch {
84
+ return false;
85
+ }
86
+ });
87
+ if (sameSchemeCandidates.length === 1) {
88
+ return sameSchemeCandidates[0];
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ private synthesizeCandidateForReferencingScheme(candidateModelUri: string, reference: URI): string | undefined {
94
+ let candidate: URI;
95
+ try {
96
+ candidate = URI.parse(candidateModelUri);
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ if (candidate.scheme === reference.scheme) {
101
+ return undefined;
102
+ }
103
+ if (reference.scheme !== 'git' || candidate.scheme !== 'file') {
104
+ return undefined;
105
+ }
106
+ const refQuery = reference.query?.trim();
107
+ if (!refQuery) {
108
+ return undefined;
109
+ }
110
+ let parsedQuery: any;
111
+ try {
112
+ parsedQuery = JSON.parse(refQuery);
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ if (typeof parsedQuery !== 'object' || parsedQuery === null) {
117
+ return undefined;
118
+ }
119
+ const ref = typeof parsedQuery.ref === 'string' ? parsedQuery.ref : undefined;
120
+ if (!ref) {
121
+ return undefined;
122
+ }
123
+ const gitQuery = JSON.stringify({
124
+ ...parsedQuery,
125
+ path: candidate.path,
126
+ ref,
127
+ });
128
+ return URI.from({
129
+ scheme: 'git',
130
+ authority: candidate.authority,
131
+ path: candidate.path,
132
+ query: gitQuery,
133
+ fragment: undefined,
134
+ }).toString();
54
135
  }
55
136
 
56
137
  resolveOntologyIri(modelUri: string): string | undefined {
@@ -156,9 +237,6 @@ export class OmlIndex {
156
237
  if (!namespace) {
157
238
  return;
158
239
  }
159
- if (isTransientEditorDocumentUri(modelUri)) {
160
- return;
161
- }
162
240
  const modelUris = this.workspaceModelUrisByOntologyIri.get(ontologyIri) ?? new Set<string>();
163
241
  modelUris.add(modelUri);
164
242
  this.workspaceModelUrisByOntologyIri.set(ontologyIri, modelUris);
@@ -171,7 +249,7 @@ export class OmlIndex {
171
249
  this.removeTypesForModelUri(modelUri);
172
250
  this.removeTypeHierarchyForModelUri(modelUri);
173
251
  const root = document.parseResult?.value;
174
- if (!isOntology(root) || isTransientEditorDocumentUri(modelUri)) {
252
+ if (!isOntology(root)) {
175
253
  return;
176
254
  }
177
255
 
@@ -208,22 +208,23 @@ export class OmlScopeProvider extends DefaultScopeProvider {
208
208
  };
209
209
  }
210
210
 
211
- private findOntologyByNamespace(normalizedNamespace: string): WorkspaceOntology | undefined {
211
+ private findOntologyByNamespace(normalizedNamespace: string, referencingDocumentUri?: string): WorkspaceOntology | undefined {
212
212
  const index = getOntologyModelIndex(this.services.shared as LangiumSharedServices);
213
- const modelUri = index.resolveModelUri(normalizedNamespace);
213
+ const modelUri = index.resolveModelUri(normalizedNamespace, referencingDocumentUri);
214
214
  return modelUri ? this.resolveOntologyDocument(modelUri) : undefined;
215
215
  }
216
216
 
217
217
  private resolveImports(ontology: Ontology): ResolvedImport[] {
218
218
  const imports = ((ontology as any).ownedImports ?? []) as Import[];
219
219
  const resolved: ResolvedImport[] = [];
220
+ const referencingDocumentUri = ontology.$document?.uri?.toString?.();
220
221
  for (const imp of imports) {
221
222
  const ns = this.getImportNamespace(imp);
222
223
  const normalized = normalizeNamespace(ns);
223
224
  if (!normalized) {
224
225
  continue;
225
226
  }
226
- const target = this.findOntologyByNamespace(normalized);
227
+ const target = this.findOntologyByNamespace(normalized, referencingDocumentUri);
227
228
  if (!target) {
228
229
  continue;
229
230
  }
@@ -0,0 +1,132 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ export interface OmlFuzzyIndexedEntry {
4
+ iri: string;
5
+ label?: string;
6
+ fragment: string;
7
+ fragmentTokens: string[];
8
+ labelTokens: string[];
9
+ }
10
+
11
+ export interface OmlFuzzyDiagnostics {
12
+ fragment: string;
13
+ label: string;
14
+ exactFragment: boolean;
15
+ exactLabel: boolean;
16
+ containsInputInFragment: boolean;
17
+ containsInputInLabel: boolean;
18
+ regexMatchFragment: boolean;
19
+ regexMatchLabel: boolean;
20
+ fragmentTokenHits: number;
21
+ labelTokenHits: number;
22
+ }
23
+
24
+ export function iriFragment(iri: string): string {
25
+ return iri.split(/[#/]/).pop() ?? '';
26
+ }
27
+
28
+ export function tokenizeForFuzzy(text: string): string[] {
29
+ return splitCamelCase(text)
30
+ .toLowerCase()
31
+ .split(/[^a-z0-9]+/g)
32
+ .map((token) => token.trim())
33
+ .filter((token) => token.length > 0);
34
+ }
35
+
36
+ export function buildSearchRegex(text: string, regexSource: string, useRegex: boolean): RegExp | undefined {
37
+ const source = useRegex ? regexSource : '';
38
+ if (!source) {
39
+ return undefined;
40
+ }
41
+ try {
42
+ return new RegExp(source, 'i');
43
+ } catch {
44
+ return new RegExp(escapeRegExp(text), 'i');
45
+ }
46
+ }
47
+
48
+ export function fuzzyDiagnostics(
49
+ input: string,
50
+ tokens: string[],
51
+ entry: OmlFuzzyIndexedEntry,
52
+ regex: RegExp | undefined,
53
+ ): OmlFuzzyDiagnostics {
54
+ const fragment = entry.fragment.toLowerCase();
55
+ const lowerLabel = (entry.label ?? '').toLowerCase();
56
+ const lowerInput = input.toLowerCase();
57
+ const exactFragment = fragment === lowerInput;
58
+ const exactLabel = lowerLabel === lowerInput;
59
+ const containsInputInFragment = fragment.includes(lowerInput);
60
+ const containsInputInLabel = lowerLabel.includes(lowerInput);
61
+ const regexMatchFragment = regex ? regex.test(entry.fragment) : false;
62
+ const regexMatchLabel = regex ? regex.test(entry.label ?? '') : false;
63
+ const fragmentTokenSet = new Set(entry.fragmentTokens);
64
+ const labelTokenSet = new Set(entry.labelTokens);
65
+ let fragmentTokenHits = 0;
66
+ let labelTokenHits = 0;
67
+ for (const token of tokens) {
68
+ if (fragmentTokenSet.has(token)) {
69
+ fragmentTokenHits += 1;
70
+ }
71
+ if (labelTokenSet.has(token)) {
72
+ labelTokenHits += 1;
73
+ }
74
+ }
75
+ return {
76
+ fragment,
77
+ label: lowerLabel,
78
+ exactFragment,
79
+ exactLabel,
80
+ containsInputInFragment,
81
+ containsInputInLabel,
82
+ regexMatchFragment,
83
+ regexMatchLabel,
84
+ fragmentTokenHits,
85
+ labelTokenHits,
86
+ };
87
+ }
88
+
89
+ export function fuzzyScore(
90
+ diagnostics: OmlFuzzyDiagnostics,
91
+ queryTokens: string[],
92
+ fragmentTokens: string[],
93
+ labelTokens: string[],
94
+ usedRegex: boolean,
95
+ ): number {
96
+ const fragmentSet = new Set(fragmentTokens);
97
+ const labelSet = new Set(labelTokens);
98
+ let uniqueTokenHits = 0;
99
+ for (const token of queryTokens) {
100
+ if (fragmentSet.has(token) || labelSet.has(token)) {
101
+ uniqueTokenHits += 1;
102
+ }
103
+ }
104
+ const allTokensMatchedInFragment = queryTokens.length > 0 && queryTokens.every((token) => fragmentSet.has(token));
105
+ let score = 0;
106
+ if (diagnostics.exactFragment || diagnostics.exactLabel) {
107
+ score += 200;
108
+ }
109
+ if (diagnostics.containsInputInFragment || diagnostics.containsInputInLabel) {
110
+ score += 80;
111
+ }
112
+ if (usedRegex && (diagnostics.regexMatchFragment || diagnostics.regexMatchLabel)) {
113
+ score += diagnostics.regexMatchFragment ? 120 : 70;
114
+ }
115
+ if (allTokensMatchedInFragment && diagnostics.fragmentTokenHits >= 2) {
116
+ score += 80;
117
+ }
118
+ score += uniqueTokenHits * 60;
119
+ score += diagnostics.fragmentTokenHits * 20;
120
+ score += diagnostics.labelTokenHits * 10;
121
+ return score;
122
+ }
123
+
124
+ function splitCamelCase(value: string): string {
125
+ return value
126
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
127
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
128
+ }
129
+
130
+ function escapeRegExp(value: string): string {
131
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
132
+ }