@likec4/language-server 1.44.0 → 1.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LikeC4LanguageServices.d.ts +4 -15
- package/dist/LikeC4LanguageServices.js +4 -32
- package/dist/Rpc.js +44 -21
- package/dist/ast.d.ts +10 -0
- package/dist/ast.js +13 -2
- package/dist/browser.js +2 -2
- package/dist/bundled.js +2 -0
- package/dist/bundled.mjs +3838 -4059
- package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
- package/dist/filesystem/ChokidarWatcher.js +29 -18
- package/dist/filesystem/LikeC4FileSystem.js +4 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/generated/ast.d.ts +46 -9
- package/dist/generated/ast.js +56 -4
- package/dist/generated/grammar.js +1 -1
- package/dist/generated-lib/icons.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -3
- package/dist/lsp/DocumentSymbolProvider.js +12 -1
- package/dist/mcp/server/StdioLikeC4MCPServer.js +10 -6
- package/dist/mcp/server/StreamableLikeC4MCPServer.js +97 -97
- package/dist/mcp/server/WithMCPServer.js +4 -6
- package/dist/mcp/tools/read-deployment.js +18 -0
- package/dist/mcp/tools/read-element.js +24 -0
- package/dist/mcp/tools/search-element.js +5 -5
- package/dist/mcp/utils.js +1 -1
- package/dist/model/builder/buildModel.js +70 -1
- package/dist/model/deployments-index.js +2 -2
- package/dist/model/fqn-index.d.ts +1 -2
- package/dist/model/fqn-index.js +21 -18
- package/dist/model/model-builder.js +0 -2
- package/dist/model/model-parser.d.ts +3 -0
- package/dist/model/model-parser.js +41 -27
- package/dist/model/parser/Base.js +8 -3
- package/dist/model/parser/GlobalsParser.d.ts +1 -0
- package/dist/model/parser/ModelParser.d.ts +2 -1
- package/dist/model/parser/ModelParser.js +45 -1
- package/dist/model/parser/SpecificationParser.js +4 -0
- package/dist/model/parser/ViewsParser.d.ts +1 -0
- package/dist/model/parser/ViewsParser.js +16 -1
- package/dist/model-change/ModelChanges.d.ts +2 -2
- package/dist/model-change/ModelChanges.js +41 -11
- package/dist/protocol.d.ts +33 -10
- package/dist/protocol.js +13 -4
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +11 -1
- package/dist/validation/relation.d.ts +1 -0
- package/dist/validation/relation.js +87 -1
- package/dist/validation/view-checks.d.ts +4 -0
- package/dist/validation/view-checks.js +46 -0
- package/dist/view-utils/manual-layout.js +2 -4
- package/dist/views/LikeC4ManualLayouts.d.ts +16 -2
- package/dist/views/LikeC4ManualLayouts.js +100 -23
- package/dist/views/LikeC4Views.d.ts +26 -5
- package/dist/views/LikeC4Views.js +49 -33
- package/dist/workspace/AstNodeDescriptionProvider.js +6 -3
- package/dist/workspace/IndexManager.js +1 -1
- package/dist/workspace/LangiumDocuments.d.ts +3 -2
- package/dist/workspace/LangiumDocuments.js +29 -15
- package/dist/workspace/ProjectsManager.d.ts +45 -16
- package/dist/workspace/ProjectsManager.js +227 -45
- package/dist/workspace/WorkspaceManager.js +43 -0
- package/package.json +22 -21
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2023-2025 Denis Davydkov
|
|
4
|
+
// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
5
|
+
//
|
|
6
|
+
// Portions of this file have been modified by NVIDIA CORPORATION & AFFILIATES.
|
|
1
7
|
import { invariant } 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
|
|
45
|
-
- shape:<value
|
|
46
|
-
- meta:<key
|
|
47
|
-
- #<value
|
|
48
|
-
-
|
|
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
|
@@ -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
|
|
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 =
|
|
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
|
|
14
|
+
constructor(services: LikeC4Services);
|
|
16
15
|
private documents;
|
|
17
16
|
get(document: LikeC4LangiumDocument): DocumentFqnIndex;
|
|
18
17
|
resolve(reference: ast.Referenceable): Fqn;
|
package/dist/model/fqn-index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2023-2025 Denis Davydkov
|
|
4
|
+
// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
5
|
+
//
|
|
6
|
+
// Portions of this file have been modified by NVIDIA CORPORATION & AFFILIATES.
|
|
1
7
|
import { 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
|
|
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.
|
|
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
|
-
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
continue;
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
81
|
+
logger.trace(`create parser {projectId} document {doc}`, {
|
|
75
82
|
projectId: doc.likec4ProjectId,
|
|
76
|
-
doc:
|
|
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 (
|
|
115
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|