@oml/language 0.14.17 → 0.15.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/generated/ast.d.ts +1 -1
- package/out/oml/generated/ast.js +1 -1
- package/out/oml/generated/grammar.d.ts +1 -1
- package/out/oml/generated/grammar.js +1 -1
- package/out/oml/generated/module.d.ts +1 -1
- package/out/oml/generated/module.js +1 -1
- package/out/oml/oml-candidates.js +1 -42
- package/out/oml/oml-candidates.js.map +1 -1
- package/out/oml/oml-document.js +1 -5
- package/out/oml/oml-document.js.map +1 -1
- package/out/oml/oml-index.d.ts +1 -0
- package/out/oml/oml-index.js +8 -1
- package/out/oml/oml-index.js.map +1 -1
- package/out/oml/oml-scope.js +10 -4
- package/out/oml/oml-scope.js.map +1 -1
- package/out/oml/oml-update.d.ts +32 -15
- package/out/oml/oml-update.js +393 -199
- package/out/oml/oml-update.js.map +1 -1
- package/out/oml/oml-utils.d.ts +18 -0
- package/out/oml/oml-utils.js +183 -26
- package/out/oml/oml-utils.js.map +1 -1
- package/out/oml/oml-workspace.js +1 -4
- package/out/oml/oml-workspace.js.map +1 -1
- package/package.json +1 -1
- package/src/oml/generated/ast.ts +1 -1
- package/src/oml/generated/grammar.ts +1 -1
- package/src/oml/generated/module.ts +1 -1
- package/src/oml/oml-candidates.ts +1 -46
- package/src/oml/oml-document.ts +1 -5
- package/src/oml/oml-index.ts +9 -1
- package/src/oml/oml-scope.ts +10 -4
- package/src/oml/oml-update.ts +459 -223
- package/src/oml/oml-utils.ts +199 -26
- package/src/oml/oml-workspace.ts +1 -4
package/out/oml/oml-update.js
CHANGED
|
@@ -1,26 +1,80 @@
|
|
|
1
1
|
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
2
|
import { DocumentState, URI } from 'langium';
|
|
3
3
|
import { RequestType } from 'vscode-languageserver-protocol';
|
|
4
|
-
import { isAnnotation, isConceptInstance, isDescription, isDescriptionBox, isImport, isOntology, isPropertyValueAssertion, isRelationInstance, isRule, isTypeAssertion, isVocabulary, isVocabularyBundle, } from './generated/ast.js';
|
|
4
|
+
import { isAnnotation, isConceptInstance, isDescription, isDescriptionBundle, isDescriptionBox, isImport, isOntology, isPropertyValueAssertion, isRelationInstance, isRule, isTypeAssertion, isVocabulary, isVocabularyBundle, } from './generated/ast.js';
|
|
5
5
|
import { getOntologyModelIndex } from './oml-index.js';
|
|
6
6
|
import { serializeOntology } from './oml-serializer.js';
|
|
7
|
-
import { collectOntologyMembers, findOntologyMemberByName, getIriForNode, normalizeNamespace, } from './oml-utils.js';
|
|
8
|
-
const
|
|
7
|
+
import { collectOntologyMembers, expandRefTextToIri, findOntologyMemberByName, getLocalNameForNamespace, getRefText, getIriForNode, isSameIriTarget, localNameFromRefText, normalizeIri, normalizeNamespace, parseIriParts, pickImportPrefix, resolveImportNamespaceRaw, resolveImportPrefix, withNamespaceSeparator, } from './oml-utils.js';
|
|
8
|
+
const OmlUpdateRequestType = new RequestType('oml/update');
|
|
9
9
|
const RDF_TYPE_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
10
10
|
const OML_HAS_SOURCE_IRI = 'http://opencaesar.io/oml#hasSource';
|
|
11
11
|
const OML_HAS_TARGET_IRI = 'http://opencaesar.io/oml#hasTarget';
|
|
12
12
|
const XSD_STRING_IRI = 'http://www.w3.org/2001/XMLSchema#string';
|
|
13
|
-
export async function applyOmlUpdate(shared, params, logError) {
|
|
13
|
+
export async function applyOmlUpdate(shared, params, logError, workspaceUri, preview) {
|
|
14
14
|
const operations = Array.isArray(params?.operations) ? params.operations : [];
|
|
15
|
-
const referencingUri = typeof params?.referencingUri === 'string'
|
|
16
|
-
? params.referencingUri.trim()
|
|
17
|
-
: undefined;
|
|
18
15
|
const contexts = new Map();
|
|
19
16
|
const changedOntologyIris = new Set();
|
|
17
|
+
const createdOntologyByIri = new Map();
|
|
18
|
+
const deletedOntologyModelUriByIri = new Map();
|
|
20
19
|
for (let i = 0; i < operations.length; i += 1) {
|
|
21
20
|
const operation = operations[i];
|
|
22
21
|
try {
|
|
23
|
-
|
|
22
|
+
if (operation.kind === 'createOntology') {
|
|
23
|
+
const created = await createOntologyOperation(shared, operation, workspaceUri);
|
|
24
|
+
contexts.set(created.ontologyIri, {
|
|
25
|
+
ontologyIri: created.ontologyIri,
|
|
26
|
+
modelUri: created.modelUri,
|
|
27
|
+
ontology: created.ontology,
|
|
28
|
+
document: undefined
|
|
29
|
+
});
|
|
30
|
+
createdOntologyByIri.set(created.ontologyIri, created);
|
|
31
|
+
changedOntologyIris.add(created.ontologyIri);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (operation.kind === 'deleteOntology') {
|
|
35
|
+
const normalizedOntologyIri = normalizeOntologyIriKey(operation.ontologyIri);
|
|
36
|
+
if (createdOntologyByIri.has(normalizedOntologyIri)) {
|
|
37
|
+
createdOntologyByIri.delete(normalizedOntologyIri);
|
|
38
|
+
contexts.delete(normalizedOntologyIri);
|
|
39
|
+
changedOntologyIris.delete(normalizedOntologyIri);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const existingContext = contexts.get(normalizedOntologyIri);
|
|
43
|
+
if (existingContext) {
|
|
44
|
+
deletedOntologyModelUriByIri.set(existingContext.ontologyIri, existingContext.modelUri);
|
|
45
|
+
contexts.delete(existingContext.ontologyIri);
|
|
46
|
+
changedOntologyIris.delete(existingContext.ontologyIri);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const modelUri = resolveModelUriFromLoadedDocuments(shared, normalizedOntologyIri);
|
|
50
|
+
if (!modelUri) {
|
|
51
|
+
// Idempotent delete: if ontology is already absent, treat as successful no-op.
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
deletedOntologyModelUriByIri.set(normalizedOntologyIri, modelUri);
|
|
55
|
+
contexts.delete(normalizedOntologyIri);
|
|
56
|
+
changedOntologyIris.delete(normalizedOntologyIri);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (operation.kind === 'addImport') {
|
|
60
|
+
const importing = await resolveOntologyContext(shared, operation.importingIri, contexts, workspaceUri);
|
|
61
|
+
const imported = await resolveOntologyContext(shared, operation.importedIri, contexts, workspaceUri);
|
|
62
|
+
const changed = await addImport(shared, importing.ontology, imported.ontology);
|
|
63
|
+
if (changed) {
|
|
64
|
+
changedOntologyIris.add(importing.ontologyIri);
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (operation.kind === 'removeImport') {
|
|
69
|
+
const importing = await resolveOntologyContext(shared, operation.importingIri, contexts, workspaceUri);
|
|
70
|
+
const imported = await resolveOntologyContext(shared, operation.importedIri, contexts, workspaceUri);
|
|
71
|
+
const changed = removeImport(importing.ontology, imported.ontology);
|
|
72
|
+
if (changed) {
|
|
73
|
+
changedOntologyIris.add(importing.ontologyIri);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const context = await resolveOntologyContext(shared, getOperationTargetOntologyIri(operation), contexts, workspaceUri);
|
|
24
78
|
const changed = await executeOperation(shared, context, operation, contexts);
|
|
25
79
|
for (const ontologyIri of changed) {
|
|
26
80
|
changedOntologyIris.add(ontologyIri);
|
|
@@ -32,7 +86,7 @@ export async function applyOmlUpdate(shared, params, logError) {
|
|
|
32
86
|
const details = error instanceof Error && error.stack
|
|
33
87
|
? `${message}\n${error.stack}`
|
|
34
88
|
: message;
|
|
35
|
-
logError?.(`[oml]
|
|
89
|
+
logError?.(`[oml] OmlUpdateRequest failed at operation ${i} (${operation.kind}): ${details}`);
|
|
36
90
|
return {
|
|
37
91
|
errors: [{
|
|
38
92
|
operationIndex: i,
|
|
@@ -41,16 +95,62 @@ export async function applyOmlUpdate(shared, params, logError) {
|
|
|
41
95
|
};
|
|
42
96
|
}
|
|
43
97
|
}
|
|
44
|
-
const edit = buildWorkspaceEdit(contexts, changedOntologyIris);
|
|
98
|
+
const edit = buildWorkspaceEdit(contexts, changedOntologyIris, createdOntologyByIri, deletedOntologyModelUriByIri);
|
|
99
|
+
if (preview) {
|
|
100
|
+
const existingContexts = new Map([...contexts.entries()].filter(([iri]) => !createdOntologyByIri.has(iri)));
|
|
101
|
+
await restoreContextsFromDocuments(shared, existingContexts);
|
|
102
|
+
}
|
|
45
103
|
return edit ? { edit } : {};
|
|
46
104
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
105
|
+
function resolveModelUriFromLoadedDocuments(shared, ontologyIri) {
|
|
106
|
+
const normalized = normalizeOntologyIriKey(ontologyIri);
|
|
107
|
+
const documentsService = shared.workspace.LangiumDocuments;
|
|
108
|
+
const all = documentsService?.all ?? [];
|
|
109
|
+
const iterable = Array.isArray(all)
|
|
110
|
+
? all
|
|
111
|
+
: (typeof all?.toArray === 'function' ? all.toArray() : Array.from(all));
|
|
112
|
+
for (const document of iterable) {
|
|
113
|
+
const root = document?.parseResult?.value;
|
|
114
|
+
if (!root || !isOntology(root)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const namespace = normalizeNamespace(String(root.namespace ?? ''));
|
|
118
|
+
if (!namespace) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (normalizeOntologyIriKey(namespace) === normalized) {
|
|
122
|
+
return String(document.uri);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
export function registerOmlUpdateRequests(connection, shared) {
|
|
128
|
+
connection.onRequest(OmlUpdateRequestType, async (params) => {
|
|
129
|
+
return await applyOmlUpdate(shared, params, (message) => connection.console?.error(message), params.referencingUri);
|
|
50
130
|
});
|
|
51
131
|
}
|
|
132
|
+
function getOperationTargetOntologyIri(operation) {
|
|
133
|
+
switch (operation.kind) {
|
|
134
|
+
case 'createInstance':
|
|
135
|
+
case 'createRelationInstance':
|
|
136
|
+
case 'createInstanceRef':
|
|
137
|
+
case 'createRelationInstanceRef':
|
|
138
|
+
case 'addAssertion':
|
|
139
|
+
case 'updateAssertion':
|
|
140
|
+
case 'removeAssertion':
|
|
141
|
+
return operation.descriptionIri;
|
|
142
|
+
case 'addAnnotation':
|
|
143
|
+
case 'updateAnnotation':
|
|
144
|
+
case 'removeAnnotation':
|
|
145
|
+
case 'deleteMemberCascade':
|
|
146
|
+
case 'deleteMemberRef':
|
|
147
|
+
return operation.ontologyIri;
|
|
148
|
+
default:
|
|
149
|
+
throw new Error(`Unsupported operation target resolution for '${operation.kind}'.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
52
152
|
async function resolveOntologyContext(shared, ontologyIri, cache, referencingUri) {
|
|
53
|
-
const normalizedOntologyIri =
|
|
153
|
+
const normalizedOntologyIri = normalizeOntologyIriKey(ontologyIri);
|
|
54
154
|
const cached = cache.get(normalizedOntologyIri);
|
|
55
155
|
if (cached) {
|
|
56
156
|
return cached;
|
|
@@ -933,88 +1033,6 @@ function toRefText(ontology, iri) {
|
|
|
933
1033
|
}
|
|
934
1034
|
return `<${normalizedIri}>`;
|
|
935
1035
|
}
|
|
936
|
-
function getLocalNameForNamespace(iri, rawNamespace) {
|
|
937
|
-
const namespace = normalizeNamespace(String(rawNamespace ?? ''));
|
|
938
|
-
if (!namespace) {
|
|
939
|
-
return undefined;
|
|
940
|
-
}
|
|
941
|
-
const hashPrefix = `${namespace}#`;
|
|
942
|
-
if (iri.startsWith(hashPrefix) && iri.length > hashPrefix.length) {
|
|
943
|
-
return iri.slice(hashPrefix.length);
|
|
944
|
-
}
|
|
945
|
-
const slashPrefix = `${namespace}/`;
|
|
946
|
-
if (iri.startsWith(slashPrefix) && iri.length > slashPrefix.length) {
|
|
947
|
-
return iri.slice(slashPrefix.length);
|
|
948
|
-
}
|
|
949
|
-
return undefined;
|
|
950
|
-
}
|
|
951
|
-
function resolveImportPrefix(imp) {
|
|
952
|
-
const explicitPrefix = typeof imp?.prefix === 'string' ? imp.prefix.trim() : '';
|
|
953
|
-
if (explicitPrefix) {
|
|
954
|
-
return explicitPrefix;
|
|
955
|
-
}
|
|
956
|
-
const resolvedPrefix = typeof imp?.imported?.ref?.prefix === 'string' ? imp.imported.ref.prefix.trim() : '';
|
|
957
|
-
return resolvedPrefix || undefined;
|
|
958
|
-
}
|
|
959
|
-
function resolveImportNamespaceRaw(imp) {
|
|
960
|
-
const resolvedNamespace = typeof imp?.imported?.ref?.namespace === 'string' ? imp.imported.ref.namespace : '';
|
|
961
|
-
if (resolvedNamespace.trim().length > 0) {
|
|
962
|
-
return resolvedNamespace.trim();
|
|
963
|
-
}
|
|
964
|
-
const refText = typeof imp?.imported?.$refText === 'string' ? imp.imported.$refText.trim() : '';
|
|
965
|
-
return refText || undefined;
|
|
966
|
-
}
|
|
967
|
-
function expandRefTextToIri(ontology, refText) {
|
|
968
|
-
const trimmed = String(refText ?? '').trim();
|
|
969
|
-
if (!trimmed) {
|
|
970
|
-
return undefined;
|
|
971
|
-
}
|
|
972
|
-
if (trimmed === 'a') {
|
|
973
|
-
return RDF_TYPE_IRI;
|
|
974
|
-
}
|
|
975
|
-
if (trimmed.startsWith('<') && trimmed.endsWith('>')) {
|
|
976
|
-
return normalizeIri(trimmed);
|
|
977
|
-
}
|
|
978
|
-
if (trimmed.includes('://')) {
|
|
979
|
-
return normalizeIri(trimmed);
|
|
980
|
-
}
|
|
981
|
-
const localInCurrentOntology = expandLocalRefText(ontology?.namespace, trimmed);
|
|
982
|
-
if (localInCurrentOntology) {
|
|
983
|
-
return localInCurrentOntology;
|
|
984
|
-
}
|
|
985
|
-
const separatorIndex = trimmed.indexOf(':');
|
|
986
|
-
if (separatorIndex <= 0) {
|
|
987
|
-
return undefined;
|
|
988
|
-
}
|
|
989
|
-
const prefix = trimmed.slice(0, separatorIndex);
|
|
990
|
-
const local = trimmed.slice(separatorIndex + 1);
|
|
991
|
-
if (!local) {
|
|
992
|
-
return undefined;
|
|
993
|
-
}
|
|
994
|
-
if (typeof ontology?.prefix === 'string' && ontology.prefix.trim() === prefix) {
|
|
995
|
-
return expandLocalRefText(ontology.namespace, local);
|
|
996
|
-
}
|
|
997
|
-
const imports = Array.isArray(ontology?.ownedImports) ? ontology.ownedImports : [];
|
|
998
|
-
for (const imp of imports) {
|
|
999
|
-
if (resolveImportPrefix(imp) !== prefix) {
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1002
|
-
return expandLocalRefText(resolveImportNamespaceRaw(imp), local);
|
|
1003
|
-
}
|
|
1004
|
-
return undefined;
|
|
1005
|
-
}
|
|
1006
|
-
function expandLocalRefText(rawNamespace, localName) {
|
|
1007
|
-
const namespaceText = String(rawNamespace ?? '').trim();
|
|
1008
|
-
const local = String(localName ?? '').trim();
|
|
1009
|
-
if (!namespaceText || !local) {
|
|
1010
|
-
return undefined;
|
|
1011
|
-
}
|
|
1012
|
-
const namespace = normalizeIri(namespaceText);
|
|
1013
|
-
if (namespace.endsWith('#') || namespace.endsWith('/')) {
|
|
1014
|
-
return `${namespace}${local}`;
|
|
1015
|
-
}
|
|
1016
|
-
return `${namespace}#${local}`;
|
|
1017
|
-
}
|
|
1018
1036
|
async function ensureReferenceImport(shared, ontology, iri) {
|
|
1019
1037
|
const target = parseIriParts(iri);
|
|
1020
1038
|
if (!target) {
|
|
@@ -1025,23 +1043,28 @@ async function ensureReferenceImport(shared, ontology, iri) {
|
|
|
1025
1043
|
return;
|
|
1026
1044
|
}
|
|
1027
1045
|
const imports = Array.isArray(ontology?.ownedImports) ? ontology.ownedImports : [];
|
|
1028
|
-
const hasImport = imports.some((imp) => {
|
|
1029
|
-
const importedNamespace = normalizeNamespace(String(resolveImportNamespaceRaw(imp) ?? ''));
|
|
1030
|
-
return importedNamespace === target.namespace;
|
|
1031
|
-
});
|
|
1032
|
-
if (hasImport) {
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
1046
|
const usedPrefixes = new Set();
|
|
1036
1047
|
const rootPrefix = typeof ontology?.prefix === 'string' ? ontology.prefix.trim() : '';
|
|
1037
1048
|
if (rootPrefix) {
|
|
1038
1049
|
usedPrefixes.add(rootPrefix);
|
|
1039
1050
|
}
|
|
1040
1051
|
for (const imp of imports) {
|
|
1041
|
-
const
|
|
1042
|
-
if (
|
|
1043
|
-
usedPrefixes.add(
|
|
1052
|
+
const p = resolveImportPrefix(imp);
|
|
1053
|
+
if (p) {
|
|
1054
|
+
usedPrefixes.add(p);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const existingImport = imports.find((imp) => {
|
|
1058
|
+
const importedNamespace = normalizeNamespace(String(resolveImportNamespaceRaw(imp) ?? ''));
|
|
1059
|
+
return importedNamespace === target.namespace;
|
|
1060
|
+
});
|
|
1061
|
+
if (existingImport) {
|
|
1062
|
+
// If the existing import has no prefix (e.g., a bundle `includes` added without one),
|
|
1063
|
+
// patch it now so that the reference can use the abbreviated form.
|
|
1064
|
+
if (!resolveImportPrefix(existingImport)) {
|
|
1065
|
+
existingImport.prefix = pickImportPrefix(target.namespace, usedPrefixes);
|
|
1044
1066
|
}
|
|
1067
|
+
return;
|
|
1045
1068
|
}
|
|
1046
1069
|
const prefix = pickImportPrefix(target.namespace, usedPrefixes);
|
|
1047
1070
|
const importedRef = `<${target.namespace}${target.separator}>`;
|
|
@@ -1054,15 +1077,93 @@ async function ensureReferenceImport(shared, ontology, iri) {
|
|
|
1054
1077
|
};
|
|
1055
1078
|
pushAstArrayChild(ontology, 'ownedImports', importStatement);
|
|
1056
1079
|
}
|
|
1057
|
-
async function
|
|
1058
|
-
const
|
|
1080
|
+
async function addImport(shared, importingOntology, importedOntology) {
|
|
1081
|
+
const importingNamespace = normalizeNamespace(String(importingOntology?.namespace ?? ''));
|
|
1082
|
+
const importedNamespaceRaw = String(importedOntology?.namespace ?? '').trim();
|
|
1083
|
+
const importedNamespace = normalizeNamespace(importedNamespaceRaw);
|
|
1084
|
+
if (!importingNamespace || !importedNamespace) {
|
|
1085
|
+
throw new Error('addImport requires both importing and imported ontologies to have valid namespaces.');
|
|
1086
|
+
}
|
|
1087
|
+
if (importingNamespace === importedNamespace) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
const imports = Array.isArray(importingOntology?.ownedImports) ? importingOntology.ownedImports : [];
|
|
1091
|
+
const hasImport = imports.some((imp) => {
|
|
1092
|
+
const imported = normalizeNamespace(String(resolveImportNamespaceRaw(imp) ?? ''));
|
|
1093
|
+
return imported === importedNamespace;
|
|
1094
|
+
});
|
|
1095
|
+
if (hasImport) {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
const separator = importedNamespaceRaw.endsWith('/') ? '/' : '#';
|
|
1099
|
+
const importedRef = `<${importedNamespace}${separator}>`;
|
|
1100
|
+
const importKind = await resolveImportKind(shared, importingOntology, importedNamespace, importedOntology);
|
|
1101
|
+
const usedPrefixes = new Set();
|
|
1102
|
+
const rootPrefix = typeof importingOntology?.prefix === 'string' ? importingOntology.prefix.trim() : '';
|
|
1103
|
+
if (rootPrefix) {
|
|
1104
|
+
usedPrefixes.add(rootPrefix);
|
|
1105
|
+
}
|
|
1106
|
+
for (const imp of imports) {
|
|
1107
|
+
const p = resolveImportPrefix(imp);
|
|
1108
|
+
if (p) {
|
|
1109
|
+
usedPrefixes.add(p);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// `includes` never gets a prefix — if an annotation later references a term from this
|
|
1113
|
+
// vocabulary, ensureReferenceImport will patch the prefix onto this import at that point.
|
|
1114
|
+
let prefix;
|
|
1115
|
+
if (importKind !== 'includes') {
|
|
1116
|
+
const candidatePrefix = typeof importedOntology?.prefix === 'string' ? importedOntology.prefix.trim() : '';
|
|
1117
|
+
prefix = candidatePrefix && !usedPrefixes.has(candidatePrefix)
|
|
1118
|
+
? candidatePrefix
|
|
1119
|
+
: pickImportPrefix(importedNamespace, usedPrefixes);
|
|
1120
|
+
}
|
|
1121
|
+
const importStatement = {
|
|
1122
|
+
$type: 'Import',
|
|
1123
|
+
kind: importKind,
|
|
1124
|
+
...(prefix ? { prefix } : {}),
|
|
1125
|
+
imported: { $refText: importedRef }
|
|
1126
|
+
};
|
|
1127
|
+
pushAstArrayChild(importingOntology, 'ownedImports', importStatement);
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
function removeImport(importingOntology, importedOntology) {
|
|
1131
|
+
const importedNamespace = normalizeNamespace(String(importedOntology?.namespace ?? ''));
|
|
1132
|
+
if (!importedNamespace) {
|
|
1133
|
+
throw new Error('removeImport requires imported ontology to have a valid namespace.');
|
|
1134
|
+
}
|
|
1135
|
+
const imports = Array.isArray(importingOntology?.ownedImports) ? importingOntology.ownedImports : [];
|
|
1136
|
+
if (imports.length === 0) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const kept = imports.filter((imp) => {
|
|
1140
|
+
const namespace = normalizeNamespace(String(resolveImportNamespaceRaw(imp) ?? ''));
|
|
1141
|
+
return namespace !== importedNamespace;
|
|
1142
|
+
});
|
|
1143
|
+
if (kept.length === imports.length) {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
importingOntology.ownedImports = kept;
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
async function resolveImportKind(shared, importing, importedNamespace, importedOntology) {
|
|
1150
|
+
const imported = importedOntology ?? await findOntologyByNamespace(shared, importedNamespace);
|
|
1059
1151
|
if (imported) {
|
|
1060
1152
|
if (importing.$type === imported.$type) {
|
|
1061
1153
|
return 'extends';
|
|
1062
1154
|
}
|
|
1155
|
+
if (isDescriptionBundle(importing) && isDescription(imported)) {
|
|
1156
|
+
return 'includes';
|
|
1157
|
+
}
|
|
1063
1158
|
if (isVocabulary(importing) && isDescription(imported)) {
|
|
1064
1159
|
return 'uses';
|
|
1065
1160
|
}
|
|
1161
|
+
if (isDescriptionBundle(importing) && isVocabularyBundle(imported)) {
|
|
1162
|
+
return 'uses';
|
|
1163
|
+
}
|
|
1164
|
+
if (isDescriptionBundle(importing) && isVocabulary(imported)) {
|
|
1165
|
+
return 'uses';
|
|
1166
|
+
}
|
|
1066
1167
|
if (isDescriptionBox(importing) && isVocabulary(imported)) {
|
|
1067
1168
|
return 'uses';
|
|
1068
1169
|
}
|
|
@@ -1093,41 +1194,6 @@ async function findOntologyByNamespace(shared, namespace) {
|
|
|
1093
1194
|
const ontology = document?.parseResult?.value;
|
|
1094
1195
|
return ontology && isOntology(ontology) ? ontology : undefined;
|
|
1095
1196
|
}
|
|
1096
|
-
function parseIriParts(iri) {
|
|
1097
|
-
const normalized = normalizeIri(iri);
|
|
1098
|
-
if (!normalized) {
|
|
1099
|
-
return undefined;
|
|
1100
|
-
}
|
|
1101
|
-
const hashIndex = normalized.lastIndexOf('#');
|
|
1102
|
-
if (hashIndex > 0 && hashIndex < normalized.length - 1) {
|
|
1103
|
-
return {
|
|
1104
|
-
namespace: normalizeNamespace(normalized.slice(0, hashIndex)),
|
|
1105
|
-
fragment: normalized.slice(hashIndex + 1),
|
|
1106
|
-
separator: '#',
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
const slashIndex = normalized.lastIndexOf('/');
|
|
1110
|
-
if (slashIndex > 0 && slashIndex < normalized.length - 1) {
|
|
1111
|
-
return {
|
|
1112
|
-
namespace: normalizeNamespace(normalized.slice(0, slashIndex)),
|
|
1113
|
-
fragment: normalized.slice(slashIndex + 1),
|
|
1114
|
-
separator: '/',
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
return undefined;
|
|
1118
|
-
}
|
|
1119
|
-
function getRefText(ref) {
|
|
1120
|
-
if (typeof ref === 'string') {
|
|
1121
|
-
return ref;
|
|
1122
|
-
}
|
|
1123
|
-
if (typeof ref?.$refText === 'string') {
|
|
1124
|
-
return ref.$refText;
|
|
1125
|
-
}
|
|
1126
|
-
if (typeof ref?.$refNode?.text === 'string') {
|
|
1127
|
-
return ref.$refNode.text;
|
|
1128
|
-
}
|
|
1129
|
-
return undefined;
|
|
1130
|
-
}
|
|
1131
1197
|
function matchesRefTextTarget(ontology, statement, namespace, fragment) {
|
|
1132
1198
|
const refText = getRefText(statement?.ref);
|
|
1133
1199
|
if (!refText) {
|
|
@@ -1178,56 +1244,6 @@ function matchesRefTextFragment(statement, fragment) {
|
|
|
1178
1244
|
const local = localNameFromRefText(refText);
|
|
1179
1245
|
return !!local && local === fragment;
|
|
1180
1246
|
}
|
|
1181
|
-
function localNameFromRefText(refText) {
|
|
1182
|
-
const trimmed = refText.trim().replace(/^<|>$/g, '');
|
|
1183
|
-
if (!trimmed) {
|
|
1184
|
-
return undefined;
|
|
1185
|
-
}
|
|
1186
|
-
const colonIndex = trimmed.indexOf(':');
|
|
1187
|
-
if (colonIndex > 0 && colonIndex < trimmed.length - 1) {
|
|
1188
|
-
return trimmed.slice(colonIndex + 1);
|
|
1189
|
-
}
|
|
1190
|
-
const hashIndex = trimmed.lastIndexOf('#');
|
|
1191
|
-
if (hashIndex >= 0 && hashIndex < trimmed.length - 1) {
|
|
1192
|
-
return trimmed.slice(hashIndex + 1);
|
|
1193
|
-
}
|
|
1194
|
-
const slashIndex = trimmed.lastIndexOf('/');
|
|
1195
|
-
if (slashIndex >= 0 && slashIndex < trimmed.length - 1) {
|
|
1196
|
-
return trimmed.slice(slashIndex + 1);
|
|
1197
|
-
}
|
|
1198
|
-
return trimmed;
|
|
1199
|
-
}
|
|
1200
|
-
function isSameIriTarget(left, right) {
|
|
1201
|
-
const leftNorm = normalizeIri(left);
|
|
1202
|
-
const rightNorm = normalizeIri(right);
|
|
1203
|
-
if (leftNorm === rightNorm) {
|
|
1204
|
-
return true;
|
|
1205
|
-
}
|
|
1206
|
-
const leftParts = parseIriParts(leftNorm);
|
|
1207
|
-
const rightParts = parseIriParts(rightNorm);
|
|
1208
|
-
if (!leftParts || !rightParts) {
|
|
1209
|
-
return false;
|
|
1210
|
-
}
|
|
1211
|
-
return leftParts.namespace === rightParts.namespace
|
|
1212
|
-
&& leftParts.fragment === rightParts.fragment;
|
|
1213
|
-
}
|
|
1214
|
-
function pickImportPrefix(namespace, usedPrefixes) {
|
|
1215
|
-
const normalizedNamespace = normalizeNamespace(namespace);
|
|
1216
|
-
const pieces = normalizedNamespace.split('/').filter((piece) => piece.length > 0);
|
|
1217
|
-
const tail = (pieces[pieces.length - 1] ?? normalizedNamespace)
|
|
1218
|
-
.replace(/[^A-Za-z0-9_]/g, '')
|
|
1219
|
-
.toLowerCase();
|
|
1220
|
-
const base = /^[A-Za-z_]/.test(tail) ? tail : `ns${tail}`;
|
|
1221
|
-
const candidateBase = (base || 'ns').slice(0, 32);
|
|
1222
|
-
if (!usedPrefixes.has(candidateBase)) {
|
|
1223
|
-
return candidateBase;
|
|
1224
|
-
}
|
|
1225
|
-
let suffix = 1;
|
|
1226
|
-
while (usedPrefixes.has(`${candidateBase}${suffix}`)) {
|
|
1227
|
-
suffix += 1;
|
|
1228
|
-
}
|
|
1229
|
-
return `${candidateBase}${suffix}`;
|
|
1230
|
-
}
|
|
1231
1247
|
function asLiteral(ontology, value) {
|
|
1232
1248
|
if (isTypedQuotedLiteralTransport(value)) {
|
|
1233
1249
|
const literalValue = String(value.value ?? '');
|
|
@@ -1355,8 +1371,8 @@ function isIriLike(value) {
|
|
|
1355
1371
|
}
|
|
1356
1372
|
return trimmed.includes('://');
|
|
1357
1373
|
}
|
|
1358
|
-
function
|
|
1359
|
-
return
|
|
1374
|
+
function normalizeOntologyIriKey(value) {
|
|
1375
|
+
return normalizeNamespace(normalizeIri(value));
|
|
1360
1376
|
}
|
|
1361
1377
|
function normalizePropertyAssertionGroup(subject, target, duplicates) {
|
|
1362
1378
|
if (!Array.isArray(subject?.ownedPropertyValues) || !target || duplicates.length === 0) {
|
|
@@ -1466,16 +1482,23 @@ function isSourcePredicate(predicateIri) {
|
|
|
1466
1482
|
function isTargetPredicate(predicateIri) {
|
|
1467
1483
|
return normalizeIri(predicateIri) === OML_HAS_TARGET_IRI;
|
|
1468
1484
|
}
|
|
1469
|
-
function buildWorkspaceEdit(contexts, changedOntologyIris) {
|
|
1470
|
-
if (changedOntologyIris.size === 0) {
|
|
1485
|
+
function buildWorkspaceEdit(contexts, changedOntologyIris, createdOntologyByIri, deletedOntologyModelUriByIri) {
|
|
1486
|
+
if (changedOntologyIris.size === 0 && createdOntologyByIri.size === 0 && deletedOntologyModelUriByIri.size === 0) {
|
|
1471
1487
|
return undefined;
|
|
1472
1488
|
}
|
|
1473
1489
|
const changes = {};
|
|
1490
|
+
const deletedModelUris = new Set([...deletedOntologyModelUriByIri.values()]);
|
|
1474
1491
|
for (const ontologyIri of [...changedOntologyIris].sort()) {
|
|
1492
|
+
if (createdOntologyByIri.has(ontologyIri)) {
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1475
1495
|
const context = contexts.get(ontologyIri);
|
|
1476
1496
|
if (!context) {
|
|
1477
1497
|
continue;
|
|
1478
1498
|
}
|
|
1499
|
+
if (deletedModelUris.has(context.modelUri)) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1479
1502
|
const serialized = serializeOntology(context.ontology);
|
|
1480
1503
|
const textDocument = context.document?.textDocument;
|
|
1481
1504
|
if (!textDocument) {
|
|
@@ -1490,14 +1513,185 @@ function buildWorkspaceEdit(contexts, changedOntologyIris) {
|
|
|
1490
1513
|
newText: serialized
|
|
1491
1514
|
}];
|
|
1492
1515
|
}
|
|
1493
|
-
|
|
1516
|
+
for (const [createdIri, created] of [...createdOntologyByIri.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1517
|
+
const createdContext = contexts.get(createdIri);
|
|
1518
|
+
const content = createdContext ? serializeOntology(createdContext.ontology) : created.content;
|
|
1519
|
+
changes[created.modelUri] = [{
|
|
1520
|
+
range: {
|
|
1521
|
+
start: { line: 0, character: 0 },
|
|
1522
|
+
end: { line: 0, character: 0 }
|
|
1523
|
+
},
|
|
1524
|
+
newText: content
|
|
1525
|
+
}];
|
|
1526
|
+
}
|
|
1527
|
+
const documentChanges = [...deletedModelUris].sort().map((uri) => ({ kind: 'delete', uri }));
|
|
1528
|
+
if (Object.keys(changes).length === 0 && documentChanges.length === 0) {
|
|
1529
|
+
return undefined;
|
|
1530
|
+
}
|
|
1531
|
+
if (documentChanges.length === 0) {
|
|
1532
|
+
return { changes };
|
|
1533
|
+
}
|
|
1534
|
+
if (Object.keys(changes).length === 0) {
|
|
1535
|
+
return { documentChanges };
|
|
1536
|
+
}
|
|
1537
|
+
return { changes, documentChanges };
|
|
1538
|
+
}
|
|
1539
|
+
function detectOmlSrcDocument(shared) {
|
|
1540
|
+
const documentsService = shared.workspace.LangiumDocuments;
|
|
1541
|
+
const all = documentsService?.all ?? [];
|
|
1542
|
+
const iterable = Array.isArray(all)
|
|
1543
|
+
? all
|
|
1544
|
+
: (typeof all?.toArray === 'function' ? all.toArray() : Array.from(all));
|
|
1545
|
+
for (const document of iterable) {
|
|
1546
|
+
const uri = String(document?.uri ?? '');
|
|
1547
|
+
if (uri.includes('/oml/')) {
|
|
1548
|
+
return uri;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return undefined;
|
|
1552
|
+
}
|
|
1553
|
+
async function createOntologyOperation(shared, operation, workspaceUri) {
|
|
1554
|
+
const namespaceCore = normalizeNamespace(operation.ontologyNamespace);
|
|
1555
|
+
if (!namespaceCore) {
|
|
1556
|
+
throw new Error('createOntology requires a non-empty ontologyNamespace.');
|
|
1557
|
+
}
|
|
1558
|
+
const ontologyPrefix = operation.ontologyPrefix.trim();
|
|
1559
|
+
if (!ontologyPrefix) {
|
|
1560
|
+
throw new Error('createOntology requires a non-empty ontologyPrefix.');
|
|
1561
|
+
}
|
|
1562
|
+
const namespaceWithSeparator = withNamespaceSeparator(operation.ontologyNamespace);
|
|
1563
|
+
const index = getOntologyModelIndex(shared);
|
|
1564
|
+
const referencingUri = operation.targetFolder ? undefined : detectOmlSrcDocument(shared) ?? workspaceUri;
|
|
1565
|
+
const existingModelUri = index.resolveModelUri(namespaceCore, referencingUri);
|
|
1566
|
+
if (existingModelUri) {
|
|
1567
|
+
throw new Error(`Ontology '${namespaceWithSeparator}' already exists at '${existingModelUri}'.`);
|
|
1568
|
+
}
|
|
1569
|
+
const modelUri = deriveNewOntologyModelUri(namespaceCore, referencingUri, operation.targetFolder, workspaceUri);
|
|
1570
|
+
const ontologyIri = normalizeOntologyIriKey(namespaceWithSeparator);
|
|
1571
|
+
const ontology = createOntologyAst(operation.ontologyKind, namespaceWithSeparator, ontologyPrefix);
|
|
1572
|
+
const content = serializeOntology(ontology);
|
|
1573
|
+
return {
|
|
1574
|
+
ontologyIri,
|
|
1575
|
+
modelUri,
|
|
1576
|
+
content,
|
|
1577
|
+
ontology
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function deriveNewOntologyModelUri(namespaceCore, referencingUri, targetFolder, workspaceUri) {
|
|
1581
|
+
let namespaceUrl;
|
|
1582
|
+
try {
|
|
1583
|
+
namespaceUrl = new URL(namespaceCore.includes('://') ? namespaceCore : `http://${namespaceCore}`);
|
|
1584
|
+
}
|
|
1585
|
+
catch {
|
|
1586
|
+
throw new Error(`Unable to derive ontology file path from namespace '${namespaceCore}'.`);
|
|
1587
|
+
}
|
|
1588
|
+
const nsHost = namespaceUrl.host;
|
|
1589
|
+
const nsPath = (namespaceUrl.pathname ?? '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1590
|
+
const relative = [nsHost, ...nsPath.split('/').filter(Boolean)].join('/');
|
|
1591
|
+
if (!relative) {
|
|
1592
|
+
throw new Error(`Unable to derive ontology file path from namespace '${namespaceCore}'.`);
|
|
1593
|
+
}
|
|
1594
|
+
if (targetFolder) {
|
|
1595
|
+
const normalizedFolder = targetFolder.trim().replace(/\/+$/, '');
|
|
1596
|
+
if (!normalizedFolder) {
|
|
1597
|
+
throw new Error('createOntology targetFolder must not be empty.');
|
|
1598
|
+
}
|
|
1599
|
+
let folderPath = normalizedFolder;
|
|
1600
|
+
if (!normalizedFolder.startsWith('/') && workspaceUri) {
|
|
1601
|
+
const workspacePath = URI.parse(workspaceUri).path ?? '';
|
|
1602
|
+
folderPath = `${workspacePath.replace(/\/+$/, '')}/${normalizedFolder}`;
|
|
1603
|
+
}
|
|
1604
|
+
return URI.from({
|
|
1605
|
+
scheme: 'file',
|
|
1606
|
+
authority: '',
|
|
1607
|
+
path: `${folderPath}/${relative}.oml`,
|
|
1608
|
+
query: undefined,
|
|
1609
|
+
fragment: undefined
|
|
1610
|
+
}).toString();
|
|
1611
|
+
}
|
|
1612
|
+
if (!referencingUri) {
|
|
1613
|
+
throw new Error('createOntology requires either targetFolder or referencingUri to derive output path.');
|
|
1614
|
+
}
|
|
1615
|
+
let reference;
|
|
1616
|
+
try {
|
|
1617
|
+
reference = URI.parse(referencingUri);
|
|
1618
|
+
}
|
|
1619
|
+
catch {
|
|
1620
|
+
throw new Error(`Invalid referencingUri '${referencingUri}'.`);
|
|
1621
|
+
}
|
|
1622
|
+
const refPath = reference.path ?? '';
|
|
1623
|
+
const marker = '/oml/';
|
|
1624
|
+
const markerIndex = refPath.lastIndexOf(marker);
|
|
1625
|
+
if (markerIndex < 0) {
|
|
1626
|
+
throw new Error(`Unable to derive ontology path from referencingUri '${referencingUri}': missing '/oml/' segment.`);
|
|
1627
|
+
}
|
|
1628
|
+
const basePath = refPath.slice(0, markerIndex + marker.length);
|
|
1629
|
+
const targetPath = `${basePath}${relative}.oml`;
|
|
1630
|
+
return URI.from({
|
|
1631
|
+
scheme: reference.scheme,
|
|
1632
|
+
authority: reference.authority,
|
|
1633
|
+
path: targetPath,
|
|
1634
|
+
query: reference.query,
|
|
1635
|
+
fragment: undefined
|
|
1636
|
+
}).toString();
|
|
1637
|
+
}
|
|
1638
|
+
function createOntologyAst(ontologyKind, namespace, prefix) {
|
|
1639
|
+
switch (ontologyKind) {
|
|
1640
|
+
case 'vocabulary':
|
|
1641
|
+
return {
|
|
1642
|
+
$type: 'Vocabulary',
|
|
1643
|
+
namespace,
|
|
1644
|
+
prefix,
|
|
1645
|
+
ownedAnnotations: [],
|
|
1646
|
+
ownedImports: [],
|
|
1647
|
+
ownedStatements: []
|
|
1648
|
+
};
|
|
1649
|
+
case 'description':
|
|
1650
|
+
return {
|
|
1651
|
+
$type: 'Description',
|
|
1652
|
+
namespace,
|
|
1653
|
+
prefix,
|
|
1654
|
+
ownedAnnotations: [],
|
|
1655
|
+
ownedImports: [],
|
|
1656
|
+
ownedStatements: []
|
|
1657
|
+
};
|
|
1658
|
+
case 'vocabulary bundle':
|
|
1659
|
+
return {
|
|
1660
|
+
$type: 'VocabularyBundle',
|
|
1661
|
+
namespace,
|
|
1662
|
+
prefix,
|
|
1663
|
+
ownedAnnotations: [],
|
|
1664
|
+
ownedImports: []
|
|
1665
|
+
};
|
|
1666
|
+
case 'description bundle':
|
|
1667
|
+
return {
|
|
1668
|
+
$type: 'DescriptionBundle',
|
|
1669
|
+
namespace,
|
|
1670
|
+
prefix,
|
|
1671
|
+
ownedAnnotations: [],
|
|
1672
|
+
ownedImports: []
|
|
1673
|
+
};
|
|
1674
|
+
default:
|
|
1675
|
+
throw new Error(`Unsupported ontology kind '${ontologyKind}'.`);
|
|
1676
|
+
}
|
|
1494
1677
|
}
|
|
1495
1678
|
async function restoreContextsFromDocuments(shared, contexts) {
|
|
1496
1679
|
const uris = [...new Set([...contexts.values()].map((context) => URI.parse(context.modelUri)))];
|
|
1680
|
+
const langiumDocuments = shared.workspace.LangiumDocuments;
|
|
1681
|
+
// Langium skips re-parsing when the on-disk text matches the CST text stored in the
|
|
1682
|
+
// current parseResult. Preview modifies the AST in-place without changing the file, so
|
|
1683
|
+
// the texts are still equal and the optimisation would keep the modified AST. Clearing
|
|
1684
|
+
// $cstNode forces oldText to be undefined, guaranteeing a fresh parse from disk.
|
|
1685
|
+
for (const context of contexts.values()) {
|
|
1686
|
+
const doc = langiumDocuments.getDocument(URI.parse(context.modelUri));
|
|
1687
|
+
const root = doc?.parseResult?.value;
|
|
1688
|
+
if (root) {
|
|
1689
|
+
root.$cstNode = undefined;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1497
1692
|
if (uris.length > 0) {
|
|
1498
1693
|
await shared.workspace.DocumentBuilder.update(uris, []);
|
|
1499
1694
|
}
|
|
1500
|
-
const langiumDocuments = shared.workspace.LangiumDocuments;
|
|
1501
1695
|
for (const context of contexts.values()) {
|
|
1502
1696
|
const refreshed = langiumDocuments.getDocument(URI.parse(context.modelUri))
|
|
1503
1697
|
?? await langiumDocuments.getOrCreateDocument(URI.parse(context.modelUri));
|