@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,4 +1,5 @@
|
|
|
1
|
-
import type { LayoutedView, ViewId } from '@likec4/core';
|
|
1
|
+
import type { LayoutedView, ProjectId, ViewId } from '@likec4/core';
|
|
2
|
+
import { URI, WorkspaceCache } from 'langium';
|
|
2
3
|
import { type Location } from 'vscode-languageserver-types';
|
|
3
4
|
import type { LikeC4Services } from '../module';
|
|
4
5
|
import type { Project } from '../workspace/ProjectsManager';
|
|
@@ -19,10 +20,23 @@ export interface LikeC4ManualLayoutsModuleContext {
|
|
|
19
20
|
export declare const WithLikeC4ManualLayouts: LikeC4ManualLayoutsModuleContext;
|
|
20
21
|
export declare class DefaultLikeC4ManualLayouts implements LikeC4ManualLayouts {
|
|
21
22
|
private services;
|
|
22
|
-
|
|
23
|
+
protected cache: WorkspaceCache<ProjectId, Promise<Record<ViewId, LayoutedView> | null>>;
|
|
23
24
|
constructor(services: LikeC4Services);
|
|
24
25
|
read(project: Project): Promise<Record<ViewId, LayoutedView> | null>;
|
|
25
26
|
write(project: Project, layouted: LayoutedView): Promise<Location>;
|
|
26
27
|
remove(project: Project, view: ViewId): Promise<Location | null>;
|
|
27
28
|
clearCaches(): void;
|
|
29
|
+
/**
|
|
30
|
+
* When we save snapshot - it may contain fullpath to icons on the machine it was created,
|
|
31
|
+
* that is wrong when opened on another.
|
|
32
|
+
*
|
|
33
|
+
* Prepares a snapshot for writing by converting absolute icon paths to relative paths.
|
|
34
|
+
* Absolute paths starting with 'file://' are converted to relative paths prefixed with 'file://./'
|
|
35
|
+
*/
|
|
36
|
+
protected normalizeIconPathsForWrite(layouted: LayoutedView, projectUri: URI): LayoutedView;
|
|
37
|
+
/**
|
|
38
|
+
* Postprocesses a snapshot after reading by converting relative icon paths back to absolute paths.
|
|
39
|
+
* Relative paths prefixed with 'file://./' are converted to absolute paths based on project folder.
|
|
40
|
+
*/
|
|
41
|
+
protected resolveIconPathsAfterRead(layouted: LayoutedView, projectUri: URI): LayoutedView;
|
|
28
42
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { getOrCreate } from '@likec4/core/utils';
|
|
2
1
|
import JSON5 from 'json5';
|
|
3
|
-
import { UriUtils } from 'langium';
|
|
2
|
+
import { DocumentState, URI, UriUtils, WorkspaceCache } from 'langium';
|
|
4
3
|
import { indexBy, prop } from 'remeda';
|
|
5
4
|
import { Position, Range, } from 'vscode-languageserver-types';
|
|
6
5
|
import { logger as rootLogger } from '../logger';
|
|
7
|
-
const
|
|
6
|
+
const layoutsLogger = rootLogger.getChild('manual-layouts');
|
|
8
7
|
/**
|
|
9
8
|
* @todo sync with vscode extension watchers
|
|
10
9
|
* (search for ".likec4.snap" references)
|
|
@@ -19,14 +18,17 @@ function getManualLayoutsOutDir(project) {
|
|
|
19
18
|
export const WithLikeC4ManualLayouts = {
|
|
20
19
|
manualLayouts: (services) => new DefaultLikeC4ManualLayouts(services),
|
|
21
20
|
};
|
|
21
|
+
const RELATIVE_PATH_PREFIX = 'file://./';
|
|
22
22
|
export class DefaultLikeC4ManualLayouts {
|
|
23
23
|
services;
|
|
24
|
-
|
|
24
|
+
cache;
|
|
25
25
|
constructor(services) {
|
|
26
26
|
this.services = services;
|
|
27
|
+
this.cache = new WorkspaceCache(services.shared, DocumentState.Validated);
|
|
27
28
|
}
|
|
28
29
|
async read(project) {
|
|
29
|
-
return await
|
|
30
|
+
return await this.cache.get(project.id, async () => {
|
|
31
|
+
const logger = layoutsLogger.getChild(project.id);
|
|
30
32
|
const fs = this.services.shared.workspace.FileSystemProvider;
|
|
31
33
|
const outDir = getManualLayoutsOutDir(project);
|
|
32
34
|
const manualLayouts = [];
|
|
@@ -39,8 +41,10 @@ export class DefaultLikeC4ManualLayouts {
|
|
|
39
41
|
if (file.isFile) {
|
|
40
42
|
try {
|
|
41
43
|
const content = await fs.readFile(file.uri);
|
|
44
|
+
const parsed = JSON5.parse(content);
|
|
45
|
+
const resolved = this.resolveIconPathsAfterRead(parsed, project.folderUri);
|
|
42
46
|
manualLayouts.push({
|
|
43
|
-
...
|
|
47
|
+
...resolved,
|
|
44
48
|
_layout: 'manual',
|
|
45
49
|
});
|
|
46
50
|
}
|
|
@@ -63,6 +67,7 @@ export class DefaultLikeC4ManualLayouts {
|
|
|
63
67
|
});
|
|
64
68
|
}
|
|
65
69
|
async write(project, layouted) {
|
|
70
|
+
const logger = layoutsLogger.getChild(project.id);
|
|
66
71
|
const outDir = getManualLayoutsOutDir(project);
|
|
67
72
|
const file = UriUtils.joinPath(outDir, fileName(layouted.id));
|
|
68
73
|
// Ensure the manualLayout field is omitted (may exist in migration)
|
|
@@ -70,7 +75,9 @@ export class DefaultLikeC4ManualLayouts {
|
|
|
70
75
|
const { manualLayout: _, ...rest } = layouted;
|
|
71
76
|
layouted = rest;
|
|
72
77
|
}
|
|
73
|
-
const content = JSON5.stringify(
|
|
78
|
+
const content = JSON5.stringify(
|
|
79
|
+
// Normalize icon paths before writing
|
|
80
|
+
this.normalizeIconPathsForWrite(layouted, project.folderUri), {
|
|
74
81
|
space: 2,
|
|
75
82
|
quote: '\'',
|
|
76
83
|
});
|
|
@@ -81,23 +88,29 @@ export class DefaultLikeC4ManualLayouts {
|
|
|
81
88
|
logger.debug `write snapshot of ${layouted.id} in project ${project.id} to ${file.fsPath}`;
|
|
82
89
|
const fs = this.services.shared.workspace.FileSystemProvider;
|
|
83
90
|
try {
|
|
84
|
-
await fs.writeFile(file, content);
|
|
91
|
+
await fs.writeFile(file, content + '\n');
|
|
85
92
|
}
|
|
86
93
|
catch (err) {
|
|
87
94
|
logger.warn(`Failed to write snapshot ${layouted.id} to ${file.fsPath}`, { err });
|
|
88
95
|
}
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
projectCaches
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
const projectCachesPromise = this.cache.get(project.id);
|
|
97
|
+
if (projectCachesPromise) {
|
|
98
|
+
const projectCaches = await projectCachesPromise;
|
|
99
|
+
if (projectCaches) {
|
|
100
|
+
logger.debug `update snapshot cache of ${layouted.id} in project ${project.id}`;
|
|
101
|
+
projectCaches[layouted.id] = layouted;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
logger.debug `clean cache of project ${project.id}`;
|
|
105
|
+
// Cache was null, remove it entirely
|
|
106
|
+
this.cache.delete(project.id);
|
|
107
|
+
}
|
|
96
108
|
}
|
|
97
109
|
this.services.likec4.ModelBuilder.clearCache();
|
|
98
110
|
return location;
|
|
99
111
|
}
|
|
100
112
|
async remove(project, view) {
|
|
113
|
+
const logger = layoutsLogger.getChild(project.id);
|
|
101
114
|
const outDir = getManualLayoutsOutDir(project);
|
|
102
115
|
const file = UriUtils.joinPath(outDir, fileName(view));
|
|
103
116
|
logger.debug `delete snapshot of ${view} in project ${project.id}. File: ${file.fsPath}`;
|
|
@@ -115,18 +128,82 @@ export class DefaultLikeC4ManualLayouts {
|
|
|
115
128
|
catch (err) {
|
|
116
129
|
logger.warn(`Failed to delete snapshot ${view} from ${file.fsPath}`, { err });
|
|
117
130
|
}
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
const projectCachesPromise = this.cache.get(project.id);
|
|
132
|
+
if (projectCachesPromise) {
|
|
133
|
+
const projectCaches = await projectCachesPromise;
|
|
134
|
+
if (projectCaches) {
|
|
135
|
+
logger.debug `clean cached view ${view} in project ${project.id}`;
|
|
136
|
+
delete projectCaches[view];
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
logger.debug `reset empty cache of project ${project.id}`;
|
|
140
|
+
// Cache was null, remove it entirely
|
|
141
|
+
this.cache.delete(project.id);
|
|
142
|
+
}
|
|
125
143
|
}
|
|
126
144
|
this.services.likec4.ModelBuilder.clearCache();
|
|
127
145
|
return location;
|
|
128
146
|
}
|
|
129
147
|
clearCaches() {
|
|
130
|
-
this.
|
|
148
|
+
this.cache.clear();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* When we save snapshot - it may contain fullpath to icons on the machine it was created,
|
|
152
|
+
* that is wrong when opened on another.
|
|
153
|
+
*
|
|
154
|
+
* Prepares a snapshot for writing by converting absolute icon paths to relative paths.
|
|
155
|
+
* Absolute paths starting with 'file://' are converted to relative paths prefixed with 'file://./'
|
|
156
|
+
*/
|
|
157
|
+
normalizeIconPathsForWrite(layouted, projectUri) {
|
|
158
|
+
const nodes = layouted.nodes.map((node) => {
|
|
159
|
+
if (!node.icon || typeof node.icon !== 'string') {
|
|
160
|
+
return node;
|
|
161
|
+
}
|
|
162
|
+
// Check if icon is an absolute file path
|
|
163
|
+
if (node.icon.startsWith('file://')) {
|
|
164
|
+
const iconUri = URI.parse(node.icon);
|
|
165
|
+
// Get relative path from project folder to icon
|
|
166
|
+
const relativePath = UriUtils.relative(projectUri, iconUri);
|
|
167
|
+
// If icon is outside of project folder - leave it as is,
|
|
168
|
+
// to avoid security issues on reading snapshots on another machine
|
|
169
|
+
if (relativePath.startsWith('..')) {
|
|
170
|
+
return node;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...node,
|
|
174
|
+
icon: `${RELATIVE_PATH_PREFIX}${relativePath}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return node;
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
...layouted,
|
|
181
|
+
nodes: nodes,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Postprocesses a snapshot after reading by converting relative icon paths back to absolute paths.
|
|
186
|
+
* Relative paths prefixed with 'file://./' are converted to absolute paths based on project folder.
|
|
187
|
+
*/
|
|
188
|
+
resolveIconPathsAfterRead(layouted, projectUri) {
|
|
189
|
+
const nodes = layouted.nodes.map((node) => {
|
|
190
|
+
if (!node.icon || typeof node.icon !== 'string') {
|
|
191
|
+
return node;
|
|
192
|
+
}
|
|
193
|
+
// Check if icon is a relative file path
|
|
194
|
+
if (node.icon.startsWith(RELATIVE_PATH_PREFIX)) {
|
|
195
|
+
const relativePath = node.icon.substring(RELATIVE_PATH_PREFIX.length);
|
|
196
|
+
const absoluteUri = UriUtils.joinPath(projectUri, relativePath);
|
|
197
|
+
return {
|
|
198
|
+
...node,
|
|
199
|
+
icon: absoluteUri.toString(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return node;
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
...layouted,
|
|
206
|
+
nodes: nodes,
|
|
207
|
+
};
|
|
131
208
|
}
|
|
132
209
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComputedView, DiagramView, LayoutedView, ProjectId, ViewId } from '@likec4/core';
|
|
1
|
+
import type { ComputedView, DiagramView, LayoutedView, LayoutType, ProjectId, ViewId } from '@likec4/core';
|
|
2
2
|
import { type QueueGraphvizLayoter, GraphvizLayouter } from '@likec4/layouts';
|
|
3
3
|
import type { CancellationToken } from 'vscode-languageserver';
|
|
4
4
|
import type { LikeC4Services } from '../module';
|
|
@@ -11,6 +11,18 @@ type GraphvizSvgOut = {
|
|
|
11
11
|
readonly dot: string;
|
|
12
12
|
readonly svg: string;
|
|
13
13
|
};
|
|
14
|
+
type LayoutViewParams = {
|
|
15
|
+
viewId: ViewId;
|
|
16
|
+
/**
|
|
17
|
+
* Type of layout to apply
|
|
18
|
+
* - 'manual' - applies manual layout if any
|
|
19
|
+
* - 'auto' - returns latest version with drifts from manual layout if any
|
|
20
|
+
* - undefined - returns latest layout as is
|
|
21
|
+
*/
|
|
22
|
+
layoutType?: LayoutType | undefined;
|
|
23
|
+
projectId?: ProjectId | undefined;
|
|
24
|
+
cancelToken?: CancellationToken | undefined;
|
|
25
|
+
};
|
|
14
26
|
export interface LikeC4Views {
|
|
15
27
|
readonly layouter: GraphvizLayouter;
|
|
16
28
|
/**
|
|
@@ -18,14 +30,18 @@ export interface LikeC4Views {
|
|
|
18
30
|
*/
|
|
19
31
|
computedViews(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<ComputedView[]>;
|
|
20
32
|
/**
|
|
21
|
-
*
|
|
33
|
+
* Layouts all views (ignoring any manual snapshots)
|
|
22
34
|
*/
|
|
23
35
|
layoutAllViews(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<GraphvizOut[]>;
|
|
24
36
|
/**
|
|
25
|
-
* Layouts a view
|
|
37
|
+
* Layouts a view.
|
|
38
|
+
* If layoutType is 'manual' - applies manual layout if any.
|
|
39
|
+
* If layoutType is 'auto' - returns latest version with drifts from manual layout if any
|
|
40
|
+
* If not specified - returns latest layout as is
|
|
41
|
+
*
|
|
26
42
|
* If view not found in model, but there is a snapshot - it will be returned (with empty DOT)
|
|
27
43
|
*/
|
|
28
|
-
layoutView(
|
|
44
|
+
layoutView(params: LayoutViewParams): Promise<GraphvizOut | null>;
|
|
29
45
|
/**
|
|
30
46
|
* Returns diagrams.
|
|
31
47
|
* If diagram has manual layout, it will be used.
|
|
@@ -55,7 +71,7 @@ export declare class DefaultLikeC4Views implements LikeC4Views {
|
|
|
55
71
|
computedViews(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<ComputedView[]>;
|
|
56
72
|
private _layoutAllViews;
|
|
57
73
|
layoutAllViews(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<GraphvizOut[]>;
|
|
58
|
-
layoutView(viewId
|
|
74
|
+
layoutView({ viewId, layoutType, projectId, cancelToken, }: LayoutViewParams): Promise<GraphvizOut | null>;
|
|
59
75
|
diagrams(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<Array<LayoutedView>>;
|
|
60
76
|
viewsAsGraphvizOut(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<Array<GraphvizSvgOut>>;
|
|
61
77
|
/**
|
|
@@ -63,6 +79,11 @@ export declare class DefaultLikeC4Views implements LikeC4Views {
|
|
|
63
79
|
*/
|
|
64
80
|
openView(viewId: ViewId, projectId: ProjectId): Promise<void>;
|
|
65
81
|
private reportViewError;
|
|
82
|
+
/**
|
|
83
|
+
* Applies manual layout or calculates drifts from snapshot
|
|
84
|
+
* if layoutType is specified
|
|
85
|
+
*/
|
|
86
|
+
private withLayoutType;
|
|
66
87
|
private viewSucceed;
|
|
67
88
|
}
|
|
68
89
|
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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');
|
|
@@ -78,47 +78,48 @@ export class DefaultLikeC4Views {
|
|
|
78
78
|
const likeC4Model = await this.ModelBuilder.computeModel(projectId, cancelToken);
|
|
79
79
|
return await this._layoutAllViews(likeC4Model, cancelToken);
|
|
80
80
|
}
|
|
81
|
-
async layoutView(viewId, projectId, cancelToken) {
|
|
81
|
+
async layoutView({ viewId, layoutType, projectId, cancelToken, }) {
|
|
82
82
|
const model = await this.ModelBuilder.computeModel(projectId, cancelToken);
|
|
83
83
|
const view = model.findView(viewId)?.$view;
|
|
84
84
|
projectId = model.project.id;
|
|
85
85
|
const logger = viewsLogger.getChild(projectId);
|
|
86
86
|
if (!view) {
|
|
87
87
|
logger.warn `layoutView ${viewId} not found`;
|
|
88
|
-
const
|
|
89
|
-
const manualLayouts = await this.services.likec4.ManualLayouts.read(project);
|
|
90
|
-
const snapshot = manualLayouts?.[viewId];
|
|
88
|
+
const snapshot = model.findManualLayout(viewId);
|
|
91
89
|
if (snapshot) {
|
|
92
90
|
logger.debug `found manual layout for ${viewId}`;
|
|
91
|
+
let diagram = { ...snapshot };
|
|
92
|
+
diagram.drifts = [
|
|
93
|
+
'not-exists',
|
|
94
|
+
];
|
|
95
|
+
diagram._layout = 'manual';
|
|
93
96
|
return {
|
|
94
|
-
diagram:
|
|
95
|
-
...snapshot,
|
|
96
|
-
_layout: 'manual',
|
|
97
|
-
drifts: snapshot.drifts
|
|
98
|
-
? unique([
|
|
99
|
-
...snapshot.drifts,
|
|
100
|
-
'not-exists',
|
|
101
|
-
])
|
|
102
|
-
: ['not-exists'],
|
|
103
|
-
},
|
|
97
|
+
diagram: diagram,
|
|
104
98
|
dot: '# manual layout',
|
|
105
99
|
};
|
|
106
100
|
}
|
|
107
101
|
return null;
|
|
108
102
|
}
|
|
109
|
-
let cached = this.cache.get(view);
|
|
110
|
-
if (cached) {
|
|
111
|
-
logger.debug `layout ${viewId} from cache`;
|
|
112
|
-
return await Promise.resolve().then(() => cached);
|
|
113
|
-
}
|
|
114
103
|
try {
|
|
115
104
|
const m0 = performanceMark();
|
|
116
|
-
const
|
|
105
|
+
const out = this.cache.get(view) ?? await this.layouter.layout({
|
|
117
106
|
view,
|
|
118
107
|
styles: model.$styles,
|
|
119
108
|
});
|
|
120
|
-
|
|
121
|
-
|
|
109
|
+
if (this.cache.has(view)) {
|
|
110
|
+
logger.debug `layout ${viewId} from cache`;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.viewSucceed(view, model, out);
|
|
114
|
+
logger.debug(`layout {viewId} in ${m0.pretty}`, { viewId });
|
|
115
|
+
}
|
|
116
|
+
if (isTruthy(layoutType)) {
|
|
117
|
+
return {
|
|
118
|
+
dot: out.dot,
|
|
119
|
+
diagram: this.withLayoutType(out.diagram, model, layoutType),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
122
123
|
}
|
|
123
124
|
catch (e) {
|
|
124
125
|
const errMessage = loggable(e);
|
|
@@ -131,11 +132,8 @@ export class DefaultLikeC4Views {
|
|
|
131
132
|
const likeC4Model = await this.ModelBuilder.computeModel(projectId, cancelToken);
|
|
132
133
|
const layouted = await this._layoutAllViews(likeC4Model, cancelToken);
|
|
133
134
|
return layouted.map(({ diagram }) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return applyManualLayout(diagram, manualLayout);
|
|
137
|
-
}
|
|
138
|
-
return diagram;
|
|
135
|
+
// Apply manual layout if any
|
|
136
|
+
return this.withLayoutType(diagram, likeC4Model, 'manual');
|
|
139
137
|
});
|
|
140
138
|
}
|
|
141
139
|
async viewsAsGraphvizOut(projectId, cancelToken) {
|
|
@@ -181,18 +179,36 @@ export class DefaultLikeC4Views {
|
|
|
181
179
|
}
|
|
182
180
|
reportViewError(view, projectId, error) {
|
|
183
181
|
const key = `${projectId}-${view.id}`;
|
|
182
|
+
this.cache.delete(view);
|
|
184
183
|
if (!this.viewsWithReportedErrors.has(key)) {
|
|
185
184
|
this.services.shared.lsp.Connection?.window.showErrorMessage(`LikeC4: ${error}`);
|
|
186
185
|
this.viewsWithReportedErrors.add(key);
|
|
187
186
|
}
|
|
188
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Applies manual layout or calculates drifts from snapshot
|
|
190
|
+
* if layoutType is specified
|
|
191
|
+
*/
|
|
192
|
+
withLayoutType(layouted, likec4model, layoutType) {
|
|
193
|
+
if (!layoutType) {
|
|
194
|
+
return layouted;
|
|
195
|
+
}
|
|
196
|
+
const snapshot = likec4model.findManualLayout(layouted.id);
|
|
197
|
+
if (!snapshot) {
|
|
198
|
+
return layouted;
|
|
199
|
+
}
|
|
200
|
+
if (layoutType === 'manual') {
|
|
201
|
+
if (layouted[_layout] === 'manual') {
|
|
202
|
+
viewsLogger.error(`View ${layouted.id} already has manual layout, this should not happen`);
|
|
203
|
+
return layouted;
|
|
204
|
+
}
|
|
205
|
+
return applyManualLayout(layouted, snapshot);
|
|
206
|
+
}
|
|
207
|
+
return calcDriftsFromSnapshot(layouted, snapshot);
|
|
208
|
+
}
|
|
189
209
|
viewSucceed(view, likec4model, result) {
|
|
190
210
|
const projectId = likec4model.project.id;
|
|
191
211
|
const key = `${projectId}-${view.id}`;
|
|
192
|
-
const snapshot = likec4model.$data.manualLayouts?.[view.id];
|
|
193
|
-
if (snapshot) {
|
|
194
|
-
result.diagram = applyLayoutDriftReasons(result.diagram, snapshot);
|
|
195
|
-
}
|
|
196
212
|
this.viewsWithReportedErrors.delete(key);
|
|
197
213
|
this.cache.set(view, result);
|
|
198
214
|
return result;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AstUtils, DefaultAstNodeDescriptionProvider, } from 'langium';
|
|
2
|
+
import { isLikeC4Builtin } from '../likec4lib';
|
|
2
3
|
export class AstNodeDescriptionProvider extends DefaultAstNodeDescriptionProvider {
|
|
3
4
|
services;
|
|
4
5
|
constructor(services) {
|
|
@@ -6,10 +7,12 @@ export class AstNodeDescriptionProvider extends DefaultAstNodeDescriptionProvide
|
|
|
6
7
|
this.services = services;
|
|
7
8
|
}
|
|
8
9
|
createDescription(node, name, document) {
|
|
9
|
-
|
|
10
|
+
document ??= AstUtils.getDocument(node);
|
|
10
11
|
const description = super.createDescription(node, name, document);
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
if (!isLikeC4Builtin(document.uri)) {
|
|
13
|
+
document.likec4ProjectId ??= this.services.shared.workspace.ProjectsManager.belongsTo(document);
|
|
14
|
+
description.likec4ProjectId = document.likec4ProjectId;
|
|
15
|
+
}
|
|
13
16
|
return description;
|
|
14
17
|
}
|
|
15
18
|
}
|
|
@@ -8,7 +8,7 @@ export class IndexManager extends DefaultIndexManager {
|
|
|
8
8
|
async updateContent(document, cancelToken) {
|
|
9
9
|
const projects = this.services.workspace.ProjectsManager;
|
|
10
10
|
// Ensure the document is assigned to a project
|
|
11
|
-
document.likec4ProjectId = projects.belongsTo(document
|
|
11
|
+
document.likec4ProjectId = projects.belongsTo(document);
|
|
12
12
|
await super.updateContent(document, cancelToken);
|
|
13
13
|
}
|
|
14
14
|
projectElements(projectId, nodeType, uris) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NonEmptyArray, ProjectId } from '@likec4/core';
|
|
2
|
-
import type { LangiumDocument, Stream } from 'langium';
|
|
2
|
+
import type { LangiumDocument, Stream, URI } from 'langium';
|
|
3
3
|
import { DefaultLangiumDocuments } from 'langium';
|
|
4
4
|
import { type LikeC4LangiumDocument } from '../ast';
|
|
5
5
|
import type { LikeC4SharedServices } from '../module';
|
|
@@ -8,11 +8,12 @@ export declare class LangiumDocuments extends DefaultLangiumDocuments {
|
|
|
8
8
|
protected compare: (a: string | undefined, b: string | undefined) => number;
|
|
9
9
|
constructor(services: LikeC4SharedServices);
|
|
10
10
|
addDocument(document: LangiumDocument): void;
|
|
11
|
+
getDocument(uri: URI): LikeC4LangiumDocument | undefined;
|
|
12
|
+
get all(): Stream<LikeC4LangiumDocument>;
|
|
11
13
|
/**
|
|
12
14
|
* Returns all user documents, excluding built-in documents.
|
|
13
15
|
*/
|
|
14
16
|
get allExcludingBuiltin(): Stream<LikeC4LangiumDocument>;
|
|
15
17
|
projectDocuments(projectId: ProjectId): Stream<LikeC4LangiumDocument>;
|
|
16
18
|
groupedByProject(): Record<ProjectId, NonEmptyArray<LikeC4LangiumDocument>>;
|
|
17
|
-
resetProjectIds(): void;
|
|
18
19
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { compareNaturalHierarchically } from '@likec4/core/utils';
|
|
2
|
-
import { DefaultLangiumDocuments } from 'langium';
|
|
2
|
+
import { DefaultLangiumDocuments, stream } from 'langium';
|
|
3
3
|
import { groupBy, prop } from 'remeda';
|
|
4
4
|
import { isLikeC4LangiumDocument } from '../ast';
|
|
5
|
+
import { LikeC4LanguageMetaData } from '../generated/module';
|
|
5
6
|
import { isLikeC4Builtin } from '../likec4lib';
|
|
6
7
|
/**
|
|
7
8
|
* Compare function for document paths to ensure consistent order
|
|
8
9
|
*/
|
|
9
10
|
const compare = compareNaturalHierarchically('/', true);
|
|
10
11
|
const ensureOrder = (a, b) => compare(a.uri.path, b.uri.path);
|
|
12
|
+
const exclude = (doc) => {
|
|
13
|
+
return doc.textDocument.languageId !== LikeC4LanguageMetaData.languageId || isLikeC4Builtin(doc.uri);
|
|
14
|
+
};
|
|
11
15
|
export class LangiumDocuments extends DefaultLangiumDocuments {
|
|
12
16
|
services;
|
|
13
17
|
compare = compareNaturalHierarchically('/', true);
|
|
@@ -27,17 +31,36 @@ export class LangiumDocuments extends DefaultLangiumDocuments {
|
|
|
27
31
|
this.documentMap.set(doc.uri.toString(), doc);
|
|
28
32
|
}
|
|
29
33
|
}
|
|
34
|
+
getDocument(uri) {
|
|
35
|
+
const doc = super.getDocument(uri);
|
|
36
|
+
if (doc && !exclude(doc)) {
|
|
37
|
+
doc.likec4ProjectId ??= this.services.workspace.ProjectsManager.belongsTo(doc);
|
|
38
|
+
}
|
|
39
|
+
if (doc && !isLikeC4LangiumDocument(doc)) {
|
|
40
|
+
throw new Error(`Document ${doc.uri.path} is not a LikeC4 document`);
|
|
41
|
+
}
|
|
42
|
+
return doc;
|
|
43
|
+
}
|
|
44
|
+
get all() {
|
|
45
|
+
return stream(this.documentMap.values())
|
|
46
|
+
.filter((doc) => {
|
|
47
|
+
if (doc.textDocument.languageId === LikeC4LanguageMetaData.languageId) {
|
|
48
|
+
if (!isLikeC4Builtin(doc.uri)) {
|
|
49
|
+
doc.likec4ProjectId ??= this.services.workspace.ProjectsManager.belongsTo(doc);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
30
56
|
/**
|
|
31
57
|
* Returns all user documents, excluding built-in documents.
|
|
32
58
|
*/
|
|
33
59
|
get allExcludingBuiltin() {
|
|
34
60
|
const projects = this.services.workspace.ProjectsManager;
|
|
35
61
|
return super.all.filter((doc) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
doc.likec4ProjectId = projects.belongsTo(doc.uri);
|
|
40
|
-
return !projects.isExcluded(doc);
|
|
62
|
+
// Exclude built-in and non-LikeC4 documents, and also documents excluded by ProjectsManager
|
|
63
|
+
return !exclude(doc) && !projects.isExcluded(doc);
|
|
41
64
|
});
|
|
42
65
|
}
|
|
43
66
|
projectDocuments(projectId) {
|
|
@@ -46,13 +69,4 @@ export class LangiumDocuments extends DefaultLangiumDocuments {
|
|
|
46
69
|
groupedByProject() {
|
|
47
70
|
return groupBy(this.allExcludingBuiltin.toArray(), prop('likec4ProjectId'));
|
|
48
71
|
}
|
|
49
|
-
resetProjectIds() {
|
|
50
|
-
const projects = this.services.workspace.ProjectsManager;
|
|
51
|
-
this.all.forEach(doc => {
|
|
52
|
-
if (!isLikeC4LangiumDocument(doc) || isLikeC4Builtin(doc.uri)) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
doc.likec4ProjectId = projects.belongsTo(doc);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
72
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
import type { scalar } from '@likec4/core/types';
|
|
3
|
+
import type { ProjectId, scalar } from '@likec4/core/types';
|
|
4
4
|
import { type Cancellation, type LangiumDocument, URI, WorkspaceCache } from 'langium';
|
|
5
5
|
import type { Tagged } from 'type-fest';
|
|
6
6
|
import type { LikeC4SharedServices } from '../module';
|
|
@@ -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;
|
|
@@ -29,7 +45,7 @@ export declare class ProjectsManager {
|
|
|
29
45
|
* The global project ID used for all documents
|
|
30
46
|
* that are not part of a specific project.
|
|
31
47
|
*/
|
|
32
|
-
static readonly DefaultProjectId:
|
|
48
|
+
static readonly DefaultProjectId: ProjectId<string>;
|
|
33
49
|
constructor(services: LikeC4SharedServices);
|
|
34
50
|
/**
|
|
35
51
|
* Returns:
|
|
@@ -40,6 +56,7 @@ export declare class ProjectsManager {
|
|
|
40
56
|
*/
|
|
41
57
|
get defaultProjectId(): scalar.ProjectId | undefined;
|
|
42
58
|
set defaultProjectId(id: string | scalar.ProjectId | undefined);
|
|
59
|
+
get default(): ProjectData;
|
|
43
60
|
get all(): NonEmptyReadonlyArray<scalar.ProjectId>;
|
|
44
61
|
getProject(arg: scalar.ProjectId | LangiumDocument): Project;
|
|
45
62
|
/**
|
|
@@ -64,31 +81,43 @@ export declare class ProjectsManager {
|
|
|
64
81
|
*/
|
|
65
82
|
isConfigFile(entry: URI): boolean;
|
|
66
83
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* @param entry The file system entry to check
|
|
84
|
+
* Registers likec4 project by config file.
|
|
70
85
|
*/
|
|
71
|
-
registerConfigFile(configFile: URI): Promise<ProjectData
|
|
86
|
+
registerConfigFile(configFile: URI): Promise<ProjectData>;
|
|
72
87
|
/**
|
|
73
88
|
* Registers (or reloads) likec4 project by config file or config object.
|
|
74
89
|
* If there is some project registered at same folder, it will be reloaded.
|
|
75
90
|
*/
|
|
76
|
-
registerProject(opts:
|
|
77
|
-
config: LikeC4ProjectConfigInput;
|
|
91
|
+
registerProject(opts: {
|
|
92
|
+
config: LikeC4ProjectConfig | LikeC4ProjectConfigInput;
|
|
78
93
|
folderUri: URI | string;
|
|
79
94
|
}): Promise<ProjectData>;
|
|
80
95
|
/**
|
|
81
|
-
*
|
|
82
|
-
* If
|
|
96
|
+
* Determines which project the given document belongs to.
|
|
97
|
+
* If the document does not belong to any project, returns the default project ID.
|
|
83
98
|
*/
|
|
84
|
-
private _registerProject;
|
|
85
99
|
belongsTo(document: LangiumDocument | URI | string): scalar.ProjectId;
|
|
86
100
|
reloadProjects(): Promise<void>;
|
|
87
101
|
protected _reloadProjects(): Promise<void>;
|
|
88
102
|
protected uniqueProjectId(name: string): scalar.ProjectId;
|
|
89
|
-
protected
|
|
90
|
-
|
|
91
|
-
protected findProjectForDocument(documentUri: string):
|
|
92
|
-
protected get mappingsToProject(): WorkspaceCache<string,
|
|
103
|
+
protected reset(): void;
|
|
104
|
+
rebuidProject(projectId: ProjectId, cancelToken?: Cancellation.CancellationToken): Promise<void>;
|
|
105
|
+
protected findProjectForDocument(documentUri: string): ProjectData;
|
|
106
|
+
protected get mappingsToProject(): WorkspaceCache<string, ProjectData>;
|
|
107
|
+
/**
|
|
108
|
+
* The mapping between documents and projects they belong to.
|
|
109
|
+
* Lazy-created due to initialization order of the LanguageServer
|
|
110
|
+
*/
|
|
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
|
+
}>;
|
|
121
|
+
private getWorkspaceFolder;
|
|
93
122
|
}
|
|
94
123
|
export {};
|