@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 } from '@likec4/core';
2
8
  import * as z from 'zod/v3';
3
9
  import { likec4Tool } from '../utils';
@@ -25,6 +31,7 @@ Output fields:
25
31
  - tags: string[] — Tags assigned to this entity
26
32
  - project: string — Project id
27
33
  - metadata: Record<string, string>
34
+ - links: Array<{ title: string|null, url: string, relative: string|null }> — external links associated with this deployment entity
28
35
  - shape: string — Rendered shape
29
36
  - color: string — Rendered color
30
37
  - children: string[] — Child deployment ids (empty for instances)
@@ -55,6 +62,7 @@ Example response (deployed instance):
55
62
  "tags": ["prod"],
56
63
  "project": "default",
57
64
  "metadata": {},
65
+ "links": [],
58
66
  "shape": "rectangle",
59
67
  "color": "#2F80ED",
60
68
  "children": [],
@@ -88,6 +96,11 @@ Example response (deployed instance):
88
96
  tags: z.array(z.string()),
89
97
  project: z.string(),
90
98
  metadata: z.record(z.union([z.string(), z.array(z.string())])),
99
+ links: z.array(z.object({
100
+ title: z.string().nullable().describe('Optional link title'),
101
+ url: z.string().describe('Link URL'),
102
+ relative: z.string().nullable().describe('Relative path (if URL is relative to workspace root)'),
103
+ })).describe('External links associated with this deployment entity'),
91
104
  shape: z.string(),
92
105
  color: z.string(),
93
106
  children: z.array(z.string()).describe('Children of this deployment node (Array of Deployment ids)'),
@@ -116,6 +129,11 @@ Example response (deployed instance):
116
129
  tags: [...element.tags],
117
130
  project: projectId,
118
131
  metadata: element.getMetadata(),
132
+ links: (element.links ?? []).map(link => ({
133
+ title: link.title ?? null,
134
+ url: link.url,
135
+ relative: link.relative ?? null,
136
+ })),
119
137
  shape: element.shape,
120
138
  color: element.color,
121
139
  children: element.isInstance() ? [] : [...element.children()].map(c => c.id),
@@ -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 } from '@likec4/core';
2
8
  import * as z from 'zod/v3';
3
9
  import { likec4Tool } from '../utils';
@@ -21,6 +27,7 @@ Response (JSON object):
21
27
  - tags: string[] — assigned tags
22
28
  - project: string — project id this element belongs to
23
29
  - metadata: Record<string, string> — element metadata
30
+ - links: Array<{ title: string|null, url: string, relative: string|null }> — external links associated with this element
24
31
  - shape: string — rendered shape
25
32
  - color: string — rendered color
26
33
  - children: string[] — ids (FQNs) of direct child elements
@@ -52,6 +59,13 @@ Example response:
52
59
  "tags": ["public"],
53
60
  "project": "default",
54
61
  "metadata": { "owner": "web" },
62
+ "links": [
63
+ {
64
+ "title": "Documentation",
65
+ "url": "https://docs.example.com/frontend",
66
+ "relative": null
67
+ }
68
+ ],
55
69
  "shape": "rounded-rectangle",
56
70
  "color": "#2F80ED",
57
71
  "children": ["shop.frontend.auth"],
@@ -103,6 +117,11 @@ Example response:
103
117
  tags: z.array(z.string()),
104
118
  project: z.string(),
105
119
  metadata: z.record(z.union([z.string(), z.array(z.string())])),
120
+ links: z.array(z.object({
121
+ title: z.string().nullable().describe('Optional link title'),
122
+ url: z.string().describe('Link URL'),
123
+ relative: z.string().nullable().describe('Relative path (if URL is relative to workspace root)'),
124
+ })).describe('External links associated with this element'),
106
125
  shape: z.string(),
107
126
  color: z.string(),
108
127
  children: z.array(z.string()).describe('Children of this element (Array of FQNs)'),
@@ -155,6 +174,11 @@ Example response:
155
174
  tags: [...element.tags],
156
175
  project: projectId,
157
176
  metadata: element.getMetadata(),
177
+ links: (element.links ?? []).map(link => ({
178
+ title: link.title ?? null,
179
+ url: link.url,
180
+ relative: link.relative ?? null,
181
+ })),
158
182
  shape: element.shape,
159
183
  color: element.color,
