@oml/language 0.14.17 → 0.16.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.
@@ -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 OmlEditRequestType = new RequestType('oml/update');
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
- const context = await resolveOntologyContext(shared, operation.ontologyIri, contexts, referencingUri);
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] OmlEditRequest failed at operation ${i} (${operation.kind}): ${details}`);
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
- export function registerOmlEditRequests(connection, shared) {
48
- connection.onRequest(OmlEditRequestType, async (params) => {
49
- return await applyOmlUpdate(shared, params, (message) => connection.console?.error(message));
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 = normalizeIri(ontologyIri);
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 prefix = resolveImportPrefix(imp);
1042
- if (prefix) {
1043
- usedPrefixes.add(prefix);
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 resolveImportKind(shared, importing, importedNamespace) {
1058
- const imported = await findOntologyByNamespace(shared, importedNamespace);
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 normalizeIri(value) {
1359
- return String(value ?? '').trim().replace(/^<|>$/g, '');
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
- return Object.keys(changes).length > 0 ? { changes } : undefined;
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));