@likec4/language-server 1.45.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 (40) hide show
  1. package/dist/LikeC4LanguageServices.d.ts +3 -0
  2. package/dist/LikeC4LanguageServices.js +2 -0
  3. package/dist/Rpc.js +13 -2
  4. package/dist/ast.d.ts +10 -0
  5. package/dist/ast.js +7 -0
  6. package/dist/bundled.mjs +3819 -4062
  7. package/dist/filesystem/ChokidarWatcher.js +2 -2
  8. package/dist/filesystem/LikeC4FileSystem.js +5 -1
  9. package/dist/filesystem/index.d.ts +2 -0
  10. package/dist/generated/ast.d.ts +46 -9
  11. package/dist/generated/ast.js +56 -4
  12. package/dist/generated/grammar.js +1 -1
  13. package/dist/generated-lib/icons.js +1 -1
  14. package/dist/lsp/DocumentSymbolProvider.js +12 -1
  15. package/dist/mcp/server/WithMCPServer.js +0 -2
  16. package/dist/mcp/tools/read-deployment.js +18 -0
  17. package/dist/mcp/tools/read-element.js +24 -0
  18. package/dist/model/builder/buildModel.js +70 -1
  19. package/dist/model/fqn-index.js +8 -2
  20. package/dist/model/model-parser.d.ts +3 -0
  21. package/dist/model/model-parser.js +7 -0
  22. package/dist/model/parser/Base.js +8 -3
  23. package/dist/model/parser/GlobalsParser.d.ts +1 -0
  24. package/dist/model/parser/ModelParser.d.ts +2 -1
  25. package/dist/model/parser/ModelParser.js +45 -1
  26. package/dist/model/parser/ViewsParser.d.ts +1 -0
  27. package/dist/model/parser/ViewsParser.js +13 -0
  28. package/dist/model-change/ModelChanges.js +6 -3
  29. package/dist/validation/index.d.ts +1 -1
  30. package/dist/validation/index.js +11 -1
  31. package/dist/validation/relation.d.ts +1 -0
  32. package/dist/validation/relation.js +87 -1
  33. package/dist/validation/view-checks.d.ts +4 -0
  34. package/dist/validation/view-checks.js +46 -0
  35. package/dist/views/LikeC4ManualLayouts.js +2 -2
  36. package/dist/views/LikeC4Views.js +2 -2
  37. package/dist/workspace/ProjectsManager.d.ts +26 -1
  38. package/dist/workspace/ProjectsManager.js +98 -12
  39. package/dist/workspace/WorkspaceManager.js +38 -0
  40. package/package.json +17 -17
@@ -15,6 +15,7 @@ export declare function GlobalsParser<TBase extends WithViewsParser>(B: TBase):
15
15
  parseViewRuleGlobalPredicateRef(astRule: ast.ViewRuleGlobalPredicateRef | ast.DynamicViewGlobalPredicateRef): c4.ViewRuleGlobalPredicateRef;
16
16
  parseViewRuleStyleOrGlobalRef(astRule: ast.ViewRuleStyleOrGlobalRef): c4.ViewRuleGlobalStyle | c4.ElementViewRuleStyle<c4.aux.Any>;
17
17
  parseViewRuleGroup(astNode: ast.ViewRuleGroup): c4.ElementViewRuleGroup;
18
+ parseViewRuleRank(astRule: ast.ViewRuleRank): c4.ElementViewRuleRank;
18
19
  parseViewRuleStyle(astRule: ast.ViewRuleStyle | ast.GlobalStyle): c4.ElementViewRuleStyle;
19
20
  parseViewRuleGlobalStyle(astRule: ast.ViewRuleGlobalStyle): c4.ViewRuleGlobalStyle;
20
21
  parseDynamicElementView(astNode: ast.DynamicView, additionalStyles: (c4.ViewRuleGlobalStyle | c4.ElementViewRuleStyle<c4.aux.Any>)[]): import("../../ast").ParsedAstDynamicView;
