@oml/language 0.7.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/README.md +44 -0
- package/out/oml/generated/ast.d.ts +2109 -0
- package/out/oml/generated/ast.js +1807 -0
- package/out/oml/generated/ast.js.map +1 -0
- package/out/oml/generated/grammar.d.ts +6 -0
- package/out/oml/generated/grammar.js +6885 -0
- package/out/oml/generated/grammar.js.map +1 -0
- package/out/oml/generated/module.d.ts +13 -0
- package/out/oml/generated/module.js +21 -0
- package/out/oml/generated/module.js.map +1 -0
- package/out/oml/index.d.ts +17 -0
- package/out/oml/index.js +19 -0
- package/out/oml/index.js.map +1 -0
- package/out/oml/oml-candidates.d.ts +27 -0
- package/out/oml/oml-candidates.js +146 -0
- package/out/oml/oml-candidates.js.map +1 -0
- package/out/oml/oml-code-actions.d.ts +6 -0
- package/out/oml/oml-code-actions.js +79 -0
- package/out/oml/oml-code-actions.js.map +1 -0
- package/out/oml/oml-completion.d.ts +50 -0
- package/out/oml/oml-completion.js +188 -0
- package/out/oml/oml-completion.js.map +1 -0
- package/out/oml/oml-converter.d.ts +10 -0
- package/out/oml/oml-converter.js +62 -0
- package/out/oml/oml-converter.js.map +1 -0
- package/out/oml/oml-document-validator.d.ts +10 -0
- package/out/oml/oml-document-validator.js +31 -0
- package/out/oml/oml-document-validator.js.map +1 -0
- package/out/oml/oml-document.d.ts +9 -0
- package/out/oml/oml-document.js +18 -0
- package/out/oml/oml-document.js.map +1 -0
- package/out/oml/oml-edit.d.ts +72 -0
- package/out/oml/oml-edit.js +1155 -0
- package/out/oml/oml-edit.js.map +1 -0
- package/out/oml/oml-formatter.d.ts +22 -0
- package/out/oml/oml-formatter.js +357 -0
- package/out/oml/oml-formatter.js.map +1 -0
- package/out/oml/oml-hover.d.ts +13 -0
- package/out/oml/oml-hover.js +71 -0
- package/out/oml/oml-hover.js.map +1 -0
- package/out/oml/oml-index-manager.d.ts +10 -0
- package/out/oml/oml-index-manager.js +48 -0
- package/out/oml/oml-index-manager.js.map +1 -0
- package/out/oml/oml-index.d.ts +20 -0
- package/out/oml/oml-index.js +133 -0
- package/out/oml/oml-index.js.map +1 -0
- package/out/oml/oml-module.d.ts +42 -0
- package/out/oml/oml-module.js +76 -0
- package/out/oml/oml-module.js.map +1 -0
- package/out/oml/oml-rename.d.ts +14 -0
- package/out/oml/oml-rename.js +114 -0
- package/out/oml/oml-rename.js.map +1 -0
- package/out/oml/oml-scope.d.ts +30 -0
- package/out/oml/oml-scope.js +225 -0
- package/out/oml/oml-scope.js.map +1 -0
- package/out/oml/oml-serializer.d.ts +2 -0
- package/out/oml/oml-serializer.js +883 -0
- package/out/oml/oml-serializer.js.map +1 -0
- package/out/oml/oml-utils.d.ts +53 -0
- package/out/oml/oml-utils.js +241 -0
- package/out/oml/oml-utils.js.map +1 -0
- package/out/oml/oml-validator.d.ts +49 -0
- package/out/oml/oml-validator.js +668 -0
- package/out/oml/oml-validator.js.map +1 -0
- package/out/oml/oml-workspace.d.ts +23 -0
- package/out/oml/oml-workspace.js +68 -0
- package/out/oml/oml-workspace.js.map +1 -0
- package/package.json +50 -0
- package/src/oml/generated/ast.ts +2641 -0
- package/src/oml/generated/grammar.ts +6887 -0
- package/src/oml/generated/module.ts +25 -0
- package/src/oml/index.ts +19 -0
- package/src/oml/oml-candidates.ts +176 -0
- package/src/oml/oml-code-actions.ts +120 -0
- package/src/oml/oml-completion.ts +222 -0
- package/src/oml/oml-converter.ts +66 -0
- package/src/oml/oml-document-validator.ts +39 -0
- package/src/oml/oml-document.ts +24 -0
- package/src/oml/oml-edit.ts +1292 -0
- package/src/oml/oml-formatter.ts +390 -0
- package/src/oml/oml-hover.ts +93 -0
- package/src/oml/oml-index-manager.ts +56 -0
- package/src/oml/oml-index.ts +145 -0
- package/src/oml/oml-module.ts +105 -0
- package/src/oml/oml-rename.ts +140 -0
- package/src/oml/oml-scope.ts +279 -0
- package/src/oml/oml-serializer.ts +1080 -0
- package/src/oml/oml-utils.ts +294 -0
- package/src/oml/oml-validator.ts +725 -0
- package/src/oml/oml-workspace.ts +81 -0
- package/src/oml/oml.langium +594 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
* This file was generated by langium-cli 4.1.0.
|
|
3
|
+
* DO NOT EDIT MANUALLY!
|
|
4
|
+
******************************************************************************/
|
|
5
|
+
|
|
6
|
+
import type { LangiumSharedCoreServices, LangiumCoreServices, LangiumGeneratedCoreServices, LangiumGeneratedSharedCoreServices, LanguageMetaData, Module } from 'langium';
|
|
7
|
+
import { OmlAstReflection } from './ast.js';
|
|
8
|
+
import { OmlGrammar } from './grammar.js';
|
|
9
|
+
|
|
10
|
+
export const OmlLanguageMetaData = {
|
|
11
|
+
languageId: 'oml',
|
|
12
|
+
fileExtensions: ['.oml'],
|
|
13
|
+
caseInsensitive: false,
|
|
14
|
+
mode: 'development'
|
|
15
|
+
} as const satisfies LanguageMetaData;
|
|
16
|
+
|
|
17
|
+
export const OmlGeneratedSharedModule: Module<LangiumSharedCoreServices, LangiumGeneratedSharedCoreServices> = {
|
|
18
|
+
AstReflection: () => new OmlAstReflection()
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const OmlGeneratedModule: Module<LangiumCoreServices, LangiumGeneratedCoreServices> = {
|
|
22
|
+
Grammar: () => OmlGrammar(),
|
|
23
|
+
LanguageMetaData: () => OmlLanguageMetaData,
|
|
24
|
+
parser: {}
|
|
25
|
+
};
|
package/src/oml/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
export * from './oml-module.js';
|
|
4
|
+
export * from './oml-candidates.js';
|
|
5
|
+
export * from './oml-completion.js';
|
|
6
|
+
export * from './oml-converter.js';
|
|
7
|
+
export * from './oml-edit.js';
|
|
8
|
+
export * from './oml-formatter.js';
|
|
9
|
+
export * from './oml-hover.js';
|
|
10
|
+
export * from './oml-index.js';
|
|
11
|
+
export * from './oml-rename.js';
|
|
12
|
+
export * from './oml-serializer.js';
|
|
13
|
+
export * from './oml-scope.js';
|
|
14
|
+
export * from './oml-validator.js';
|
|
15
|
+
export * from './oml-utils.js';
|
|
16
|
+
export * from './oml-workspace.js';
|
|
17
|
+
export * from './generated/ast.js';
|
|
18
|
+
export * from './generated/grammar.js';
|
|
19
|
+
export * from './generated/module.js';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
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';
|
|
5
|
+
import { getIriForNode, getWorkspaceSnapshot } from './oml-utils.js';
|
|
6
|
+
|
|
7
|
+
export const OmlCandidatesRequest = 'oml/candidates';
|
|
8
|
+
|
|
9
|
+
export interface OmlCandidatesParams {
|
|
10
|
+
referenceType: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OmlCandidatesResult {
|
|
14
|
+
candidates: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MaxReferenceTypeCaches = 64;
|
|
18
|
+
const workspacePreloadState = new WeakMap<object, { completed: boolean; promise?: Promise<void> }>();
|
|
19
|
+
|
|
20
|
+
type ConnectionLike = {
|
|
21
|
+
onRequest: (type: string, handler: (params: OmlCandidatesParams) => OmlCandidatesResult | Promise<OmlCandidatesResult>) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class OmlCandidates {
|
|
25
|
+
private readonly referenceCandidatesCache = new Map<string, { snapshot: string; candidates: AstNodeDescription[] }>();
|
|
26
|
+
private readonly workspaceCandidateIrisCache = new Map<string, { snapshot: string; candidates: string[] }>();
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly indexManager: IndexManager,
|
|
30
|
+
private readonly langiumDocuments: LangiumDocuments,
|
|
31
|
+
private readonly astNodeLocator: AstNodeLocator,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
getReferenceCandidates(referenceType: string, linkable: Stream<AstNodeDescription>): Stream<AstNodeDescription> {
|
|
35
|
+
const workspace = stream(this.getWorkspaceCandidates(referenceType));
|
|
36
|
+
return linkable.concat(workspace).distinct((desc) => `${desc.documentUri.toString()}|${desc.path}|${desc.name}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getWorkspaceCandidateIris(referenceType: string): string[] {
|
|
40
|
+
const normalizedReferenceType = referenceType.trim();
|
|
41
|
+
if (!normalizedReferenceType) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
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);
|
|
50
|
+
const iris: string[] = [];
|
|
51
|
+
for (const description of workspace) {
|
|
52
|
+
const node = this.resolveDescriptionNode(description);
|
|
53
|
+
const iri = this.resolveNodeIri(node);
|
|
54
|
+
if (iri) {
|
|
55
|
+
iris.push(iri);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const candidates = [...new Set(iris)];
|
|
59
|
+
this.workspaceCandidateIrisCache.set(normalizedReferenceType, { snapshot, candidates });
|
|
60
|
+
this.pruneCache(this.workspaceCandidateIrisCache, MaxReferenceTypeCaches);
|
|
61
|
+
return candidates;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private getWorkspaceCandidates(referenceType: string): AstNodeDescription[] {
|
|
65
|
+
const snapshot = getWorkspaceSnapshot(this.langiumDocuments);
|
|
66
|
+
const cached = this.referenceCandidatesCache.get(referenceType);
|
|
67
|
+
if (cached && cached.snapshot === snapshot) {
|
|
68
|
+
return cached.candidates;
|
|
69
|
+
}
|
|
70
|
+
const candidates = this.indexManager.allElements(referenceType).toArray();
|
|
71
|
+
this.referenceCandidatesCache.set(referenceType, { snapshot, candidates });
|
|
72
|
+
this.pruneCache(this.referenceCandidatesCache, MaxReferenceTypeCaches);
|
|
73
|
+
return candidates;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private resolveNodeIri(node: unknown): string | undefined {
|
|
77
|
+
if (isMember(node)) {
|
|
78
|
+
const iri = getIriForNode(node);
|
|
79
|
+
if (iri) {
|
|
80
|
+
return iri;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (isConceptInstance(node) || isRelationInstance(node)) {
|
|
84
|
+
const refText = node.ref?.$refText;
|
|
85
|
+
if (typeof refText === 'string' && refText.trim().length > 0) {
|
|
86
|
+
return refText.replace(/^<|>$/g, '').trim();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private resolveDescriptionNode(description: AstNodeDescription): unknown {
|
|
93
|
+
if (description.node) {
|
|
94
|
+
return description.node;
|
|
95
|
+
}
|
|
96
|
+
const document = this.langiumDocuments.getDocument(description.documentUri);
|
|
97
|
+
const root = document?.parseResult?.value;
|
|
98
|
+
if (!root) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return this.astNodeLocator.getAstNode(root, description.path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private pruneCache<T>(cache: Map<string, T>, maxSize: number): void {
|
|
105
|
+
if (cache.size <= maxSize) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const overflow = cache.size - maxSize;
|
|
109
|
+
const keys = cache.keys();
|
|
110
|
+
for (let i = 0; i < overflow; i += 1) {
|
|
111
|
+
const next = keys.next();
|
|
112
|
+
if (next.done) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
cache.delete(next.value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function registerOmlCandidatesRequests(connection: ConnectionLike, shared: any): void {
|
|
121
|
+
const candidates = new OmlCandidates(
|
|
122
|
+
shared.workspace.IndexManager,
|
|
123
|
+
shared.workspace.LangiumDocuments,
|
|
124
|
+
shared.workspace.AstNodeLocator
|
|
125
|
+
);
|
|
126
|
+
connection.onRequest(OmlCandidatesRequest, async (params: OmlCandidatesParams): Promise<OmlCandidatesResult> => {
|
|
127
|
+
await ensureWorkspaceIndexed(shared);
|
|
128
|
+
const referenceType = (params?.referenceType ?? '').trim();
|
|
129
|
+
if (!referenceType) {
|
|
130
|
+
return { candidates: [] };
|
|
131
|
+
}
|
|
132
|
+
return { candidates: candidates.getWorkspaceCandidateIris(referenceType) };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function ensureWorkspaceIndexed(shared: any): Promise<void> {
|
|
137
|
+
const key = shared as object;
|
|
138
|
+
const existing = workspacePreloadState.get(key);
|
|
139
|
+
if (existing?.completed) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (existing?.promise) {
|
|
143
|
+
await existing.promise;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const state = existing ?? { completed: false };
|
|
147
|
+
const promise = (async () => {
|
|
148
|
+
const workspace = shared.workspace.WorkspaceManager;
|
|
149
|
+
const documents = shared.workspace.LangiumDocuments;
|
|
150
|
+
const builder = shared.workspace.DocumentBuilder;
|
|
151
|
+
await workspace.ready;
|
|
152
|
+
const folderUris = workspace.workspaceFolders ?? [];
|
|
153
|
+
const omlUris = new Set<string>();
|
|
154
|
+
for (const folder of folderUris) {
|
|
155
|
+
const entries = await workspace.searchFolder(URI.parse(folder.uri));
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const entryUri = entry.toString();
|
|
158
|
+
if (entryUri.toLowerCase().endsWith('.oml')) {
|
|
159
|
+
omlUris.add(entryUri);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (omlUris.size > 0) {
|
|
164
|
+
const docs = await Promise.all(
|
|
165
|
+
[...omlUris].map((uri) => documents.getOrCreateDocument(URI.parse(uri)))
|
|
166
|
+
);
|
|
167
|
+
await builder.build(docs, { validation: { categories: ['built-in', 'fast'] } });
|
|
168
|
+
}
|
|
169
|
+
state.completed = true;
|
|
170
|
+
})().finally(() => {
|
|
171
|
+
state.promise = undefined;
|
|
172
|
+
});
|
|
173
|
+
state.promise = promise;
|
|
174
|
+
workspacePreloadState.set(key, state);
|
|
175
|
+
await promise;
|
|
176
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import type { LangiumDocument, MaybePromise } from 'langium';
|
|
4
|
+
import type { CodeActionProvider } from 'langium/lsp';
|
|
5
|
+
import { CodeActionKind } from 'vscode-languageserver';
|
|
6
|
+
import type { CodeAction, CodeActionParams, Command, Range } from 'vscode-languageserver';
|
|
7
|
+
import { OmlEmptyDocumentDiagnosticCode } from './oml-document-validator.js';
|
|
8
|
+
|
|
9
|
+
export class OmlCodeActionProvider implements CodeActionProvider {
|
|
10
|
+
getCodeActions(document: LangiumDocument, params: CodeActionParams): MaybePromise<Array<Command | CodeAction> | undefined> {
|
|
11
|
+
const hasEmptyDocumentDiagnostic = params.context.diagnostics.some(diagnostic =>
|
|
12
|
+
diagnostic.data?.code === OmlEmptyDocumentDiagnosticCode || diagnostic.code === OmlEmptyDocumentDiagnosticCode
|
|
13
|
+
);
|
|
14
|
+
if (!hasEmptyDocumentDiagnostic) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const initializer = deriveOntologyInitializer(document.textDocument.uri);
|
|
19
|
+
if (!initializer) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const text = document.textDocument.getText();
|
|
24
|
+
const replaceRange: Range = {
|
|
25
|
+
start: document.textDocument.positionAt(0),
|
|
26
|
+
end: document.textDocument.positionAt(text.length)
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
createInitializeCodeAction(
|
|
31
|
+
document.textDocument.uri,
|
|
32
|
+
replaceRange,
|
|
33
|
+
`Initialize as description`,
|
|
34
|
+
renderOntologyStub('description', initializer.iri, initializer.prefix)
|
|
35
|
+
),
|
|
36
|
+
createInitializeCodeAction(
|
|
37
|
+
document.textDocument.uri,
|
|
38
|
+
replaceRange,
|
|
39
|
+
`Initialize as vocabulary`,
|
|
40
|
+
renderOntologyStub('vocabulary', initializer.iri, initializer.prefix)
|
|
41
|
+
),
|
|
42
|
+
createInitializeCodeAction(
|
|
43
|
+
document.textDocument.uri,
|
|
44
|
+
replaceRange,
|
|
45
|
+
`Initialize as description bundle`,
|
|
46
|
+
renderOntologyStub('description bundle', initializer.iri, initializer.prefix)
|
|
47
|
+
),
|
|
48
|
+
createInitializeCodeAction(
|
|
49
|
+
document.textDocument.uri,
|
|
50
|
+
replaceRange,
|
|
51
|
+
`Initialize as vocabulary bundle`,
|
|
52
|
+
renderOntologyStub('vocabulary bundle', initializer.iri, initializer.prefix)
|
|
53
|
+
)
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createInitializeCodeAction(uri: string, range: Range, title: string, newText: string): CodeAction {
|
|
59
|
+
return {
|
|
60
|
+
title,
|
|
61
|
+
kind: CodeActionKind.QuickFix,
|
|
62
|
+
edit: {
|
|
63
|
+
changes: {
|
|
64
|
+
[uri]: [{
|
|
65
|
+
range,
|
|
66
|
+
newText
|
|
67
|
+
}]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderOntologyStub(
|
|
74
|
+
kind: 'description' | 'vocabulary' | 'description bundle' | 'vocabulary bundle',
|
|
75
|
+
iri: string,
|
|
76
|
+
prefix: string
|
|
77
|
+
): string {
|
|
78
|
+
return `${kind} <${iri}> as ${prefix} {\n}\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function deriveOntologyInitializer(uri: string): { iri: string; prefix: string } | undefined {
|
|
82
|
+
let parsed: URL;
|
|
83
|
+
try {
|
|
84
|
+
parsed = new URL(uri);
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const decodedPath = decodeURIComponent(parsed.pathname);
|
|
90
|
+
const omlMarker = '/oml/';
|
|
91
|
+
const markerIndex = decodedPath.lastIndexOf(omlMarker);
|
|
92
|
+
if (markerIndex < 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const relativePath = decodedPath.slice(markerIndex + omlMarker.length);
|
|
97
|
+
if (!relativePath.endsWith('.oml')) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const withoutExtension = relativePath.slice(0, -'.oml'.length);
|
|
102
|
+
const segments = withoutExtension.split('/').filter(Boolean);
|
|
103
|
+
if (segments.length === 0) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const prefix = sanitizePrefix(segments[segments.length - 1]);
|
|
108
|
+
if (!prefix) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
iri: `http://${segments.join('/')}#`,
|
|
114
|
+
prefix
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sanitizePrefix(value: string): string {
|
|
119
|
+
return value.replace(/[^A-Za-z0-9_-]/g, '').trim();
|
|
120
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import type { AstNodeDescription, ReferenceInfo, Stream, LangiumDocuments, AstNodeLocator, IndexManager } from 'langium';
|
|
4
|
+
import type { CompletionItem, TextEdit } from 'vscode-languageserver-types';
|
|
5
|
+
import { DefaultCompletionProvider, type CompletionContext, type CompletionValueItem, type LangiumServices } from 'langium/lsp';
|
|
6
|
+
import { Element, isDescription, isDescriptionBox, isMember, isOntology, isVocabulary, isVocabularyBundle, Member, Ontology } from './generated/ast.js';
|
|
7
|
+
import { getLangiumDocumentVersion } from './oml-utils.js';
|
|
8
|
+
import { OmlCandidates } from './oml-candidates.js';
|
|
9
|
+
|
|
10
|
+
interface PreparedCompletionContext {
|
|
11
|
+
key: string;
|
|
12
|
+
ontology: Ontology;
|
|
13
|
+
importsByNamespace: Map<string, { prefix?: string }>;
|
|
14
|
+
importInsertOffset: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OML Completion Provider - Grammar-aware completions using Langium LSP
|
|
19
|
+
* Extends Langium's DefaultCompletionProvider to customize completion behavior for OML
|
|
20
|
+
*/
|
|
21
|
+
export class OmlCompletionProvider extends DefaultCompletionProvider {
|
|
22
|
+
private static readonly MaxResolvedMemberCaches = 5000;
|
|
23
|
+
protected readonly indexManager: IndexManager;
|
|
24
|
+
protected readonly langiumDocuments: LangiumDocuments;
|
|
25
|
+
protected readonly astNodeLocator: AstNodeLocator;
|
|
26
|
+
private readonly candidates: OmlCandidates;
|
|
27
|
+
private readonly resolvedMemberCache = new Map<string, Member | null>();
|
|
28
|
+
private preparedCompletionContext?: PreparedCompletionContext;
|
|
29
|
+
|
|
30
|
+
constructor(services: LangiumServices) {
|
|
31
|
+
super(services);
|
|
32
|
+
this.indexManager = services.shared.workspace.IndexManager;
|
|
33
|
+
this.langiumDocuments = services.shared.workspace.LangiumDocuments;
|
|
34
|
+
this.astNodeLocator = services.workspace.AstNodeLocator;
|
|
35
|
+
this.candidates = new OmlCandidates(this.indexManager, this.langiumDocuments, this.astNodeLocator);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get reference candidates - include both local definitions and imported IRIs
|
|
40
|
+
* Local definitions don't start with '<', imported ones do
|
|
41
|
+
*/
|
|
42
|
+
protected override getReferenceCandidates(refInfo: ReferenceInfo, _context: CompletionContext): Stream<AstNodeDescription> {
|
|
43
|
+
const referenceType = this.astReflection.getReferenceType(refInfo);
|
|
44
|
+
const linkable = this.scopeProvider.getScope(refInfo).getAllElements();
|
|
45
|
+
return this.candidates.getReferenceCandidates(referenceType, linkable);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Customize completion text edit to replace the entire token
|
|
50
|
+
*/
|
|
51
|
+
protected override buildCompletionTextEdit(context: CompletionContext, _label: string, newText: string): TextEdit | undefined {
|
|
52
|
+
const start = context.textDocument.positionAt(context.tokenOffset);
|
|
53
|
+
const end = context.position;
|
|
54
|
+
return { newText, range: { start, end } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fill completion item with OML-specific details:
|
|
59
|
+
* - Show abbreviated IRIs (prefix:name) instead of full IRIs
|
|
60
|
+
* - Include namespace in detail
|
|
61
|
+
* - Auto-generate import statements for cross-file references
|
|
62
|
+
*/
|
|
63
|
+
protected override fillCompletionItem(context: CompletionContext, item: CompletionValueItem): CompletionItem | undefined {
|
|
64
|
+
if ('nodeDescription' in item) {
|
|
65
|
+
const desc = item.nodeDescription;
|
|
66
|
+
const prepared = this.getPreparedCompletionContext(context);
|
|
67
|
+
if (!prepared) {
|
|
68
|
+
return super.fillCompletionItem(context, item);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if this is a member reference (could be a cross-file reference)
|
|
72
|
+
const member = this.resolveMember(desc);
|
|
73
|
+
if (member) {
|
|
74
|
+
const [namespace, name] = this.getIri(member);
|
|
75
|
+
const ontology = prepared.ontology;
|
|
76
|
+
|
|
77
|
+
// Find existing import for this namespace
|
|
78
|
+
const imp = prepared.importsByNamespace.get(namespace);
|
|
79
|
+
|
|
80
|
+
const importedOntology = this.getOntology(member);
|
|
81
|
+
const prefix = (imp && imp.prefix) ? imp.prefix : importedOntology.prefix;
|
|
82
|
+
|
|
83
|
+
// Build abbreviated IRI: prefix:name (or just name if same ontology)
|
|
84
|
+
const abbreviatedIri = (ontology !== importedOntology && prefix) ? `${prefix}:${name}` : name;
|
|
85
|
+
|
|
86
|
+
item = { ...item, detail: namespace, label: abbreviatedIri };
|
|
87
|
+
|
|
88
|
+
// Auto-generate import statement if not already imported and from different ontology
|
|
89
|
+
if (!imp && ontology !== importedOntology) {
|
|
90
|
+
const importStatement = this.getImportStatement(ontology, importedOntology);
|
|
91
|
+
|
|
92
|
+
if (importStatement) {
|
|
93
|
+
const start = context.textDocument.positionAt(prepared.importInsertOffset);
|
|
94
|
+
const addImportStatement: TextEdit = {
|
|
95
|
+
newText: '\n\n\t' + importStatement,
|
|
96
|
+
range: { start, end: start }
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
item = { ...item, additionalTextEdits: [addImportStatement] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return super.fillCompletionItem(context, item);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected resolveMember(desc: AstNodeDescription): Member | undefined {
|
|
108
|
+
if (desc.node && isMember(desc.node)) {
|
|
109
|
+
return desc.node as Member;
|
|
110
|
+
}
|
|
111
|
+
const doc = this.langiumDocuments.getDocument(desc.documentUri);
|
|
112
|
+
const root = doc?.parseResult?.value;
|
|
113
|
+
if (!root) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const key = `${desc.documentUri.toString()}@${getLangiumDocumentVersion(doc)}|${desc.path}`;
|
|
117
|
+
const cached = this.resolvedMemberCache.get(key);
|
|
118
|
+
if (cached !== undefined) {
|
|
119
|
+
return cached ?? undefined;
|
|
120
|
+
}
|
|
121
|
+
const resolved = this.astNodeLocator.getAstNode(root, desc.path);
|
|
122
|
+
const member = resolved && isMember(resolved) ? (resolved as Member) : undefined;
|
|
123
|
+
this.resolvedMemberCache.set(key, member ?? null);
|
|
124
|
+
this.pruneCache(this.resolvedMemberCache, OmlCompletionProvider.MaxResolvedMemberCaches);
|
|
125
|
+
return member;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract IRI components (namespace and name) from a member
|
|
130
|
+
*/
|
|
131
|
+
getIri(member: Member): [string, string] {
|
|
132
|
+
return [this.getOntology(member).namespace.replace(/^<|>$/g, ''), member.name!];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the ontology containing an element
|
|
137
|
+
*/
|
|
138
|
+
getOntology(element: Element): Ontology {
|
|
139
|
+
while (element && !isOntology(element)) {
|
|
140
|
+
element = (element as any).$container;
|
|
141
|
+
}
|
|
142
|
+
return element as Ontology;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Generate appropriate import statement based on ontology types
|
|
147
|
+
*/
|
|
148
|
+
getImportStatement(importing: Ontology, imported: Ontology): string | undefined {
|
|
149
|
+
const namespace = imported.namespace.replace(/^<|>$/g, '');
|
|
150
|
+
const prefixPart = imported.prefix ? ` as ${imported.prefix}` : '';
|
|
151
|
+
|
|
152
|
+
if (importing.$type === imported.$type) {
|
|
153
|
+
return `extends <${namespace}>${prefixPart}`;
|
|
154
|
+
} else if (isVocabulary(importing) && isDescription(imported)) {
|
|
155
|
+
return `uses <${namespace}>${prefixPart}`;
|
|
156
|
+
} else if (isDescriptionBox(importing) && isVocabulary(imported)) {
|
|
157
|
+
return `uses <${namespace}>${prefixPart}`;
|
|
158
|
+
} else if (isVocabularyBundle(importing) && isVocabulary(imported)) {
|
|
159
|
+
return `includes <${namespace}>${prefixPart}`;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getImportNamespace(ownedImport: any): string {
|
|
165
|
+
const raw = ownedImport?.imported?.$refText ?? ownedImport?.imported ?? '';
|
|
166
|
+
return typeof raw === 'string' ? raw.replace(/^<|>$/g, '') : '';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private getPreparedCompletionContext(context: CompletionContext): PreparedCompletionContext | undefined {
|
|
170
|
+
const ontology = context.document.parseResult?.value;
|
|
171
|
+
if (!ontology || !isOntology(ontology)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const uri = context.document.uri?.toString() ?? context.textDocument.uri;
|
|
175
|
+
const version = context.textDocument.version ?? 0;
|
|
176
|
+
const key = `${uri}@${version}@${context.tokenOffset}`;
|
|
177
|
+
if (this.preparedCompletionContext?.key === key) {
|
|
178
|
+
return this.preparedCompletionContext;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const importsByNamespace = new Map<string, { prefix?: string }>();
|
|
182
|
+
for (const ownedImport of ontology.ownedImports ?? []) {
|
|
183
|
+
const ns = this.getImportNamespace(ownedImport);
|
|
184
|
+
if (!ns || importsByNamespace.has(ns)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
importsByNamespace.set(ns, { prefix: ownedImport.prefix });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lastImport = ontology.ownedImports?.at(-1);
|
|
191
|
+
let importInsertOffset = 0;
|
|
192
|
+
if (lastImport?.$cstNode) {
|
|
193
|
+
importInsertOffset = lastImport.$cstNode.offset + lastImport.$cstNode.length;
|
|
194
|
+
} else {
|
|
195
|
+
const openBrace = context.textDocument.getText().indexOf('{');
|
|
196
|
+
importInsertOffset = openBrace >= 0 ? openBrace + 1 : 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.preparedCompletionContext = {
|
|
200
|
+
key,
|
|
201
|
+
ontology,
|
|
202
|
+
importsByNamespace,
|
|
203
|
+
importInsertOffset
|
|
204
|
+
};
|
|
205
|
+
return this.preparedCompletionContext;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private pruneCache<T>(cache: Map<string, T>, maxSize: number): void {
|
|
209
|
+
if (cache.size <= maxSize) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const overflow = cache.size - maxSize;
|
|
213
|
+
const keys = cache.keys();
|
|
214
|
+
for (let i = 0; i < overflow; i += 1) {
|
|
215
|
+
const next = keys.next();
|
|
216
|
+
if (next.done) {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
cache.delete(next.value);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import { DefaultValueConverter, GrammarAST } from 'langium';
|
|
4
|
+
import type { CstNode, ValueType } from 'langium';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes NAMESPACE tokens by stripping surrounding angle brackets when used via RuleCall.
|
|
8
|
+
* This ensures that namespace values stored in the AST are consistent and don't include the brackets.
|
|
9
|
+
*/
|
|
10
|
+
export class OmlValueConverter extends DefaultValueConverter {
|
|
11
|
+
protected override runConverter(rule: GrammarAST.AbstractRule, input: string, cstNode: CstNode): ValueType {
|
|
12
|
+
const source: any = (cstNode as any).grammarSource;
|
|
13
|
+
if (rule.name === 'NAMESPACE' && source?.$type === 'RuleCall') {
|
|
14
|
+
// Strip angle brackets from namespace: <http://example.com#> -> http://example.com#
|
|
15
|
+
return input.substring(1, input.length - 1);
|
|
16
|
+
}
|
|
17
|
+
if (rule.name === 'STRING') {
|
|
18
|
+
if (input.startsWith('"""')) {
|
|
19
|
+
const content = input.substring(3, input.length - 3);
|
|
20
|
+
return this.stripIndentation(content);
|
|
21
|
+
} else if (input.startsWith("'''")) {
|
|
22
|
+
const content = input.substring(3, input.length - 3);
|
|
23
|
+
return this.stripIndentation(content);
|
|
24
|
+
} else if (input.startsWith('"')) {
|
|
25
|
+
return input.substring(1, input.length - 1);
|
|
26
|
+
} else if (input.startsWith("'")) {
|
|
27
|
+
return input.substring(1, input.length - 1);
|
|
28
|
+
}
|
|
29
|
+
return input;
|
|
30
|
+
}
|
|
31
|
+
return super.runConverter(rule, input, cstNode);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private stripIndentation(content: string): string {
|
|
35
|
+
const lines = content.split(/\r?\n/);
|
|
36
|
+
if (lines.length <= 1) {
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
const tail = lines.slice(1);
|
|
40
|
+
let minIndent = Infinity;
|
|
41
|
+
|
|
42
|
+
for (const line of tail) {
|
|
43
|
+
if (line.trim().length === 0) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const match = line.match(/^[ \t]*/);
|
|
47
|
+
const indent = match ? match[0].length : 0;
|
|
48
|
+
if (indent < minIndent) {
|
|
49
|
+
minIndent = indent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (minIndent === Infinity || minIndent === 0) {
|
|
54
|
+
return content;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const newLines = [lines[0], ...tail.map(line => {
|
|
58
|
+
if (line.trim().length === 0) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
return line.substring(minIndent);
|
|
62
|
+
})];
|
|
63
|
+
|
|
64
|
+
return newLines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import { DefaultDocumentValidator, type ValidationOptions } from 'langium';
|
|
4
|
+
import type { LangiumDocument } from 'langium';
|
|
5
|
+
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
6
|
+
import type { Diagnostic } from 'vscode-languageserver-types';
|
|
7
|
+
import type { CancellationToken } from 'vscode-jsonrpc';
|
|
8
|
+
import type { OmlServices } from './oml-module.js';
|
|
9
|
+
|
|
10
|
+
export const OmlEmptyDocumentDiagnosticCode = 'oml/empty-document';
|
|
11
|
+
|
|
12
|
+
export class OmlDocumentValidator extends DefaultDocumentValidator {
|
|
13
|
+
constructor(services: OmlServices) {
|
|
14
|
+
super(services);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override async validateDocument(document: LangiumDocument, options: ValidationOptions = {}, cancelToken?: CancellationToken): Promise<Diagnostic[]> {
|
|
18
|
+
if (document.textDocument.getText().trim().length === 0) {
|
|
19
|
+
return [createEmptyDocumentDiagnostic()];
|
|
20
|
+
}
|
|
21
|
+
return super.validateDocument(document, options, cancelToken);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createEmptyDocumentDiagnostic(): Diagnostic {
|
|
26
|
+
return {
|
|
27
|
+
severity: DiagnosticSeverity.Error,
|
|
28
|
+
range: {
|
|
29
|
+
start: { line: 0, character: 0 },
|
|
30
|
+
end: { line: 0, character: 0 }
|
|
31
|
+
},
|
|
32
|
+
message: 'Empty OML file. Initialize it as a description, vocabulary, description bundle, or vocabulary bundle.',
|
|
33
|
+
code: OmlEmptyDocumentDiagnosticCode,
|
|
34
|
+
data: {
|
|
35
|
+
code: OmlEmptyDocumentDiagnosticCode
|
|
36
|
+
},
|
|
37
|
+
source: 'oml'
|
|
38
|
+
};
|
|
39
|
+
}
|