@oml/language 0.9.0 → 0.11.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,16 +1,32 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
3
  import { URI, stream, type AstNodeDescription, type AstNodeLocator, type IndexManager, type LangiumDocuments, type Stream } from 'langium';
4
- import { isConceptInstance, isMember, isRelationInstance } from './generated/ast.js';
4
+ import {
5
+ isAspect,
6
+ isBooleanLiteral,
7
+ isConcept,
8
+ isConceptInstance,
9
+ isDecimalLiteral,
10
+ isDoubleLiteral,
11
+ isIntegerLiteral,
12
+ isMember,
13
+ isQuotedLiteral,
14
+ isRelationEntity,
15
+ isRelationInstance,
16
+ isScalar,
17
+ } from './generated/ast.js';
18
+ import { getOntologyModelIndex, type OmlIndex } from './oml-index.js';
5
19
  import { getIriForNode, getWorkspaceSnapshot } from './oml-utils.js';
6
20
 
7
- export const OmlCandidatesRequest = 'oml/candidates';
21
+ export const OmlCompletionRequest = 'oml/completion';
8
22
 
9
- export interface OmlCandidatesParams {
10
- referenceType: string;
23
+ export interface OmlCompletionParams {
24
+ instanceIri?: string;
25
+ propertyIri?: string;
26
+ typeIri?: string;
11
27
  }
12
28
 
13
- export interface OmlCandidatesResult {
29
+ export interface OmlCompletionResult {
14
30
  candidates: string[];
15
31
  }
16
32
 
@@ -18,14 +34,14 @@ const MaxReferenceTypeCaches = 64;
18
34
  const workspacePreloadState = new WeakMap<object, { completed: boolean; promise?: Promise<void> }>();
19
35
 
20
36
  type ConnectionLike = {
21
- onRequest: (type: string, handler: (params: OmlCandidatesParams) => OmlCandidatesResult | Promise<OmlCandidatesResult>) => void;
37
+ onRequest: (type: string, handler: (params: OmlCompletionParams) => OmlCompletionResult | Promise<OmlCompletionResult>) => void;
22
38
  };
23
39
 
24
40
  export class OmlCandidates {
25
41
  private readonly referenceCandidatesCache = new Map<string, { snapshot: string; candidates: AstNodeDescription[] }>();
26
- private readonly workspaceCandidateIrisCache = new Map<string, { snapshot: string; candidates: string[] }>();
27
42
 
28
43
  constructor(
44
+ private readonly ontologyIndex: OmlIndex,
29
45
  private readonly indexManager: IndexManager,
30
46
  private readonly langiumDocuments: LangiumDocuments,
31
47
  private readonly astNodeLocator: AstNodeLocator,
@@ -41,12 +57,7 @@ export class OmlCandidates {
41
57
  if (!normalizedReferenceType) {
42
58
  return [];
43
59
  }
44
- const snapshot = getWorkspaceSnapshot(this.langiumDocuments);
45
- const cached = this.workspaceCandidateIrisCache.get(normalizedReferenceType);
46
- if (cached && cached.snapshot === snapshot) {
47
- return cached.candidates;
48
- }
49
- const workspace = this.getWorkspaceCandidates(normalizedReferenceType);
60
+ const workspace = this.indexManager.allElements(normalizedReferenceType).toArray();
50
61
  const iris: string[] = [];
51
62
  for (const description of workspace) {
52
63
  const node = this.resolveDescriptionNode(description);
@@ -55,10 +66,39 @@ export class OmlCandidates {
55
66
  iris.push(iri);
56
67
  }
57
68
  }
58
- const candidates = [...new Set(iris)];
59
- this.workspaceCandidateIrisCache.set(normalizedReferenceType, { snapshot, candidates });
60
- this.pruneCache(this.workspaceCandidateIrisCache, MaxReferenceTypeCaches);
61
- return candidates;
69
+ return [...new Set(iris)];
70
+ }
71
+
72
+ getCompletionCandidateIris(params: OmlCompletionParams): string[] {
73
+ const typeIri = (params.typeIri ?? '').trim();
74
+ if (!typeIri) {
75
+ return this.ontologyIndex.getAllDescriptionInstanceIris();
76
+ }
77
+ const typeNode = this.resolveTypeNodeByIri(typeIri);
78
+ if (!typeNode) {
79
+ return [];
80
+ }
81
+ if (isScalar(typeNode)) {
82
+ const literals = typeNode.ownedEnumeration?.literals ?? [];
83
+ const values = literals
84
+ .map((literal) => this.serializeLiteralCandidate(literal))
85
+ .filter((value): value is string => typeof value === 'string' && value.length > 0);
86
+ return [...new Set(values)];
87
+ }
88
+ if (isConcept(typeNode)) {
89
+ const enumeration = typeNode.ownedEnumeration;
90
+ if (enumeration?.instances?.length) {
91
+ const values = enumeration.instances
92
+ .map((reference) => this.resolveNamedInstanceReferenceIri(reference))
93
+ .filter((value): value is string => typeof value === 'string' && value.length > 0);
94
+ return [...new Set(values)];
95
+ }
96
+ }
97
+ if (isAspect(typeNode) || isConcept(typeNode) || isRelationEntity(typeNode)) {
98
+ const iris = this.ontologyIndex.getTypeMemberIris(typeIri);
99
+ return iris;
100
+ }
101
+ return [];
62
102
  }
63
103
 
64
104
  private getWorkspaceCandidates(referenceType: string): AstNodeDescription[] {
@@ -89,6 +129,89 @@ export class OmlCandidates {
89
129
  return undefined;
90
130
  }
91
131
 
132
+ private normalizeIri(iri: string): string {
133
+ return iri.replace(/^<|>$/g, '').trim();
134
+ }
135
+
136
+ private resolveTypeNodeByIri(typeIri: string): unknown {
137
+ const normalized = this.normalizeIri(typeIri);
138
+ if (!normalized) {
139
+ return undefined;
140
+ }
141
+ for (const referenceType of ['Scalar', 'Concept', 'Aspect', 'RelationEntity']) {
142
+ for (const description of this.indexManager.allElements(referenceType).toArray()) {
143
+ const node = this.resolveDescriptionNode(description);
144
+ const iri = this.normalizeIri(getIriForNode(node) ?? '');
145
+ if (iri === normalized) {
146
+ return node;
147
+ }
148
+ }
149
+ }
150
+ return undefined;
151
+ }
152
+
153
+ private resolveNamedInstanceReferenceIri(reference: any): string | undefined {
154
+ const resolved = reference?.ref;
155
+ const resolvedIri = resolved ? this.normalizeIri(getIriForNode(resolved) ?? '') : '';
156
+ if (resolvedIri) {
157
+ return resolvedIri;
158
+ }
159
+ const refText = reference?.$refText;
160
+ if (typeof refText === 'string' && refText.trim().length > 0) {
161
+ return this.normalizeIri(refText);
162
+ }
163
+ return undefined;
164
+ }
165
+
166
+ private serializeLiteralCandidate(literal: any): string {
167
+ if (isQuotedLiteral(literal)) {
168
+ return this.serializeQuotedLiteralCandidate(literal);
169
+ }
170
+ if (isBooleanLiteral(literal)) {
171
+ return literal.value ? 'true' : 'false';
172
+ }
173
+ if (isIntegerLiteral(literal) || isDecimalLiteral(literal) || isDoubleLiteral(literal)) {
174
+ return `${literal.value}`;
175
+ }
176
+ return '';
177
+ }
178
+
179
+ private serializeQuotedLiteralCandidate(literal: any): string {
180
+ const base = this.serializeStringLiteral(literal.value ?? '');
181
+ if (literal.type?.ref) {
182
+ const datatypeIri = this.normalizeIri(getIriForNode(literal.type.ref) ?? literal.type.$refText ?? '');
183
+ if (datatypeIri) {
184
+ return `${base}^^${datatypeIri}`;
185
+ }
186
+ }
187
+ if (typeof literal.langTag === 'string' && literal.langTag.trim().length > 0) {
188
+ return `${base}$${literal.langTag.trim()}`;
189
+ }
190
+ return base;
191
+ }
192
+
193
+ private serializeStringLiteral(value: string): string {
194
+ const raw = value ?? '';
195
+ if (raw.includes('\n') || raw.includes('\r')) {
196
+ if (!raw.includes('"""')) {
197
+ return `"""${raw}"""`;
198
+ }
199
+ if (!raw.includes("'''")) {
200
+ return `'''${raw}'''`;
201
+ }
202
+ }
203
+ if (!raw.includes('"')) {
204
+ return `"${raw}"`;
205
+ }
206
+ if (!raw.includes("'")) {
207
+ return `'${raw}'`;
208
+ }
209
+ if (!raw.includes('"""')) {
210
+ return `"""${raw}"""`;
211
+ }
212
+ return `'''${raw}'''`;
213
+ }
214
+
92
215
  private resolveDescriptionNode(description: AstNodeDescription): unknown {
93
216
  if (description.node) {
94
217
  return description.node;
@@ -118,18 +241,16 @@ export class OmlCandidates {
118
241
  }
119
242
 
120
243
  export function registerOmlCandidatesRequests(connection: ConnectionLike, shared: any): void {
244
+ const ontologyIndex = getOntologyModelIndex(shared);
121
245
  const candidates = new OmlCandidates(
246
+ ontologyIndex,
122
247
  shared.workspace.IndexManager,
123
248
  shared.workspace.LangiumDocuments,
124
249
  shared.workspace.AstNodeLocator
125
250
  );
126
- connection.onRequest(OmlCandidatesRequest, async (params: OmlCandidatesParams): Promise<OmlCandidatesResult> => {
251
+ connection.onRequest(OmlCompletionRequest, async (params: OmlCompletionParams): Promise<OmlCompletionResult> => {
127
252
  await ensureWorkspaceIndexed(shared);
128
- const referenceType = (params?.referenceType ?? '').trim();
129
- if (!referenceType) {
130
- return { candidates: [] };
131
- }
132
- return { candidates: candidates.getWorkspaceCandidateIris(referenceType) };
253
+ return { candidates: candidates.getCompletionCandidateIris(params) };
133
254
  });
134
255
  }
135
256
 
@@ -4,6 +4,7 @@ import type { AstNodeDescription, ReferenceInfo, Stream, LangiumDocuments, AstNo
4
4
  import type { CompletionItem, TextEdit } from 'vscode-languageserver-types';
5
5
  import { DefaultCompletionProvider, type CompletionContext, type CompletionValueItem, type LangiumServices } from 'langium/lsp';
6
6
  import { Element, isDescription, isDescriptionBox, isMember, isOntology, isVocabulary, isVocabularyBundle, Member, Ontology } from './generated/ast.js';
7
+ import { getOntologyModelIndex } from './oml-index.js';
7
8
  import { getLangiumDocumentVersion } from './oml-utils.js';
8
9
  import { OmlCandidates } from './oml-candidates.js';
9
10
 
@@ -32,7 +33,7 @@ export class OmlCompletionProvider extends DefaultCompletionProvider {
32
33
  this.indexManager = services.shared.workspace.IndexManager;
33
34
  this.langiumDocuments = services.shared.workspace.LangiumDocuments;
34
35
  this.astNodeLocator = services.workspace.AstNodeLocator;
35
- this.candidates = new OmlCandidates(this.indexManager, this.langiumDocuments, this.astNodeLocator);
36
+ this.candidates = new OmlCandidates(getOntologyModelIndex(services.shared), this.indexManager, this.langiumDocuments, this.astNodeLocator);
36
37
  }
37
38
 
38
39
  /**
@@ -1,24 +1,64 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
+ import { URI } from 'langium';
3
4
  import { DefaultDocumentUpdateHandler } from 'langium/lsp';
4
5
  import type { LangiumSharedServices } from 'langium/lsp';
5
- import type { TextDocumentChangeEvent } from 'vscode-languageserver';
6
- import type { TextDocument } from 'vscode-languageserver-textdocument';
6
+ import { FileChangeType, type DidChangeWatchedFilesParams } from 'vscode-languageserver';
7
+ import { getOntologyModelIndex } from './oml-index.js';
7
8
 
8
9
  export class OmlDocumentUpdateHandler extends DefaultDocumentUpdateHandler {
9
- private readonly lastProcessedVersionByUri = new Map<string, number>();
10
+ private readonly services: LangiumSharedServices;
10
11
 
11
12
  constructor(services: LangiumSharedServices) {
12
13
  super(services);
14
+ this.services = services;
13
15
  }
14
16
 
15
- override didChangeContent(change: TextDocumentChangeEvent<TextDocument>): void {
16
- const nextVersion = change.document.version;
17
- const previousVersion = this.lastProcessedVersionByUri.get(change.document.uri);
18
- if (previousVersion === nextVersion) {
17
+ override didChangeWatchedFiles(params: DidChangeWatchedFilesParams): void {
18
+ this.cleanDeletedOntologyDocuments(params);
19
+ super.didChangeWatchedFiles(params);
20
+ }
21
+
22
+ private cleanDeletedOntologyDocuments(params: DidChangeWatchedFilesParams): void {
23
+ const deletedUris = new Set(
24
+ params.changes
25
+ .filter((change) => change.type === FileChangeType.Deleted)
26
+ .map((change) => change.uri)
27
+ );
28
+ if (deletedUris.size === 0) {
19
29
  return;
20
30
  }
21
- this.lastProcessedVersionByUri.set(change.document.uri, nextVersion);
22
- super.didChangeContent(change);
31
+
32
+ const ontologyIndex = getOntologyModelIndex(this.services);
33
+ const documents = this.services.workspace.LangiumDocuments.all.toArray();
34
+ for (const document of documents) {
35
+ if (!matchesDeletedUri(document.uri.toString(), deletedUris)) {
36
+ continue;
37
+ }
38
+ ontologyIndex.removeDocument(document);
39
+ const language = this.services.ServiceRegistry.getServices(document.uri) as any;
40
+ language?.reasoning?.ReasoningService?.onDocumentDeleted?.(document);
41
+ }
23
42
  }
24
43
  }
44
+
45
+ function matchesDeletedUri(candidateUri: string, deletedUris: ReadonlySet<string>): boolean {
46
+ for (const deletedUri of deletedUris) {
47
+ const deleted = URI.parse(deletedUri);
48
+ const candidate = URI.parse(candidateUri);
49
+ if (candidate.scheme !== deleted.scheme || (candidate.authority ?? '') !== (deleted.authority ?? '')) {
50
+ continue;
51
+ }
52
+ const deletedPath = normalizeUriPath(deleted.path);
53
+ const candidatePath = normalizeUriPath(candidate.path);
54
+ if (candidatePath === deletedPath || candidatePath.startsWith(`${deletedPath}/`)) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function normalizeUriPath(path: string): string {
62
+ const normalized = path.replace(/\/+$/, '');
63
+ return normalized || '/';
64
+ }