160
184
  children: [...element.children()].map(c => c.id),
@@ -41,11 +41,11 @@ export const searchElement = likec4Tool({
41
41
  Search LikeC4 elements and deployment nodes across all projects.
42
42
 
43
43
  Query syntax (case-insensitive):
44
- - kind:<value>: filters by kind
45
- - shape:<value>: filters by shape
46
- - meta:<key>: filters by having metadata with the given key
47
- - #<value>: matches assigned tags
48
- - Free text: matches id (FQN) or title
44
+ - kind:<value> filters by kind
45
+ - shape:<value> filters by shape
46
+ - meta:<key> filters by having metadata with the given key
47
+ - #<value> matches assigned tags
48
+ - <value> matches id (FQN) or title
49
49
 
50
50
  Request:
51
51
  - search: string — at least 2 characters
package/dist/mcp/utils.js CHANGED
@@ -39,7 +39,7 @@ function mkcallTool(name, languageServices, cb) {
39
39
  return {
40
40
  content: [{
41
41
  type: 'text',
42
- text: err instanceof Error ? err.message : loggable(err),
42
+ text: loggable(err),
43
43
  }],
44
44
  isError: true,
45
45
  };
@@ -1,11 +1,18 @@
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 { isDeploymentNode, isGlobalFqn, } from '@likec4/core';
2
8
  import { resolveRulesExtendedViews } from '@likec4/core/compute-view';
3
9
  import { computeColorValues } from '@likec4/core/styles';
4
10
  import { _stage, _type, exact, FqnRef, isExtendsElementView } from '@likec4/core/types';
5
11
  import { compareNatural, parentFqn, sortByFqnHierarchically, } from '@likec4/core/utils';
6
12
  import { UriUtils } from 'langium';
7
- import { filter, flatMap, forEach, indexBy, isDefined, isNullish, isTruthy, keys, map, mapValues, omitBy, pipe, prop, reduce, } from 'remeda';
13
+ import { filter, flatMap, forEach, hasAtLeast, indexBy, isDefined, isEmpty, isNullish, isTruthy, keys, map, mapValues, omitBy, pipe, prop, reduce, unique, } from 'remeda';
8
14
  import { logger } from '../../logger';
15
+ import { stringHash } from '../../utils/stringHash';
9
16
  import { MergedExtends } from './MergedExtends';
10
17
  import { MergedSpecification } from './MergedSpecification';
11
18
  /**
@@ -21,6 +28,9 @@ export function buildModelData(project, docs) {
21
28
  const metadataKeys = new Set();
22
29
  const elementExtends = new MergedExtends();
23
30
  const deploymentExtends = new MergedExtends();
31
+ // Collect all relation extends - we'll match them by comparing source/target/kind/title
32
+ const relationExtends = docs.flatMap(doc => doc.c4ExtendRelations);
33
+ const matchedExtendIds = new Set();
24
34
  const scanMetadataKeys = (obj) => {
25
35
  if (obj?.metadata) {
26
36
  keys(obj.metadata).forEach(key => metadataKeys.add(key));
@@ -54,6 +64,59 @@ export function buildModelData(project, docs) {
54
64
  return false;
55
65
  }
56
66
  return true;
67
+ }), map(rel => {
68
+ // Apply relation extends by matching source, target, kind, and title
69
+ // Generate a stable match key for this relation
70
+ const matchKey = stringHash('extend-relation', FqnRef.flatten(rel.source), FqnRef.flatten(rel.target), rel.kind ?? 'default', rel.title ?? '');
71
+ // Find all extends that match this relation
72
+ const relExtends = relationExtends.filter(ext => ext.id === matchKey);
73
+ if (relExtends.length === 0) {
74
+ return rel;
75
+ }
76
+ // Mark these extends as matched
77
+ relExtends.forEach(ext => matchedExtendIds.add(ext.astPath));
78
+ // Merge all extends for this relation
79
+ const tags = rel.tags ? [...rel.tags] : [];
80
+ const links = rel.links ? [...rel.links] : [];
81
+ let metadata = rel.metadata ? { ...rel.metadata } : {};
82
+ for (const ext of relExtends) {
83
+ if (ext.tags) {
84
+ tags.push(...ext.tags);
85
+ }
86
+ if (ext.links) {
87
+ // Ensure links are unique based on both URL and title
88
+ for (const incomingLink of ext.links) {
89
+ const isDuplicate = links.some(existingLink => existingLink.url === incomingLink.url && (existingLink.title || '') === (incomingLink.title || ''));
90
+ if (!isDuplicate) {
91
+ links.push(incomingLink);
92
+ }
93
+ }
94
+ }
95
+ if (ext.metadata) {
96
+ for (const [key, value] of Object.entries(ext.metadata)) {
97
+ const existingValue = metadata[key];
98
+ if (existingValue === undefined) {
99
+ metadata[key] = value;
100
+ }
101
+ else {
102
+ const existingArray = Array.isArray(existingValue) ? existingValue : [existingValue];
103
+ const incomingArray = Array.isArray(value) ? value : [value];
104
+ const merged = unique([...existingArray, ...incomingArray]);
105
+ metadata[key] = merged.length === 1 ? merged[0] : merged;
106
+ }
107
+ }
108
+ }
109
+ }
110
+ // Apply unique after accumulating all values
111
+ const uniqueTags = unique(tags);
112
+ // Links are already de-duplicated by URL and title in the loop above
113
+ const uniqueLinks = links;
114
+ return {
115
+ ...rel,
116
+ ...(hasAtLeast(uniqueTags, 1) && { tags: uniqueTags }),
117
+ ...(hasAtLeast(uniqueLinks, 1) && { links: uniqueLinks }),
118
+ ...(!isEmpty(metadata) && { metadata }),
119
+ };
57
120
  }), forEach(scanMetadataKeys), indexBy(prop('id')));
58
121
  const deploymentElements = pipe(docs, flatMap(d => {
59
122
  deploymentExtends.merge(d.c4ExtendDeployments);
@@ -139,6 +202,12 @@ export function buildModelData(project, docs) {
139
202
  if (parsedViews.some(isExtendsElementView)) {
140
203
  views = resolveRulesExtendedViews(views);
141
204
  }
205
+ // Warn about unmatched relation extends
206
+ for (const ext of relationExtends) {
207
+ if (!matchedExtendIds.has(ext.astPath)) {
208
+ logger.warn(`Relation extend at ${ext.astPath} does not match any relation in the model`);
209
+ }
210
+ }
142
211
  return {
143
212
  data: {
144
213
  [_stage]: 'parsed',
@@ -9,7 +9,7 @@ export class DeploymentsIndex extends FqnIndex {
9
9
  services;
10
10
  Names;
11
11
  constructor(services) {
12
- super(services, 'deployments-index');
12
+ super(services);
13
13
  this.services = services;
14
14
  this.Names = services.references.NameProvider;
15
15
  }
@@ -18,7 +18,7 @@ export class DeploymentsIndex extends FqnIndex {
18
18
  if (rootNodes.length === 0) {
19
19
  return DocumentFqnIndex.EMPTY;
20
20
  }
21
- const projectId = document.likec4ProjectId ??= this.projects.belongsTo(document);
21
+ const projectId = this.projects.belongsTo(document);
22
22
  const root = new Array();
23
23
  const children = new MultiMap();
24
24
  const descendants = new MultiMap();
@@ -7,12 +7,11 @@ import { ADisposable } from '../utils';
7
7
  import { type LangiumDocuments, ProjectsManager } from '../workspace';
8
8
  export declare class FqnIndex<AstNd = ast.Element> extends ADisposable {
9
9
  protected services: LikeC4Services;
10
- private cachePrefix;
11
10
  protected projects: ProjectsManager;
12
11
  protected langiumDocuments: LangiumDocuments;
13
12
  protected documentCache: DefaultWeakMap<LikeC4LangiumDocument, DocumentFqnIndex>;
14
13
  protected workspaceCache: WorkspaceCache<string, AstNodeDescriptionWithFqn[]>;
15
- constructor(services: LikeC4Services, cachePrefix?: string);
14
+ constructor(services: LikeC4Services);
16
15
  private documents;
17
16
  get(document: LikeC4LangiumDocument): DocumentFqnIndex;
18
17
  resolve(reference: ast.Referenceable): Fqn;
@@ -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, nonNullable } from '@likec4/core';
2
8
  import { Fqn } from '@likec4/core/types';
3
9
  import { ancestorsFqn, compareNatural, DefaultWeakMap, MultiMap, sortNaturalByFqn } from '@likec4/core/utils';
@@ -10,22 +16,20 @@ import { readStrictFqn } from '../utils/elementRef';
10
16
  import { ProjectsManager } from '../workspace';
11
17
  export class FqnIndex extends ADisposable {
12
18
  services;
13
- cachePrefix;
14
19
  projects;
15
20
  langiumDocuments;
16
21
  documentCache;
17
22
  workspaceCache;
18
- constructor(services, cachePrefix = 'fqn-index') {
23
+ constructor(services) {
19
24
  super();
20
25
  this.services = services;
21
- this.cachePrefix = cachePrefix;
22
26
  this.langiumDocuments = services.shared.workspace.LangiumDocuments;
23
27
  this.projects = services.shared.workspace.ProjectsManager;
24
28
  this.documentCache = new DefaultWeakMap(doc => this.createDocumentIndex(doc));
25
29
  this.workspaceCache = new WorkspaceCache(services.shared, DocumentState.IndexedContent);
26
30
  this.onDispose(services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.IndexedContent, (doc) => {
27
31
  if (isLikeC4LangiumDocument(doc)) {
28
- this.documentCache.set(doc, this.createDocumentIndex(doc));
32
+ this.documentCache.delete(doc);
29
33
  }
30
34
  }));
31
35
  }
@@ -57,23 +61,23 @@ export class FqnIndex extends ADisposable {
57
61
  // Document index is not yet created
58
62
  const doc = AstUtils.getDocument(el);
59
63
  invariant(isLikeC4LangiumDocument(doc));
60
- logWarnError(`Document ${doc.uri.path} is not indexed, but getFqn was called`);
61
- // This will create the document index
64
+ // Ensure the document is indexed
62
65
  this.get(doc);
66
+ // This will create the document index
63
67
  return nonNullable(ElementOps.readId(el), 'Element fqn must be set, invalid state');
64
68
  }
65
69
  byFqn(projectId, fqn) {
66
- return stream(this.workspaceCache.get(`${this.cachePrefix}:${projectId}:fqn:${fqn}`, () => {
70
+ return stream(this.workspaceCache.get(`${projectId}:fqn:${fqn}`, () => {
67
71
  return this
68
72
  .documents(projectId)
69
- .toArray()
70
73
  .flatMap(doc => {
71
74
  return this.get(doc).byFqn(fqn);
72
- });
75
+ })
76
+ .toArray();
73
77
  }));
74
78
  }
75
79
  rootElements(projectId) {
76
- return stream(this.workspaceCache.get(`${this.cachePrefix}:${projectId}:rootElements`, () => {
80
+ return stream(this.workspaceCache.get(`${projectId}:rootElements`, () => {
77
81
  const allchildren = this.documents(projectId)
78
82
  .reduce((map, doc) => {
79
83
  this.get(doc).rootElements().forEach(desc => {
@@ -85,7 +89,7 @@ export class FqnIndex extends ADisposable {
85
89
  }));
86
90
  }
87
91
  directChildrenOf(projectId, parent) {
88
- return stream(this.workspaceCache.get(`${this.cachePrefix}:${projectId}:directChildrenOf:${parent}`, () => {
92
+ return stream(this.workspaceCache.get(`${projectId}:directChildrenOf:${parent}`, () => {
89
93
  const allchildren = this.documents(projectId)
90
94
  .reduce((map, doc) => {
91
95
  this.get(doc).children(parent).forEach(desc => {
@@ -100,7 +104,7 @@ export class FqnIndex extends ADisposable {
100
104
  * Returns descedant elements with unique names in the scope
101
105
  */
102
106
  uniqueDescedants(projectId, parent) {
103
- return stream(this.workspaceCache.get(`${this.cachePrefix}:${projectId}:uniqueDescedants:${parent}`, () => {
107
+ return stream(this.workspaceCache.get(`${projectId}:uniqueDescedants:${parent}`, () => {
104
108
  const { children, descendants } = this.documents(projectId)
105
109
  .reduce((map, doc) => {
106
110
  const docIndex = this.get(doc);
@@ -129,18 +133,17 @@ export class FqnIndex extends ADisposable {
129
133
  if (rootElements.length === 0) {
130
134
  return DocumentFqnIndex.EMPTY;
131
135
  }
132
- const projectId = document.likec4ProjectId ??= this.projects.belongsTo(document);
136
+ const projectId = document.likec4ProjectId ?? this.projects.belongsTo(document);
133
137
  const root = new Array();
134
138
  const children = new MultiMap();
135
139
  const descendants = new MultiMap();
136
140
  const byfqn = new MultiMap();
137
141
  const Descriptions = this.services.workspace.AstNodeDescriptionProvider;
138
142
  const createAndSaveDescription = (node, name, fqn) => {
139
- const desc = {
140
- ...Descriptions.createDescription(node, name, document),
143
+ const desc = Object.assign(Descriptions.createDescription(node, name, document), {
141
144
  id: fqn,
142
145
  likec4ProjectId: projectId,
143
- };
146
+ });
144
147
  ElementOps.writeId(node, fqn);
145
148
  byfqn.set(fqn, desc);
146
149
  return desc;
@@ -163,7 +166,7 @@ export class FqnIndex extends ADisposable {
163
166
  let _nested = [];
164
167
  if (isDefined(el.body) && !isEmpty(el.body.elements)) {
165
168
  for (const child of el.body.elements) {
166
- if (!ast.isRelation(child)) {
169
+ if (!ast.isRelation(child) && !ast.isExtendRelation(child)) {
167
170
  try {
168
171
  _nested.push(...traverseNode(child, thisFqn));
169
172
  }
@@ -192,7 +195,7 @@ export class FqnIndex extends ADisposable {
192
195
  };
193
196
  for (const node of rootElements) {
194
197
  try {
195
- if (ast.isRelation(node)) {
198
+ if (ast.isRelation(node) || ast.isExtendRelation(node)) {
196
199
  continue;
197
200
  }
198
201
  traverseNode(node, null);
@@ -132,7 +132,6 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
132
132
  unsafeSyncComputeModel(projectId, manualLayouts) {
133
133
  const logger = builderLogger.getChild(projectId);
134
134
  const cache = this.cache;
135
- const viewsCache = this.cache;
136
135
  const hasManualLayouts = !!manualLayouts && !isEmpty(manualLayouts);
137
136
  const key = computedModelCacheKey(projectId) + (hasManualLayouts ? '+manualLayouts' : '');
138
137
  if (cache.has(key)) {
@@ -163,7 +162,6 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
163
162
  const previous = this.previousViews[key];
164
163
  const view = previous && eq(v, previous) ? previous : v;
165
164
  this.previousViews[key] = view;
166
- viewsCache.set(key, view);
167
165
  return [v.id, view];
168
166
  });
169
167
  const data = {
@@ -19,6 +19,7 @@ declare const DocumentParserFromMixins: {
19
19
  parseViewRuleGlobalPredicateRef(astRule: import("../generated/ast").ViewRuleGlobalPredicateRef | import("../generated/ast").DynamicViewGlobalPredicateRef): import("@likec4/core").ViewRuleGlobalPredicateRef;
20
20
  parseViewRuleStyleOrGlobalRef(astRule: import("../generated/ast").ViewRuleStyleOrGlobalRef): import("@likec4/core").ViewRuleGlobalStyle | import("@likec4/core").ElementViewRuleStyle<import("@likec4/core").Any>;
21
21
  parseViewRuleGroup(astNode: import("../generated/ast").ViewRuleGroup): import("@likec4/core").ElementViewRuleGroup;
22
+ parseViewRuleRank(astRule: import("../generated/ast").ViewRuleRank): import("@likec4/core").ElementViewRuleRank;
22
23
  parseViewRuleStyle(astRule: import("../generated/ast").ViewRuleStyle | import("../generated/ast").GlobalStyle): import("@likec4/core").ElementViewRuleStyle;
23
24
  parseViewRuleGlobalStyle(astRule: import("../generated/ast").ViewRuleGlobalStyle): import("@likec4/core").ViewRuleGlobalStyle;
24
25
  parseDynamicElementView(astNode: import("../generated/ast").DynamicView, additionalStyles: (import("@likec4/core").ViewRuleGlobalStyle | import("@likec4/core").ElementViewRuleStyle<import("@likec4/core").Any>)[]): import("../ast").ParsedAstDynamicView;
@@ -118,6 +119,7 @@ declare const DocumentParserFromMixins: {
118
119
  parseViewRuleGlobalPredicateRef(astRule: import("../generated/ast").ViewRuleGlobalPredicateRef | import("../generated/ast").DynamicViewGlobalPredicateRef): import("@likec4/core").ViewRuleGlobalPredicateRef;
119
120
  parseViewRuleStyleOrGlobalRef(astRule: import("../generated/ast").ViewRuleStyleOrGlobalRef): import("@likec4/core").ViewRuleGlobalStyle | import("@likec4/core").ElementViewRuleStyle<import("@likec4/core").Any>;
120
121
  parseViewRuleGroup(astNode: import("../generated/ast").ViewRuleGroup): import("@likec4/core").ElementViewRuleGroup;
122
+ parseViewRuleRank(astRule: import("../generated/ast").ViewRuleRank): import("@likec4/core").ElementViewRuleRank;
121
123
  parseViewRuleStyle(astRule: import("../generated/ast").ViewRuleStyle | import("../generated/ast").GlobalStyle): import("@likec4/core").ElementViewRuleStyle;
122
124
  parseViewRuleGlobalStyle(astRule: import("../generated/ast").ViewRuleGlobalStyle): import("@likec4/core").ViewRuleGlobalStyle;
123
125
  parseDynamicElementView(astNode: import("../generated/ast").DynamicView, additionalStyles: (import("@likec4/core").ViewRuleGlobalStyle | import("@likec4/core").ElementViewRuleStyle<import("@likec4/core").Any>)[]): import("../ast").ParsedAstDynamicView;
@@ -466,6 +468,7 @@ declare const DocumentParserFromMixins: {
466
468
  parseModel(): void;
467
469
  parseElement(astNode: import("../generated/ast").Element): import("../ast").ParsedAstElement;
468
470
  parseExtendElement(astNode: import("../generated/ast").ExtendElement): import("../ast").ParsedAstExtend | null;
471
+ parseExtendRelation(astNode: import("../generated/ast").ExtendRelation): import("../ast").ParsedAstExtendRelation | null;
469
472
  _resolveRelationSource(node: import("../generated/ast").Relation): import("@likec4/core").FqnRef.ModelRef | import("@likec4/core").FqnRef.ImportRef;
470
473
  parseRelation(astNode: import("../generated/ast").Relation): import("../ast").ParsedAstRelation;
471
474
  parseFqnRef(astNode: import("../generated/ast").FqnRef): import("@likec4/core").FqnRef;
@@ -1,11 +1,15 @@
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 { DefaultWeakMap, invariant, MultiMap } from '@likec4/core/utils';
2
- import { loggable } from '@likec4/log';
3
8
  import { DocumentState, UriUtils } from 'langium';
4
9
  import { pipe } from 'remeda';
5
10
  import { DiagnosticSeverity } from 'vscode-languageserver-types';
6
11
  import { isLikeC4LangiumDocument } from '../ast';
7
- import { isLikeC4Builtin } from '../likec4lib';
8
- import { logger as rootLogger } from '../logger';
12
+ import { logger as rootLogger, logWarnError } from '../logger';
9
13
  import { BaseParser } from './parser/Base';
10
14
  import { DeploymentModelParser } from './parser/DeploymentModelParser';
11
15
  import { DeploymentViewParser } from './parser/DeploymentViewParser';
@@ -25,24 +29,34 @@ export class LikeC4ModelParser {
25
29
  cachedParsers = new DefaultWeakMap((doc) => this.createParser(doc));
26
30
  constructor(services) {
27
31
  this.services = services;
28
- services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Linked, doc => {
29
- try {
32
+ services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Linked, async (doc) => {
33
+ if (this.cachedParsers.has(doc)) {
34
+ logger.trace('Linked: clear cached parser {projectId} document {doc}', {
35
+ projectId: doc.likec4ProjectId,
36
+ doc: UriUtils.basename(doc.uri),
37
+ });
38
+ this.cachedParsers.delete(doc);
39
+ }
40
+ });
41
+ services.shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Linked, async (docs) => {
42
+ for (const doc of docs) {
30
43
  if (services.shared.workspace.ProjectsManager.isExcluded(doc)) {
31
- return;
44
+ continue;
32
45
  }
33
- if (!isLikeC4Builtin(doc.uri)) {
34
- this.cachedParsers.set(doc, this.createParser(doc));
46
+ try {
47
+ // Force create parser for linked document (if not yet created)
48
+ this.parse(doc);
49
+ }
50
+ catch (error) {
51
+ logWarnError(error);
35
52
  }
36
- }
37
- catch (e) {
38
- logger.warn(loggable(e));
39
53
  }
40
54
  });
41
55
  // We need to clean up cached parser when document is validated and has errors
42
56
  // Because after that parser takes into account validation results
43
- services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Validated, doc => {
57
+ services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Validated, async (doc) => {
44
58
  if (doc.diagnostics?.some(d => d.severity === DiagnosticSeverity.Error) && this.cachedParsers.has(doc)) {
45
- logger.debug('clear cached parser {projectId} document {doc}', {
59
+ logger.trace('Validated: clear cached parser {projectId} document {doc} because of errors', {
46
60
  projectId: doc.likec4ProjectId,
47
61
  doc: UriUtils.basename(doc.uri),
48
62
  });
@@ -54,31 +68,30 @@ export class LikeC4ModelParser {
54
68
  return this.services.shared.workspace.LangiumDocuments.projectDocuments(projectId).map(d => this.parse(d));
55
69
  }
56
70
  parse(doc) {
57
- try {
58
- const parser = this.forDocument(doc);
59
- return parser.doc;
60
- }
61
- catch (cause) {
62
- throw new Error(`Error parsing document ${doc.uri.toString()}`, { cause });
63
- }
71
+ const parser = this.forDocument(doc);
72
+ return parser.doc;
64
73
  }
65
74
  forDocument(doc) {
66
- if (doc.state < DocumentState.Linked) {
67
- logger.warn(`Document {doc} is not linked`, { doc: doc.uri.toString() });
68
- }
69
75
  return this.cachedParsers.get(doc);
70
76
  }
71
77
  createParser(doc) {
72
78
  invariant(isLikeC4LangiumDocument(doc), `Document ${doc.uri.toString()} is not a LikeC4 document`);
79
+ const docbasename = UriUtils.basename(doc.uri);
73
80
  if (doc.likec4ProjectId) {
74
- logger.debug(`create parser {projectId} document {doc}`, {
81
+ logger.trace(`create parser {projectId} document {doc}`, {
75
82
  projectId: doc.likec4ProjectId,
76
- doc: UriUtils.basename(doc.uri),
83
+ doc: docbasename,
77
84
  });
78
85
  }
79
86
  else {
80
87
  logger.warn(`create parser for document without project {doc}`, { doc: doc.uri.fsPath });
81
88
  }
89
+ if (doc.state < DocumentState.Linked) {
90
+ logger.warn(`Document {doc} is not linked, state is {state}`, {
91
+ doc: docbasename,
92
+ state: doc.state,
93
+ });
94
+ }
82
95
  const props = {
83
96
  c4Specification: {
84
97
  tags: {},
@@ -90,6 +103,7 @@ export class LikeC4ModelParser {
90
103
  c4Elements: [],
91
104
  c4ExtendElements: [],
92
105
  c4ExtendDeployments: [],
106
+ c4ExtendRelations: [],
93
107
  c4Relations: [],
94
108
  c4Deployments: [],
95
109
  c4DeploymentRelations: [],
@@ -111,8 +125,8 @@ export class LikeC4ModelParser {
111
125
  parser.parseDeployment();
112
126
  parser.parseViews();
113
127
  }
114
- catch (e) {
115
- logger.error(`Error parsing document {doc}`, { doc: doc.uri.toString(), error: e });
128
+ catch (error) {
129
+ throw new Error(`Error parsing document ${doc.uri.fsPath}`, { cause: error });
116
130
  }
117
131
  return parser;
118
132
  }
@@ -264,9 +264,14 @@ export class BaseParser {
264
264
  if (isArray(elementProps)) {
265
265
  const style = this.parseStyleProps(elementProps.find(ast.isElementStyleProperty)?.props);
266
266
  // Property on element has higher priority than from style
267
- const iconProp = this.parseIconProperty(elementProps.find(ast.isIconProperty));
268
- if (iconProp) {
269
- style.icon = iconProp;
267
+ try {
268
+ const iconProp = this.parseIconProperty(elementProps.find(ast.isIconProperty));
269
+ if (iconProp) {
270
+ style.icon = iconProp;
271
+ }
272
+ }
273
+ catch (err) {
274
+ logger.warn('Failed to parse icon property on element', { err });
270
275
  }
271
276
  return style;
272
277
  }
@@ -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;