@@ -1,6 +1,6 @@
1
1
  import type * as c4 from '@likec4/core';
2
2
  import { FqnRef } from '@likec4/core/types';
3
- import { type ParsedAstElement, type ParsedAstExtend, type ParsedAstRelation, ast } from '../../ast';
3
+ import { type ParsedAstElement, type ParsedAstExtend, type ParsedAstExtendRelation, type ParsedAstRelation, ast } from '../../ast';
4
4
  import type { WithExpressionV2 } from './FqnRefParser';
5
5
  export type WithModel = ReturnType<typeof ModelParser>;
6
6
  export declare function ModelParser<TBase extends WithExpressionV2>(B: TBase): {
@@ -8,6 +8,7 @@ export declare function ModelParser<TBase extends WithExpressionV2>(B: TBase): {
8
8
  parseModel(): void;
9
9
  parseElement(astNode: ast.Element): ParsedAstElement;
10
10
  parseExtendElement(astNode: ast.ExtendElement): ParsedAstExtend | null;
11
+ parseExtendRelation(astNode: ast.ExtendRelation): ParsedAstExtendRelation | null;
11
12
  _resolveRelationSource(node: ast.Relation): FqnRef.ModelRef | FqnRef.ImportRef;
12
13
  parseRelation(astNode: ast.Relation): ParsedAstRelation;
13
14
  parseFqnRef(astNode: ast.FqnRef): c4.FqnRef;
@@ -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);
@@ -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() {
@@ -128,6 +129,9 @@ export function ViewsParser(B) {
128
129
  if (ast.isViewRuleGroup(astRule)) {
129
130
  return this.parseViewRuleGroup(astRule);
130
131
  }
132
+ if (ast.isViewRuleRank(astRule)) {
133
+ return this.parseViewRuleRank(astRule);
134
+ }
131
135
  nonexhaustive(astRule);
132
136
  }
133
137
  parseViewRulePredicate(astNode) {
@@ -192,6 +196,15 @@ export function ViewsParser(B) {
192
196
  ...this.parseStyleProps(astNode.props),
193
197
  };
194
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
+ }
195
208
  parseViewRuleStyle(astRule) {
196
209
  const targets = this.parseFqnExpressions(astRule.targets).filter((e) => c4.ModelExpression.isFqnExpr(e));
197
210
  const style = this.parseStyleProps(astRule.props.filter(ast.isStyleProperty));
@@ -15,7 +15,6 @@ export class LikeC4ModelChanges {
15
15
  }
16
16
  async applyChange(changeView) {
17
17
  const lspConnection = this.services.shared.lsp.Connection;
18
- invariant(lspConnection, 'LSP Connection not available');
19
18
  let result = null;
20
19
  try {
21
20
  await this.services.shared.workspace.WorkspaceLock.write(async () => {
@@ -32,7 +31,7 @@ export class LikeC4ModelChanges {
32
31
  };
33
32
  // TODO refactor to use separate methods for save/reset operations
34
33
  if (change.op === 'save-view-snapshot') {
35
- 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);
36
35
  // If there is an existing manual layout v1
37
36
  if (lookup.view.manualLayout) {
38
37
  // We clean it up
@@ -62,6 +61,7 @@ export class LikeC4ModelChanges {
62
61
  };
63
62
  return;
64
63
  }
64
+ invariant(lspConnection, 'This change only supported in IDE (running as Extension)');
65
65
  const { edits, modifiedRange } = this.convertToTextEdit({
66
66
  lookup,
67
67
  change,
@@ -91,13 +91,16 @@ export class LikeC4ModelChanges {
91
91
  });
92
92
  }
93
93
  catch (err) {
94
- const error = loggable(wrapError(err, `Failed to apply change ${changeView.change.op} ${changeView.viewId}:\n`));
94
+ const error = loggable(wrapError(err, `Failed to apply change ${changeView.change.op} ${changeView.viewId}`));
95
95
  logger.error(error);
96
96
  result = {
97
97
  success: false,
98
98
  error,
99
99
  };
100
100
  }
101
+ finally {
102
+ this.services.likec4.ModelBuilder.clearCache();
103
+ }
101
104
  return result ?? {
102
105
  success: false,
103
106
  error: 'Unknown error applying model change',
@@ -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
+ }
@@ -75,9 +75,9 @@ export class DefaultLikeC4ManualLayouts {
75
75
  const { manualLayout: _, ...rest } = layouted;
76
76
  layouted = rest;
77
77
  }
78
+ const content = JSON5.stringify(
78
79
  // Normalize icon paths before writing
79
- layouted = this.normalizeIconPathsForWrite(layouted, project.folderUri);
80
- const content = JSON5.stringify(layouted, {
80
+ this.normalizeIconPathsForWrite(layouted, project.folderUri), {
81
81
  space: 2,
82
82
  quote: '\'',
83
83
  });
@@ -1,8 +1,8 @@
1
- import { _layout, applyManualLayout, calcDriftsFromSnapshot, invariant } from '@likec4/core';
1
+ import { _layout, applyManualLayout, calcDriftsFromSnapshot } from '@likec4/core';
2
2
  import { GraphvizLayouter } from '@likec4/layouts';
3
3
  import { loggable } from '@likec4/log';
4
4
  import { interruptAndCheck } from 'langium';
5
- import { isTruthy, unique, values } from 'remeda';
5
+ import { isTruthy, values } from 'remeda';
6
6
  import { logger as rootLogger, logWarnError } from '../logger';
7
7
  import { performanceMark } from '../utils';
8
8
  const viewsLogger = rootLogger.getChild('views');
@@ -1,4 +1,4 @@
1
- import { type LikeC4ProjectConfig, type LikeC4ProjectConfigInput } from '@likec4/config';
1
+ import { type IncludeConfig, type LikeC4ProjectConfig, type LikeC4ProjectConfigInput } from '@likec4/config';
2
2
  import type { NonEmptyReadonlyArray } from '@likec4/core';
3
3
  import type { ProjectId, scalar } from '@likec4/core/types';
4
4
  import { type Cancellation, type LangiumDocument, URI, WorkspaceCache } from 'langium';
@@ -16,11 +16,27 @@ interface ProjectData {
16
16
  folder: ProjectFolder;
17
17
  folderUri: URI;
18
18
  exclude?: (path: string) => boolean;
19
+ /**
20
+ * Resolved include paths with both URI and folder string representations.
21
+ * These are additional directories that are part of this project.
22
+ */
23
+ includePaths?: Array<{
24
+ uri: URI;
25
+ folder: ProjectFolder;
26
+ }>;
27
+ /**
28
+ * Normalized include configuration (paths, maxDepth, fileThreshold).
29
+ */
30
+ includeConfig: IncludeConfig;
19
31
  }
20
32
  export interface Project {
21
33
  id: scalar.ProjectId;
22
34
  folderUri: URI;
23
35
  config: LikeC4ProjectConfig;
36
+ /**
37
+ * Resolved include paths as URIs (if configured).
38
+ */
39
+ includePaths?: URI[];
24
40
  }
25
41
  export declare class ProjectsManager {
26
42
  #private;
@@ -93,6 +109,15 @@ export declare class ProjectsManager {
93
109
  * Lazy-created due to initialization order of the LanguageServer
94
110
  */
95
111
  protected get documentBelongsTo(): WorkspaceCache<LangiumDocument, ProjectData>;
112
+ /**
113
+ * Returns all include paths from all projects.
114
+ * Used by WorkspaceManager to scan additional directories for C4 files.
115
+ */
116
+ getAllIncludePaths(): Array<{
117
+ projectId: scalar.ProjectId;
118
+ includePath: URI;
119
+ includeConfig: IncludeConfig;
120
+ }>;
96
121
  private getWorkspaceFolder;
97
122
  }
98
123
  export {};