@oml/language 0.13.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.
- package/out/oml/index.d.ts +3 -1
- package/out/oml/index.js +19 -1
- package/out/oml/index.js.map +1 -1
- package/out/oml/oml-candidates.d.ts +3 -3
- package/out/oml/oml-candidates.js +1 -1
- package/out/oml/oml-candidates.js.map +1 -1
- package/out/oml/oml-diagram.d.ts +17 -0
- package/out/oml/oml-diagram.js +1549 -0
- package/out/oml/oml-diagram.js.map +1 -0
- package/out/oml/oml-document.d.ts +5 -0
- package/out/oml/oml-document.js +12 -1
- package/out/oml/oml-document.js.map +1 -1
- package/out/oml/oml-index.d.ts +2 -1
- package/out/oml/oml-index.js +90 -9
- package/out/oml/oml-index.js.map +1 -1
- package/out/oml/oml-scope.js +4 -3
- package/out/oml/oml-scope.js.map +1 -1
- package/out/oml/oml-search.d.ts +24 -0
- package/out/oml/oml-search.js +95 -0
- package/out/oml/oml-search.js.map +1 -0
- package/out/oml/{oml-edit.d.ts → oml-update.d.ts} +2 -0
- package/out/oml/{oml-edit.js → oml-update.js} +256 -56
- package/out/oml/oml-update.js.map +1 -0
- package/out/oml/oml-utils.d.ts +1 -1
- package/out/oml/oml-utils.js +3 -4
- package/out/oml/oml-utils.js.map +1 -1
- package/out/oml/oml-validator.d.ts +1 -0
- package/out/oml/oml-validator.js +17 -2
- package/out/oml/oml-validator.js.map +1 -1
- package/package.json +4 -2
- package/src/oml/index.ts +32 -1
- package/src/oml/oml-candidates.ts +4 -4
- package/src/oml/oml-diagram.ts +1708 -0
- package/src/oml/oml-document.ts +13 -1
- package/src/oml/oml-index.ts +87 -9
- package/src/oml/oml-scope.ts +4 -3
- package/src/oml/oml-search.ts +132 -0
- package/src/oml/{oml-edit.ts → oml-update.ts} +302 -55
- package/src/oml/oml-utils.ts +3 -4
- package/src/oml/oml-validator.ts +17 -2
- package/out/oml/oml-edit.js.map +0 -1
package/src/oml/oml-document.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/src/oml/oml-index.ts
CHANGED
|
@@ -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
|
|
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
|
|
50
|
-
if (!
|
|
49
|
+
const indexedCandidates = this.workspaceModelUrisByOntologyIri.get(ontologyIri);
|
|
50
|
+
if (!indexedCandidates || indexedCandidates.size === 0) {
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
|
-
|
|
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)
|
|
252
|
+
if (!isOntology(root)) {
|
|
175
253
|
return;
|
|
176
254
|
}
|
|
177
255
|
|
package/src/oml/oml-scope.ts
CHANGED
|
@@ -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
|
+
}
|