@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.
Files changed (63) hide show
  1. package/dist/LikeC4LanguageServices.d.ts +4 -15
  2. package/dist/LikeC4LanguageServices.js +4 -32
  3. package/dist/Rpc.js +44 -21
  4. package/dist/ast.d.ts +10 -0
  5. package/dist/ast.js +13 -2
  6. package/dist/browser.js +2 -2
  7. package/dist/bundled.js +2 -0
  8. package/dist/bundled.mjs +3838 -4059
  9. package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
  10. package/dist/filesystem/ChokidarWatcher.js +29 -18
  11. package/dist/filesystem/LikeC4FileSystem.js +4 -0
  12. package/dist/filesystem/index.d.ts +2 -0
  13. package/dist/generated/ast.d.ts +46 -9
  14. package/dist/generated/ast.js +56 -4
  15. package/dist/generated/grammar.js +1 -1
  16. package/dist/generated-lib/icons.js +1 -1
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +5 -3
  19. package/dist/lsp/DocumentSymbolProvider.js +12 -1
  20. package/dist/mcp/server/StdioLikeC4MCPServer.js +10 -6
  21. package/dist/mcp/server/StreamableLikeC4MCPServer.js +97 -97
  22. package/dist/mcp/server/WithMCPServer.js +4 -6
  23. package/dist/mcp/tools/read-deployment.js +18 -0
  24. package/dist/mcp/tools/read-element.js +24 -0
  25. package/dist/mcp/tools/search-element.js +5 -5
  26. package/dist/mcp/utils.js +1 -1
  27. package/dist/model/builder/buildModel.js +70 -1
  28. package/dist/model/deployments-index.js +2 -2
  29. package/dist/model/fqn-index.d.ts +1 -2
  30. package/dist/model/fqn-index.js +21 -18
  31. package/dist/model/model-builder.js +0 -2
  32. package/dist/model/model-parser.d.ts +3 -0
  33. package/dist/model/model-parser.js +41 -27
  34. package/dist/model/parser/Base.js +8 -3
  35. package/dist/model/parser/GlobalsParser.d.ts +1 -0
  36. package/dist/model/parser/ModelParser.d.ts +2 -1
  37. package/dist/model/parser/ModelParser.js +45 -1
  38. package/dist/model/parser/SpecificationParser.js +4 -0
  39. package/dist/model/parser/ViewsParser.d.ts +1 -0
  40. package/dist/model/parser/ViewsParser.js +16 -1
  41. package/dist/model-change/ModelChanges.d.ts +2 -2
  42. package/dist/model-change/ModelChanges.js +41 -11
  43. package/dist/protocol.d.ts +33 -10
  44. package/dist/protocol.js +13 -4
  45. package/dist/validation/index.d.ts +1 -1
  46. package/dist/validation/index.js +11 -1
  47. package/dist/validation/relation.d.ts +1 -0
  48. package/dist/validation/relation.js +87 -1
  49. package/dist/validation/view-checks.d.ts +4 -0
  50. package/dist/validation/view-checks.js +46 -0
  51. package/dist/view-utils/manual-layout.js +2 -4
  52. package/dist/views/LikeC4ManualLayouts.d.ts +16 -2
  53. package/dist/views/LikeC4ManualLayouts.js +100 -23
  54. package/dist/views/LikeC4Views.d.ts +26 -5
  55. package/dist/views/LikeC4Views.js +49 -33
  56. package/dist/workspace/AstNodeDescriptionProvider.js +6 -3
  57. package/dist/workspace/IndexManager.js +1 -1
  58. package/dist/workspace/LangiumDocuments.d.ts +3 -2
  59. package/dist/workspace/LangiumDocuments.js +29 -15
  60. package/dist/workspace/ProjectsManager.d.ts +45 -16
  61. package/dist/workspace/ProjectsManager.js +227 -45
  62. package/dist/workspace/WorkspaceManager.js +43 -0
  63. 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
- if (el.body && el.body.elements && el.body.elements.length > 0) {
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
- logger.warn('viewOf is not resolved: ' + astNode.$cstNode?.text);
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 { Location, Range, TextEdit } from 'vscode-languageserver-types';
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<Location | null>;
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 { Location, Range, TextEdit } from 'vscode-languageserver-types';
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(`LikeC4ModelChanges: view not found: ${viewId}`);
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
- result = await this.services.likec4.ManualLayouts.write(project, change.layout);
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
- result = await this.services.likec4.ManualLayouts.remove(project, viewId);
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
- uri: textDocument.uri,
70
- range: modifiedRange,
85
+ success: true,
86
+ location: {
87
+ uri: textDocument.uri,
88
+ range: modifiedRange,
89
+ },
71
90
  };
72
91
  });
73
92
  }
74
- catch (error) {
75
- logger.error(`Failed to apply change ${changeView.change.op} ${changeView.viewId}`, { error });
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) {
@@ -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 Req: RequestType<Params, Res, void>;
106
- type Req = typeof Req;
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: LikeC4ProjectJsonConfig;
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 Req: RequestType<Params, void, void>;
156
- type Req = typeof Req;
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 Req: RequestType<Params, Res, void>;
204
- type Req = typeof Req;
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 = Location | null;
217
- const Req: RequestType<Params, Res, void>;
218
- type Req = typeof Req;
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.Req = new RequestType('likec4/validate-layout');
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.Req = new RequestType('likec4/build');
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.Req = new RequestType('likec4/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.Req = new RequestType('likec4/change-view');
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;
@@ -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,4 @@
1
+ import type { ValidationCheck } from 'langium';
2
+ import { ast } from '../ast';
3
+ import type { LikeC4Services } from '../module';
4
+ export declare const viewRuleRankChecks: (_services: LikeC4Services) => ValidationCheck<ast.ViewRuleRank>;
@@ -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, dotpos, ...e }) => ({
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, dp, ...e }) => ({
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
  })),