@likec4/language-server 1.44.0 → 1.46.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/dist/LikeC4LanguageServices.d.ts +4 -15
- package/dist/LikeC4LanguageServices.js +4 -32
- package/dist/Rpc.js +44 -21
- package/dist/ast.d.ts +10 -0
- package/dist/ast.js +13 -2
- package/dist/browser.js +2 -2
- package/dist/bundled.js +2 -0
- package/dist/bundled.mjs +3838 -4059
- package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
- package/dist/filesystem/ChokidarWatcher.js +29 -18
- package/dist/filesystem/LikeC4FileSystem.js +4 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/generated/ast.d.ts +46 -9
- package/dist/generated/ast.js +56 -4
- package/dist/generated/grammar.js +1 -1
- package/dist/generated-lib/icons.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -3
- package/dist/lsp/DocumentSymbolProvider.js +12 -1
- package/dist/mcp/server/StdioLikeC4MCPServer.js +10 -6
- package/dist/mcp/server/StreamableLikeC4MCPServer.js +97 -97
- package/dist/mcp/server/WithMCPServer.js +4 -6
- package/dist/mcp/tools/read-deployment.js +18 -0
- package/dist/mcp/tools/read-element.js +24 -0
- package/dist/mcp/tools/search-element.js +5 -5
- package/dist/mcp/utils.js +1 -1
- package/dist/model/builder/buildModel.js +70 -1
- package/dist/model/deployments-index.js +2 -2
- package/dist/model/fqn-index.d.ts +1 -2
- package/dist/model/fqn-index.js +21 -18
- package/dist/model/model-builder.js +0 -2
- package/dist/model/model-parser.d.ts +3 -0
- package/dist/model/model-parser.js +41 -27
- package/dist/model/parser/Base.js +8 -3
- package/dist/model/parser/GlobalsParser.d.ts +1 -0
- package/dist/model/parser/ModelParser.d.ts +2 -1
- package/dist/model/parser/ModelParser.js +45 -1
- package/dist/model/parser/SpecificationParser.js +4 -0
- package/dist/model/parser/ViewsParser.d.ts +1 -0
- package/dist/model/parser/ViewsParser.js +16 -1
- package/dist/model-change/ModelChanges.d.ts +2 -2
- package/dist/model-change/ModelChanges.js +41 -11
- package/dist/protocol.d.ts +33 -10
- package/dist/protocol.js +13 -4
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +11 -1
- package/dist/validation/relation.d.ts +1 -0
- package/dist/validation/relation.js +87 -1
- package/dist/validation/view-checks.d.ts +4 -0
- package/dist/validation/view-checks.js +46 -0
- package/dist/view-utils/manual-layout.js +2 -4
- package/dist/views/LikeC4ManualLayouts.d.ts +16 -2
- package/dist/views/LikeC4ManualLayouts.js +100 -23
- package/dist/views/LikeC4Views.d.ts +26 -5
- package/dist/views/LikeC4Views.js +49 -33
- package/dist/workspace/AstNodeDescriptionProvider.js +6 -3
- package/dist/workspace/IndexManager.js +1 -1
- package/dist/workspace/LangiumDocuments.d.ts +3 -2
- package/dist/workspace/LangiumDocuments.js +29 -15
- package/dist/workspace/ProjectsManager.d.ts +45 -16
- package/dist/workspace/ProjectsManager.js +227 -45
- package/dist/workspace/WorkspaceManager.js +43 -0
- package/package.json +22 -21
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2023-2025 Denis Davydkov
|
|
4
|
+
// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
5
|
+
//
|
|
6
|
+
// Portions of this file have been modified by NVIDIA CORPORATION & AFFILIATES.
|
|
1
7
|
import { invariant, isNonEmptyArray, LinkedList, nonexhaustive, nonNullable } from '@likec4/core';
|
|
2
8
|
import { exact, FqnRef } from '@likec4/core/types';
|
|
3
9
|
import { loggable } from '@likec4/log';
|
|
@@ -15,7 +21,12 @@ function* streamModel(doc) {
|
|
|
15
21
|
relations.push(el);
|
|
16
22
|
continue;
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
// Skip ExtendRelation as it doesn't have child elements
|
|
25
|
+
if (ast.isExtendRelation(el)) {
|
|
26
|
+
yield el;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (el.body && 'elements' in el.body && el.body.elements && el.body.elements.length > 0) {
|
|
19
30
|
for (const child of el.body.elements) {
|
|
20
31
|
traverseStack.push(child);
|
|
21
32
|
}
|
|
@@ -48,6 +59,13 @@ export function ModelParser(B) {
|
|
|
48
59
|
}
|
|
49
60
|
continue;
|
|
50
61
|
}
|
|
62
|
+
if (ast.isExtendRelation(el)) {
|
|
63
|
+
const parsed = this.parseExtendRelation(el);
|
|
64
|
+
if (parsed) {
|
|
65
|
+
doc.c4ExtendRelations.push(parsed);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
51
69
|
nonexhaustive(el);
|
|
52
70
|
}
|
|
53
71
|
catch (e) {
|
|
@@ -107,6 +125,32 @@ export function ModelParser(B) {
|
|
|
107
125
|
links: isNonEmptyArray(links) ? links : null,
|
|
108
126
|
});
|
|
109
127
|
}
|
|
128
|
+
parseExtendRelation(astNode) {
|
|
129
|
+
const source = this.parseFqnRef(astNode.source);
|
|
130
|
+
const target = this.parseFqnRef(astNode.target);
|
|
131
|
+
invariant(FqnRef.isModelRef(source) || FqnRef.isImportRef(source), 'Source must be a model reference');
|
|
132
|
+
invariant(FqnRef.isModelRef(target) || FqnRef.isImportRef(target), 'Target must be a model reference');
|
|
133
|
+
const tags = this.parseTags(astNode.body);
|
|
134
|
+
const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty));
|
|
135
|
+
const astPath = this.getAstNodePath(astNode);
|
|
136
|
+
const links = this.parseLinks(astNode.body) ?? [];
|
|
137
|
+
if (!tags && isEmpty(metadata ?? {}) && isEmpty(links)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// Generate a stable relation ID based on source, target, kind, and title
|
|
141
|
+
// This allows extends to match specific relations between elements
|
|
142
|
+
const kind = (astNode.kind ?? astNode.dotKind?.kind)?.ref?.name;
|
|
143
|
+
// Normalize title the same way as parseRelation does
|
|
144
|
+
const { title = '' } = this.parseBaseProps({}, { title: astNode.title });
|
|
145
|
+
const id = stringHash('extend-relation', FqnRef.flatten(source), FqnRef.flatten(target), kind ?? 'default', title);
|
|
146
|
+
return exact({
|
|
147
|
+
id,
|
|
148
|
+
astPath,
|
|
149
|
+
metadata,
|
|
150
|
+
tags,
|
|
151
|
+
links: isNonEmptyArray(links) ? links : null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
110
154
|
_resolveRelationSource(node) {
|
|
111
155
|
if (isDefined(node.source)) {
|
|
112
156
|
const source = this.parseFqnRef(node.source);
|
|
@@ -55,6 +55,10 @@ export function SpecificationParser(B) {
|
|
|
55
55
|
const tag = tagSpec.tag.name;
|
|
56
56
|
const astPath = this.getAstNodePath(tagSpec.tag);
|
|
57
57
|
const color = tagSpec.color && this.parseColorLiteral(tagSpec.color);
|
|
58
|
+
if (tag in c4Specification.tags) {
|
|
59
|
+
logger.warn(`Tag {tag} is already defined, skipping duplicate`, { tag });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
58
62
|
if (isTruthy(tag)) {
|
|
59
63
|
c4Specification.tags[tag] = {
|
|
60
64
|
astPath,
|
|
@@ -14,6 +14,7 @@ export declare function ViewsParser<TBase extends WithPredicates & WithDeploymen
|
|
|
14
14
|
parseViewRuleGlobalPredicateRef(astRule: ast.ViewRuleGlobalPredicateRef | ast.DynamicViewGlobalPredicateRef): c4.ViewRuleGlobalPredicateRef;
|
|
15
15
|
parseViewRuleStyleOrGlobalRef(astRule: ast.ViewRuleStyleOrGlobalRef): ViewRuleStyleOrGlobalRef;
|
|
16
16
|
parseViewRuleGroup(astNode: ast.ViewRuleGroup): c4.ElementViewRuleGroup;
|
|
17
|
+
parseViewRuleRank(astRule: ast.ViewRuleRank): c4.ElementViewRuleRank;
|
|
17
18
|
parseViewRuleStyle(astRule: ast.ViewRuleStyle | ast.GlobalStyle): c4.ElementViewRuleStyle;
|
|
18
19
|
parseViewRuleGlobalStyle(astRule: ast.ViewRuleGlobalStyle): c4.ViewRuleGlobalStyle;
|
|
19
20
|
parseDynamicElementView(astNode: ast.DynamicView, additionalStyles: ViewRuleStyleOrGlobalRef[]): ParsedAstDynamicView;
|
|
@@ -9,6 +9,7 @@ import { elementRef } from '../../utils/elementRef';
|
|
|
9
9
|
import { parseViewManualLayout } from '../../view-utils/manual-layout';
|
|
10
10
|
import { removeIndent, toSingleLine } from './Base';
|
|
11
11
|
const logger = mainLogger.getChild('ViewsParser');
|
|
12
|
+
const rankLogger = logger.getChild('rank');
|
|
12
13
|
export function ViewsParser(B) {
|
|
13
14
|
return class ViewsParser extends B {
|
|
14
15
|
parseViews() {
|
|
@@ -63,7 +64,9 @@ export function ViewsParser(B) {
|
|
|
63
64
|
const viewOfEl = elementRef(astNode.viewOf);
|
|
64
65
|
const _viewOf = viewOfEl && this.resolveFqn(viewOfEl);
|
|
65
66
|
if (!_viewOf) {
|
|
66
|
-
|
|
67
|
+
const viewId = astNode.name ?? 'unnamed';
|
|
68
|
+
const msg = astNode.viewOf.$cstNode?.text ?? '<unknown>';
|
|
69
|
+
logger.warn(`viewOf {viewId} not resolved {msg}`, { msg, viewId });
|
|
67
70
|
}
|
|
68
71
|
else {
|
|
69
72
|
viewOf = _viewOf;
|
|
@@ -126,6 +129,9 @@ export function ViewsParser(B) {
|
|
|
126
129
|
if (ast.isViewRuleGroup(astRule)) {
|
|
127
130
|
return this.parseViewRuleGroup(astRule);
|
|
128
131
|
}
|
|
132
|
+
if (ast.isViewRuleRank(astRule)) {
|
|
133
|
+
return this.parseViewRuleRank(astRule);
|
|
134
|
+
}
|
|
129
135
|
nonexhaustive(astRule);
|
|
130
136
|
}
|
|
131
137
|
parseViewRulePredicate(astNode) {
|
|
@@ -190,6 +196,15 @@ export function ViewsParser(B) {
|
|
|
190
196
|
...this.parseStyleProps(astNode.props),
|
|
191
197
|
};
|
|
192
198
|
}
|
|
199
|
+
parseViewRuleRank(astRule) {
|
|
200
|
+
const targets = this.parseFqnExpressions(astRule.targets).filter((e) => c4.ModelExpression.isFqnExpr(e));
|
|
201
|
+
const rank = astRule.value ?? 'same';
|
|
202
|
+
rankLogger.debug `Parsed rank constraint ${rank} with ${targets.length} target(s)`;
|
|
203
|
+
return {
|
|
204
|
+
rank,
|
|
205
|
+
targets,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
193
208
|
parseViewRuleStyle(astRule) {
|
|
194
209
|
const targets = this.parseFqnExpressions(astRule.targets).filter((e) => c4.ModelExpression.isFqnExpr(e));
|
|
195
210
|
const style = this.parseStyleProps(astRule.props.filter(ast.isStyleProperty));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ViewChange } from '@likec4/core';
|
|
2
|
-
import {
|
|
2
|
+
import { Range, TextEdit } from 'vscode-languageserver-types';
|
|
3
3
|
import type { ViewLocateResult } from '../model';
|
|
4
4
|
import type { LikeC4Services } from '../module';
|
|
5
5
|
import type { ChangeView } from '../protocol';
|
|
@@ -7,7 +7,7 @@ export declare class LikeC4ModelChanges {
|
|
|
7
7
|
private services;
|
|
8
8
|
private locator;
|
|
9
9
|
constructor(services: LikeC4Services);
|
|
10
|
-
applyChange(changeView: ChangeView.Params): Promise<
|
|
10
|
+
applyChange(changeView: ChangeView.Params): Promise<ChangeView.Res>;
|
|
11
11
|
protected convertToTextEdit({ lookup, change }: {
|
|
12
12
|
lookup: ViewLocateResult;
|
|
13
13
|
change: Exclude<ViewChange, ViewChange.SaveViewSnapshot | ViewChange.ResetManualLayout>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { invariant, nonexhaustive } from '@likec4/core';
|
|
2
|
-
import {
|
|
2
|
+
import { loggable, wrapError } from '@likec4/log';
|
|
3
|
+
import { Range, TextEdit } from 'vscode-languageserver-types';
|
|
3
4
|
import { logger as mainLogger } from '../logger';
|
|
4
5
|
import { changeElementStyle } from './changeElementStyle';
|
|
5
6
|
import { changeViewLayout } from './changeViewLayout';
|
|
@@ -14,7 +15,6 @@ export class LikeC4ModelChanges {
|
|
|
14
15
|
}
|
|
15
16
|
async applyChange(changeView) {
|
|
16
17
|
const lspConnection = this.services.shared.lsp.Connection;
|
|
17
|
-
invariant(lspConnection, 'LSP Connection not available');
|
|
18
18
|
let result = null;
|
|
19
19
|
try {
|
|
20
20
|
await this.services.shared.workspace.WorkspaceLock.write(async () => {
|
|
@@ -23,7 +23,7 @@ export class LikeC4ModelChanges {
|
|
|
23
23
|
logger.debug `Applying model change ${change.op} to view ${viewId} in project ${project.id}`;
|
|
24
24
|
const lookup = this.locator.locateViewAst(viewId, project.id);
|
|
25
25
|
if (!lookup) {
|
|
26
|
-
throw new Error(`
|
|
26
|
+
throw new Error(`View ${viewId} not found in project ${project.id}`);
|
|
27
27
|
}
|
|
28
28
|
const textDocument = {
|
|
29
29
|
uri: lookup.doc.textDocument.uri,
|
|
@@ -31,7 +31,7 @@ export class LikeC4ModelChanges {
|
|
|
31
31
|
};
|
|
32
32
|
// TODO refactor to use separate methods for save/reset operations
|
|
33
33
|
if (change.op === 'save-view-snapshot') {
|
|
34
|
-
invariant(viewId === change.layout.id, 'View ID does not match');
|
|
34
|
+
invariant(viewId === change.layout.id, 'View ID does not match, expected ' + viewId + ', got ' + change.layout.id);
|
|
35
35
|
// If there is an existing manual layout v1
|
|
36
36
|
if (lookup.view.manualLayout) {
|
|
37
37
|
// We clean it up
|
|
@@ -39,13 +39,29 @@ export class LikeC4ModelChanges {
|
|
|
39
39
|
logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
const location = await this.services.likec4.ManualLayouts.write(project, change.layout);
|
|
43
|
+
result = {
|
|
44
|
+
success: true,
|
|
45
|
+
location,
|
|
46
|
+
};
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
45
49
|
if (change.op === 'reset-manual-layout') {
|
|
46
|
-
|
|
50
|
+
// If there is an existing manual layout v1
|
|
51
|
+
if (lookup.view.manualLayout) {
|
|
52
|
+
// We clean it up
|
|
53
|
+
await removeManualLayoutV1(this.services, { lookup }).catch(err => {
|
|
54
|
+
logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const location = await this.services.likec4.ManualLayouts.remove(project, viewId);
|
|
58
|
+
result = {
|
|
59
|
+
success: true,
|
|
60
|
+
location,
|
|
61
|
+
};
|
|
47
62
|
return;
|
|
48
63
|
}
|
|
64
|
+
invariant(lspConnection, 'This change only supported in IDE (running as Extension)');
|
|
49
65
|
const { edits, modifiedRange } = this.convertToTextEdit({
|
|
50
66
|
lookup,
|
|
51
67
|
change,
|
|
@@ -66,15 +82,29 @@ export class LikeC4ModelChanges {
|
|
|
66
82
|
return;
|
|
67
83
|
}
|
|
68
84
|
result = {
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
success: true,
|
|
86
|
+
location: {
|
|
87
|
+
uri: textDocument.uri,
|
|
88
|
+
range: modifiedRange,
|
|
89
|
+
},
|
|
71
90
|
};
|
|
72
91
|
});
|
|
73
92
|
}
|
|
74
|
-
catch (
|
|
75
|
-
|
|
93
|
+
catch (err) {
|
|
94
|
+
const error = loggable(wrapError(err, `Failed to apply change ${changeView.change.op} ${changeView.viewId}`));
|
|
95
|
+
logger.error(error);
|
|
96
|
+
result = {
|
|
97
|
+
success: false,
|
|
98
|
+
error,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this.services.likec4.ModelBuilder.clearCache();
|
|
76
103
|
}
|
|
77
|
-
return result
|
|
104
|
+
return result ?? {
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'Unknown error applying model change',
|
|
107
|
+
};
|
|
78
108
|
}
|
|
79
109
|
convertToTextEdit({ lookup, change }) {
|
|
80
110
|
switch (change.op) {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -6,6 +6,18 @@ export declare namespace DidChangeModelNotification {
|
|
|
6
6
|
const type: NotificationType<string>;
|
|
7
7
|
type Type = typeof type;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* When the snapshot of a manual layout changes
|
|
11
|
+
* Send by the editor to the language server
|
|
12
|
+
*/
|
|
13
|
+
export declare namespace DidChangeSnapshotNotification {
|
|
14
|
+
type Params = {
|
|
15
|
+
snapshotUri: DocumentUri;
|
|
16
|
+
};
|
|
17
|
+
const Method: "likec4/onDidChangeSnapshot";
|
|
18
|
+
const type: NotificationType<Params>;
|
|
19
|
+
type Type = typeof type;
|
|
20
|
+
}
|
|
9
21
|
/**
|
|
10
22
|
* When server requests to open a likec4 preview panel
|
|
11
23
|
* (available only in the editor).
|
|
@@ -72,6 +84,7 @@ export declare namespace LayoutView {
|
|
|
72
84
|
type Params = {
|
|
73
85
|
viewId: ViewId;
|
|
74
86
|
projectId?: string | undefined;
|
|
87
|
+
layoutType?: 'auto' | 'manual' | undefined;
|
|
75
88
|
};
|
|
76
89
|
type Res = {
|
|
77
90
|
result: {
|
|
@@ -102,8 +115,8 @@ export declare namespace ValidateLayout {
|
|
|
102
115
|
};
|
|
103
116
|
}[] | null;
|
|
104
117
|
};
|
|
105
|
-
const
|
|
106
|
-
type Req = typeof
|
|
118
|
+
const req: RequestType<Params, Res, void>;
|
|
119
|
+
type Req = typeof req;
|
|
107
120
|
}
|
|
108
121
|
/**
|
|
109
122
|
* Request to reload projects.
|
|
@@ -123,7 +136,10 @@ export declare namespace FetchProjects {
|
|
|
123
136
|
projects: {
|
|
124
137
|
[projectId: ProjectId]: {
|
|
125
138
|
folder: URI;
|
|
126
|
-
config:
|
|
139
|
+
config: {
|
|
140
|
+
name: string;
|
|
141
|
+
title?: string | undefined;
|
|
142
|
+
};
|
|
127
143
|
docs: NonEmptyArray<DocumentUri>;
|
|
128
144
|
};
|
|
129
145
|
};
|
|
@@ -152,8 +168,8 @@ export declare namespace BuildDocuments {
|
|
|
152
168
|
type Params = {
|
|
153
169
|
docs: DocumentUri[];
|
|
154
170
|
};
|
|
155
|
-
const
|
|
156
|
-
type Req = typeof
|
|
171
|
+
const req: RequestType<Params, void, void>;
|
|
172
|
+
type Req = typeof req;
|
|
157
173
|
}
|
|
158
174
|
/**
|
|
159
175
|
* Request to locate an element, relation, deployment or view.
|
|
@@ -200,8 +216,8 @@ export declare namespace Locate {
|
|
|
200
216
|
projectId?: string | undefined;
|
|
201
217
|
};
|
|
202
218
|
type Res = Location | null;
|
|
203
|
-
const
|
|
204
|
-
type Req = typeof
|
|
219
|
+
const req: RequestType<Params, Res, void>;
|
|
220
|
+
type Req = typeof req;
|
|
205
221
|
}
|
|
206
222
|
/**
|
|
207
223
|
* Request to change the view
|
|
@@ -213,9 +229,16 @@ export declare namespace ChangeView {
|
|
|
213
229
|
change: ViewChange;
|
|
214
230
|
projectId?: string | undefined;
|
|
215
231
|
};
|
|
216
|
-
type Res =
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
type Res = {
|
|
233
|
+
success: true;
|
|
234
|
+
location: Location | null;
|
|
235
|
+
} | {
|
|
236
|
+
success: false;
|
|
237
|
+
location?: Location | null;
|
|
238
|
+
error: string;
|
|
239
|
+
};
|
|
240
|
+
const req: RequestType<Params, Res, void>;
|
|
241
|
+
type Req = typeof req;
|
|
219
242
|
}
|
|
220
243
|
/**
|
|
221
244
|
* Request to fetch telemetry metrics
|
package/dist/protocol.js
CHANGED
|
@@ -3,6 +3,15 @@ export var DidChangeModelNotification;
|
|
|
3
3
|
(function (DidChangeModelNotification) {
|
|
4
4
|
DidChangeModelNotification.type = new NotificationType('likec4/onDidChangeModel');
|
|
5
5
|
})(DidChangeModelNotification || (DidChangeModelNotification = {}));
|
|
6
|
+
/**
|
|
7
|
+
* When the snapshot of a manual layout changes
|
|
8
|
+
* Send by the editor to the language server
|
|
9
|
+
*/
|
|
10
|
+
export var DidChangeSnapshotNotification;
|
|
11
|
+
(function (DidChangeSnapshotNotification) {
|
|
12
|
+
DidChangeSnapshotNotification.Method = 'likec4/onDidChangeSnapshot';
|
|
13
|
+
DidChangeSnapshotNotification.type = new NotificationType(DidChangeSnapshotNotification.Method);
|
|
14
|
+
})(DidChangeSnapshotNotification || (DidChangeSnapshotNotification = {}));
|
|
6
15
|
/**
|
|
7
16
|
* When server requests to open a likec4 preview panel
|
|
8
17
|
* (available only in the editor).
|
|
@@ -51,7 +60,7 @@ export var LayoutView;
|
|
|
51
60
|
*/
|
|
52
61
|
export var ValidateLayout;
|
|
53
62
|
(function (ValidateLayout) {
|
|
54
|
-
ValidateLayout.
|
|
63
|
+
ValidateLayout.req = new RequestType('likec4/validate-layout');
|
|
55
64
|
})(ValidateLayout || (ValidateLayout = {}));
|
|
56
65
|
/**
|
|
57
66
|
* Request to reload projects.
|
|
@@ -79,7 +88,7 @@ export var RegisterProject;
|
|
|
79
88
|
*/
|
|
80
89
|
export var BuildDocuments;
|
|
81
90
|
(function (BuildDocuments) {
|
|
82
|
-
BuildDocuments.
|
|
91
|
+
BuildDocuments.req = new RequestType('likec4/build');
|
|
83
92
|
})(BuildDocuments || (BuildDocuments = {}));
|
|
84
93
|
/**
|
|
85
94
|
* Request to locate an element, relation, deployment or view.
|
|
@@ -87,7 +96,7 @@ export var BuildDocuments;
|
|
|
87
96
|
*/
|
|
88
97
|
export var Locate;
|
|
89
98
|
(function (Locate) {
|
|
90
|
-
Locate.
|
|
99
|
+
Locate.req = new RequestType('likec4/locate');
|
|
91
100
|
})(Locate || (Locate = {}));
|
|
92
101
|
// #endregion
|
|
93
102
|
/**
|
|
@@ -96,7 +105,7 @@ export var Locate;
|
|
|
96
105
|
*/
|
|
97
106
|
export var ChangeView;
|
|
98
107
|
(function (ChangeView) {
|
|
99
|
-
ChangeView.
|
|
108
|
+
ChangeView.req = new RequestType('likec4/change-view');
|
|
100
109
|
})(ChangeView || (ChangeView = {}));
|
|
101
110
|
/**
|
|
102
111
|
* Request to fetch telemetry metrics
|
|
@@ -4,7 +4,7 @@ import type { LikeC4Services } from '../module';
|
|
|
4
4
|
export { LikeC4DocumentValidator } from './DocumentValidator';
|
|
5
5
|
type Guard<N extends AstNode> = (n: AstNode) => n is N;
|
|
6
6
|
type Guarded<G> = G extends Guard<infer N> ? N : never;
|
|
7
|
-
declare const isValidatableAstNode: (n: AstNode) => n is ast.DynamicViewDisplayVariantProperty | ast.LinkProperty | ast.ViewStringProperty | ast.ElementStringProperty | ast.ElementStyleProperty | ast.IconProperty | ast.MetadataBody | ast.RelationStringProperty | ast.MetadataAttribute | ast.NotationProperty | ast.NotesProperty | ast.SpecificationElementStringProperty | ast.SpecificationRelationshipStringProperty | ast.HexColor | ast.RGBAColor | ast.DeployedInstance | ast.DeploymentNode | ast.DeploymentViewRulePredicate | ast.DeploymentViewRuleStyle | ast.ViewRuleAutoLayout | ast.DynamicViewGlobalPredicateRef | ast.DynamicViewIncludePredicate | ast.ViewRuleGlobalStyle | ast.ViewRuleStyle | ast.DynamicStepChain | ast.DynamicStepSingle | ast.ElementKindExpression | ast.ElementTagExpression | ast.FqnRefExpr | ast.WildcardExpression | ast.FqnExprWhere | ast.FqnExprWith | ast.DirectedRelationExpr | ast.InOutRelationExpr | ast.IncomingRelationExpr | ast.OutgoingRelationExpr | ast.RelationExprWhere | ast.RelationExprWith | ast.Element | ast.ExtendDeployment | ast.ExtendElement | ast.Imported | ast.DeploymentView | ast.DynamicView | ast.ElementView | ast.ArrowProperty | ast.ColorProperty | ast.LineProperty | ast.PaddingSizeProperty | ast.ShapeSizeProperty | ast.TextSizeProperty | ast.BorderProperty | ast.MultipleProperty | ast.OpacityProperty | ast.ShapeProperty | ast.ViewRuleGlobalPredicateRef | ast.ViewRuleGroup | ast.ViewRulePredicate | ast.DynamicViewParallelSteps | ast.SpecificationRelationshipKind | ast.SpecificationRule | ast.ElementRef | ast.DeploymentRelation | ast.Relation | ast.GlobalDynamicPredicateGroup | ast.Globals | ast.GlobalPredicateGroup | ast.GlobalStyle | ast.SpecificationDeploymentNodeKind | ast.SpecificationElementKind | ast.GlobalStyleGroup | ast.SpecificationColor | ast.NavigateToProperty | ast.Tags | ast.ImportsFromPoject | ast.SpecificationTag;
|
|
7
|
+
declare const isValidatableAstNode: (n: AstNode) => n is ast.DynamicViewDisplayVariantProperty | ast.LinkProperty | ast.ViewStringProperty | ast.ElementStringProperty | ast.ElementStyleProperty | ast.IconProperty | ast.MetadataBody | ast.RelationStringProperty | ast.MetadataAttribute | ast.NotationProperty | ast.NotesProperty | ast.SpecificationElementStringProperty | ast.SpecificationRelationshipStringProperty | ast.HexColor | ast.RGBAColor | ast.DeployedInstance | ast.DeploymentNode | ast.DeploymentViewRulePredicate | ast.DeploymentViewRuleStyle | ast.ViewRuleAutoLayout | ast.DynamicViewGlobalPredicateRef | ast.DynamicViewIncludePredicate | ast.ViewRuleGlobalStyle | ast.ViewRuleStyle | ast.DynamicStepChain | ast.DynamicStepSingle | ast.ElementKindExpression | ast.ElementTagExpression | ast.FqnRefExpr | ast.WildcardExpression | ast.FqnExprWhere | ast.FqnExprWith | ast.DirectedRelationExpr | ast.InOutRelationExpr | ast.IncomingRelationExpr | ast.OutgoingRelationExpr | ast.RelationExprWhere | ast.RelationExprWith | ast.Element | ast.ExtendDeployment | ast.ExtendElement | ast.Imported | ast.DeploymentView | ast.DynamicView | ast.ElementView | ast.ArrowProperty | ast.ColorProperty | ast.LineProperty | ast.PaddingSizeProperty | ast.ShapeSizeProperty | ast.TextSizeProperty | ast.BorderProperty | ast.MultipleProperty | ast.OpacityProperty | ast.ShapeProperty | ast.ViewRuleGlobalPredicateRef | ast.ViewRuleGroup | ast.ViewRulePredicate | ast.ViewRuleRank | ast.DynamicViewParallelSteps | ast.SpecificationRelationshipKind | ast.SpecificationRule | ast.ElementRef | ast.DeploymentRelation | ast.Relation | ast.GlobalDynamicPredicateGroup | ast.Globals | ast.GlobalPredicateGroup | ast.GlobalStyle | ast.SpecificationDeploymentNodeKind | ast.SpecificationElementKind | ast.GlobalStyleGroup | ast.SpecificationColor | ast.NavigateToProperty | ast.Tags | ast.ExtendRelation | ast.ImportsFromPoject | ast.SpecificationTag;
|
|
8
8
|
type ValidatableAstNode = Guarded<typeof isValidatableAstNode>;
|
|
9
9
|
export declare function checksFromDiagnostics(doc: LikeC4LangiumDocument): {
|
|
10
10
|
isValid: (n: ValidatableAstNode) => boolean;
|
package/dist/validation/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2023-2025 Denis Davydkov
|
|
4
|
+
// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
5
|
+
//
|
|
6
|
+
// Portions of this file have been modified by NVIDIA CORPORATION & AFFILIATES.
|
|
1
7
|
import { onNextTick } from '@likec4/core/utils';
|
|
2
8
|
import { loggable } from '@likec4/log';
|
|
3
9
|
import { DocumentState } from 'langium';
|
|
@@ -11,9 +17,10 @@ import { checkElement } from './element';
|
|
|
11
17
|
import { checkElementRef } from './element-ref';
|
|
12
18
|
import { checkImportsFromPoject } from './imports';
|
|
13
19
|
import { colorLiteralRuleChecks, iconPropertyRuleChecks, notesPropertyRuleChecks, opacityPropertyRuleChecks, } from './property-checks';
|
|
14
|
-
import { checkRelationBody, relationChecks } from './relation';
|
|
20
|
+
import { checkRelationBody, extendRelationChecks, relationChecks } from './relation';
|
|
15
21
|
import { checkDeploymentNodeKind, checkElementKind, checkGlobalPredicate, checkGlobals, checkGlobalStyleId, checkModel, checkRelationshipKind, checkSpecificationRule, checkTag, } from './specification';
|
|
16
22
|
import { viewChecks } from './view';
|
|
23
|
+
import { viewRuleRankChecks } from './view-checks';
|
|
17
24
|
import { checkFqnExprWith, checkFqnRefExpr, checkIncomingRelationExpr, checkOutgoingRelationExpr, checkRelationExpr, checkRelationExprWith, } from './view-predicates';
|
|
18
25
|
export { LikeC4DocumentValidator } from './DocumentValidator';
|
|
19
26
|
function validatableAstNodeGuards(predicates) {
|
|
@@ -60,6 +67,7 @@ const isValidatableAstNode = validatableAstNodeGuards([
|
|
|
60
67
|
ast.isElementRef,
|
|
61
68
|
ast.isExtendElement,
|
|
62
69
|
ast.isExtendDeployment,
|
|
70
|
+
ast.isExtendRelation,
|
|
63
71
|
ast.isSpecificationElementKind,
|
|
64
72
|
ast.isSpecificationRelationshipKind,
|
|
65
73
|
ast.isSpecificationDeploymentNodeKind,
|
|
@@ -108,6 +116,7 @@ export function registerValidationChecks(services) {
|
|
|
108
116
|
DeploymentNode: deploymentNodeChecks(services),
|
|
109
117
|
DeploymentRelation: deploymentRelationChecks(services),
|
|
110
118
|
ExtendDeployment: extendDeploymentChecks(services),
|
|
119
|
+
ExtendRelation: extendRelationChecks(services),
|
|
111
120
|
FqnRefExpr: checkFqnRefExpr(services),
|
|
112
121
|
RelationExpr: checkRelationExpr(services),
|
|
113
122
|
NotesProperty: notesPropertyRuleChecks(services),
|
|
@@ -137,6 +146,7 @@ export function registerValidationChecks(services) {
|
|
|
137
146
|
// Imported: checkImported(services),
|
|
138
147
|
ColorLiteral: colorLiteralRuleChecks(services),
|
|
139
148
|
DynamicViewDisplayVariantProperty: dynamicViewDisplayVariant(services),
|
|
149
|
+
ViewRuleRank: viewRuleRankChecks(services),
|
|
140
150
|
});
|
|
141
151
|
const connection = services.shared.lsp.Connection;
|
|
142
152
|
if (connection) {
|
|
@@ -3,3 +3,4 @@ import { ast } from '../ast';
|
|
|
3
3
|
import type { LikeC4Services } from '../module';
|
|
4
4
|
export declare const relationChecks: (services: LikeC4Services) => ValidationCheck<ast.Relation>;
|
|
5
5
|
export declare const checkRelationBody: (_services: LikeC4Services) => ValidationCheck<ast.RelationBody>;
|
|
6
|
+
export declare const extendRelationChecks: (services: LikeC4Services) => ValidationCheck<ast.ExtendRelation>;
|
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2023-2025 Denis Davydkov
|
|
4
|
+
// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
5
|
+
//
|
|
6
|
+
// Portions of this file have been modified by NVIDIA CORPORATION & AFFILIATES.
|
|
1
7
|
import { FqnRef, isSameHierarchy } from '@likec4/core';
|
|
2
8
|
import { AstUtils } from 'langium';
|
|
3
|
-
import { ast } from '../ast';
|
|
9
|
+
import { ast, isParsedLikeC4LangiumDocument } from '../ast';
|
|
4
10
|
import { safeCall } from '../utils';
|
|
11
|
+
import { stringHash } from '../utils/stringHash';
|
|
5
12
|
import { tryOrLog } from './_shared';
|
|
13
|
+
// Cache of relation match keys to avoid recomputing for every extend validation
|
|
14
|
+
let cachedRelationKeys = null;
|
|
15
|
+
let cachedDocsFingerprint = null;
|
|
16
|
+
const computeDocsFingerprint = (docs) => {
|
|
17
|
+
return docs
|
|
18
|
+
.map(doc => {
|
|
19
|
+
const relationHashes = (doc.c4Relations ?? [])
|
|
20
|
+
.map(rel => stringHash('extend-relation', FqnRef.flatten(rel.source), FqnRef.flatten(rel.target), rel.kind ?? 'default', rel.title ?? ''))
|
|
21
|
+
.sort()
|
|
22
|
+
.join(',');
|
|
23
|
+
return `${doc.uri.toString()}:${relationHashes}`;
|
|
24
|
+
})
|
|
25
|
+
.join('|');
|
|
26
|
+
};
|
|
6
27
|
export const relationChecks = (services) => {
|
|
7
28
|
const modelParser = services.likec4.ModelParser;
|
|
8
29
|
return tryOrLog((el, accept) => {
|
|
@@ -53,3 +74,68 @@ export const checkRelationBody = (_services) => {
|
|
|
53
74
|
}
|
|
54
75
|
});
|
|
55
76
|
};
|
|
77
|
+
export const extendRelationChecks = (services) => {
|
|
78
|
+
const modelParser = services.likec4.ModelParser;
|
|
79
|
+
return tryOrLog((el, accept) => {
|
|
80
|
+
const parser = modelParser.forDocument(AstUtils.getDocument(el));
|
|
81
|
+
const source = safeCall(() => parser.parseFqnRef(el.source));
|
|
82
|
+
if (!source) {
|
|
83
|
+
accept('error', 'Source not resolved', {
|
|
84
|
+
node: el,
|
|
85
|
+
property: 'source',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const target = safeCall(() => parser.parseFqnRef(el.target));
|
|
90
|
+
if (!target) {
|
|
91
|
+
accept('error', 'Target not resolved', {
|
|
92
|
+
node: el,
|
|
93
|
+
property: 'target',
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!FqnRef.isModelRef(source) && !FqnRef.isImportRef(source)) {
|
|
98
|
+
accept('error', 'Source must reference a model element', {
|
|
99
|
+
node: el,
|
|
100
|
+
property: 'source',
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!FqnRef.isModelRef(target) && !FqnRef.isImportRef(target)) {
|
|
105
|
+
accept('error', 'Target must reference a model element', {
|
|
106
|
+
node: el,
|
|
107
|
+
property: 'target',
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Warn if this extend does not match any relation in the workspace
|
|
112
|
+
// Build a match key identical to buildModel.ts
|
|
113
|
+
const kind = (el.kind ?? el.dotKind?.kind)?.ref?.name ?? 'default';
|
|
114
|
+
// Normalize title using the same parser helper
|
|
115
|
+
const { title = '' } = parser.parseBaseProps({}, { title: el.title });
|
|
116
|
+
const extendKey = stringHash('extend-relation', FqnRef.flatten(source), FqnRef.flatten(target), kind, title);
|
|
117
|
+
// Build (or reuse) a Set of all relation match keys across the workspace.
|
|
118
|
+
// This avoids O(E x D x R) scans on large workspaces.
|
|
119
|
+
const docs = services.shared.workspace.LangiumDocuments.all
|
|
120
|
+
.toArray()
|
|
121
|
+
.filter(isParsedLikeC4LangiumDocument);
|
|
122
|
+
const fingerprint = computeDocsFingerprint(docs);
|
|
123
|
+
if (fingerprint !== cachedDocsFingerprint) {
|
|
124
|
+
const keys = new Set();
|
|
125
|
+
for (const d of docs) {
|
|
126
|
+
for (const rel of d.c4Relations ?? []) {
|
|
127
|
+
const key = stringHash('extend-relation', FqnRef.flatten(rel.source), FqnRef.flatten(rel.target), rel.kind ?? 'default', rel.title ?? '');
|
|
128
|
+
keys.add(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
cachedRelationKeys = keys;
|
|
132
|
+
cachedDocsFingerprint = fingerprint;
|
|
133
|
+
}
|
|
134
|
+
const hasMatch = cachedRelationKeys?.has(extendKey) ?? false;
|
|
135
|
+
if (!hasMatch) {
|
|
136
|
+
accept('warning', 'This extend does not match any relation (by source, kind, target, title)', {
|
|
137
|
+
node: el,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ast } from '../ast';
|
|
2
|
+
import { tryOrLog } from './_shared';
|
|
3
|
+
// Helper to collect FqnExpr values from FqnExpressions linked list
|
|
4
|
+
function collectFqnExprs(exprs) {
|
|
5
|
+
const result = [];
|
|
6
|
+
let iter = exprs;
|
|
7
|
+
while (iter) {
|
|
8
|
+
if (iter.value) {
|
|
9
|
+
result.push(iter.value);
|
|
10
|
+
}
|
|
11
|
+
iter = iter.prev;
|
|
12
|
+
}
|
|
13
|
+
return result.reverse();
|
|
14
|
+
}
|
|
15
|
+
export const viewRuleRankChecks = (_services) => {
|
|
16
|
+
return tryOrLog((el, accept) => {
|
|
17
|
+
const targetExprs = collectFqnExprs(el.targets);
|
|
18
|
+
if (targetExprs.length < 2 && el.value === 'same') {
|
|
19
|
+
accept('warning', 'Rank rule should have at least 2 targets', {
|
|
20
|
+
node: el,
|
|
21
|
+
property: 'targets',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// Filter to only FqnRefExpr for parent comparison
|
|
25
|
+
const fqnRefExprs = targetExprs.filter(ast.isFqnRefExpr);
|
|
26
|
+
const firstParent = fqnRefExprs[0]?.ref?.parent;
|
|
27
|
+
for (let i = 1; i < fqnRefExprs.length; i++) {
|
|
28
|
+
const target = fqnRefExprs[i];
|
|
29
|
+
if (el.value === 'same' && !areSame(firstParent, target?.ref?.parent)) {
|
|
30
|
+
accept('error', 'All targets must have the same parent rank same', {
|
|
31
|
+
node: el,
|
|
32
|
+
property: 'targets',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
function areSame(a, b) {
|
|
39
|
+
if (!a && !b)
|
|
40
|
+
return true;
|
|
41
|
+
if (!a || !b)
|
|
42
|
+
return false;
|
|
43
|
+
if (a.value.ref !== b.value.ref)
|
|
44
|
+
return false;
|
|
45
|
+
return areSame(a.parent, b.parent);
|
|
46
|
+
}
|
|
@@ -14,10 +14,9 @@ function pack({ nodes, edges, ...rest }) {
|
|
|
14
14
|
b: [x, y, width, height],
|
|
15
15
|
c: isCompound,
|
|
16
16
|
})),
|
|
17
|
-
edges: mapValues(edges, ({ points, controlPoints, labelBBox,
|
|
17
|
+
edges: mapValues(edges, ({ points, controlPoints, labelBBox, ...e }) => ({
|
|
18
18
|
...!!controlPoints && { cp: controlPoints },
|
|
19
19
|
...!!labelBBox && { l: labelBBox },
|
|
20
|
-
...!!dotpos && { dp: dotpos },
|
|
21
20
|
...e,
|
|
22
21
|
p: points,
|
|
23
22
|
})),
|
|
@@ -36,10 +35,9 @@ function unpack({ nodes, edges, autoLayout, ...rest }) {
|
|
|
36
35
|
isCompound: c,
|
|
37
36
|
...n,
|
|
38
37
|
})),
|
|
39
|
-
edges: mapValues(edges, ({ p, cp, l,
|
|
38
|
+
edges: mapValues(edges, ({ p, cp, l, ...e }) => ({
|
|
40
39
|
...!!cp && { controlPoints: cp },
|
|
41
40
|
...!!l && { labelBBox: l },
|
|
42
|
-
...!!dp && { dotpos: dp },
|
|
43
41
|
...e,
|
|
44
42
|
points: p,
|
|
45
43
|
})),
|