@likec4/language-server 1.44.0 → 1.45.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 +1 -15
- package/dist/LikeC4LanguageServices.js +2 -32
- package/dist/Rpc.js +32 -20
- package/dist/ast.js +6 -2
- package/dist/browser.js +2 -2
- package/dist/bundled.js +2 -0
- package/dist/bundled.mjs +3184 -3162
- package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
- package/dist/filesystem/ChokidarWatcher.js +27 -16
- package/dist/filesystem/LikeC4FileSystem.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -3
- package/dist/mcp/server/StdioLikeC4MCPServer.js +10 -6
- package/dist/mcp/server/StreamableLikeC4MCPServer.js +97 -97
- package/dist/mcp/server/WithMCPServer.js +5 -5
- package/dist/mcp/tools/search-element.js +5 -5
- package/dist/mcp/utils.js +1 -1
- package/dist/model/deployments-index.js +2 -2
- package/dist/model/fqn-index.d.ts +1 -2
- package/dist/model/fqn-index.js +13 -16
- package/dist/model/model-builder.js +0 -2
- package/dist/model/model-parser.js +34 -27
- package/dist/model/parser/SpecificationParser.js +4 -0
- package/dist/model/parser/ViewsParser.js +3 -1
- package/dist/model-change/ModelChanges.d.ts +2 -2
- package/dist/model-change/ModelChanges.js +36 -9
- package/dist/protocol.d.ts +33 -10
- package/dist/protocol.js +13 -4
- package/dist/view-utils/manual-layout.js +2 -4
- package/dist/views/LikeC4ManualLayouts.d.ts +16 -2
- package/dist/views/LikeC4ManualLayouts.js +99 -22
- 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 +19 -15
- package/dist/workspace/ProjectsManager.js +137 -41
- package/dist/workspace/WorkspaceManager.js +5 -0
- package/package.json +16 -15
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { DefaultWeakMap, invariant, MultiMap } from '@likec4/core/utils';
|
|
2
|
-
import { loggable } from '@likec4/log';
|
|
3
2
|
import { DocumentState, UriUtils } from 'langium';
|
|
4
3
|
import { pipe } from 'remeda';
|
|
5
4
|
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
6
5
|
import { isLikeC4LangiumDocument } from '../ast';
|
|
7
|
-
import {
|
|
8
|
-
import { logger as rootLogger } from '../logger';
|
|
6
|
+
import { logger as rootLogger, logWarnError } from '../logger';
|
|
9
7
|
import { BaseParser } from './parser/Base';
|
|
10
8
|
import { DeploymentModelParser } from './parser/DeploymentModelParser';
|
|
11
9
|
import { DeploymentViewParser } from './parser/DeploymentViewParser';
|
|
@@ -25,24 +23,34 @@ export class LikeC4ModelParser {
|
|
|
25
23
|
cachedParsers = new DefaultWeakMap((doc) => this.createParser(doc));
|
|
26
24
|
constructor(services) {
|
|
27
25
|
this.services = services;
|
|
28
|
-
services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Linked, doc => {
|
|
29
|
-
|
|
26
|
+
services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Linked, async (doc) => {
|
|
27
|
+
if (this.cachedParsers.has(doc)) {
|
|
28
|
+
logger.trace('Linked: clear cached parser {projectId} document {doc}', {
|
|
29
|
+
projectId: doc.likec4ProjectId,
|
|
30
|
+
doc: UriUtils.basename(doc.uri),
|
|
31
|
+
});
|
|
32
|
+
this.cachedParsers.delete(doc);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
services.shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Linked, async (docs) => {
|
|
36
|
+
for (const doc of docs) {
|
|
30
37
|
if (services.shared.workspace.ProjectsManager.isExcluded(doc)) {
|
|
31
|
-
|
|
38
|
+
continue;
|
|
32
39
|
}
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
try {
|
|
41
|
+
// Force create parser for linked document (if not yet created)
|
|
42
|
+
this.parse(doc);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logWarnError(error);
|
|
35
46
|
}
|
|
36
|
-
}
|
|
37
|
-
catch (e) {
|
|
38
|
-
logger.warn(loggable(e));
|
|
39
47
|
}
|
|
40
48
|
});
|
|
41
49
|
// We need to clean up cached parser when document is validated and has errors
|
|
42
50
|
// Because after that parser takes into account validation results
|
|
43
|
-
services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Validated, doc => {
|
|
51
|
+
services.shared.workspace.DocumentBuilder.onDocumentPhase(DocumentState.Validated, async (doc) => {
|
|
44
52
|
if (doc.diagnostics?.some(d => d.severity === DiagnosticSeverity.Error) && this.cachedParsers.has(doc)) {
|
|
45
|
-
logger.
|
|
53
|
+
logger.trace('Validated: clear cached parser {projectId} document {doc} because of errors', {
|
|
46
54
|
projectId: doc.likec4ProjectId,
|
|
47
55
|
doc: UriUtils.basename(doc.uri),
|
|
48
56
|
});
|
|
@@ -54,31 +62,30 @@ export class LikeC4ModelParser {
|
|
|
54
62
|
return this.services.shared.workspace.LangiumDocuments.projectDocuments(projectId).map(d => this.parse(d));
|
|
55
63
|
}
|
|
56
64
|
parse(doc) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return parser.doc;
|
|
60
|
-
}
|
|
61
|
-
catch (cause) {
|
|
62
|
-
throw new Error(`Error parsing document ${doc.uri.toString()}`, { cause });
|
|
63
|
-
}
|
|
65
|
+
const parser = this.forDocument(doc);
|
|
66
|
+
return parser.doc;
|
|
64
67
|
}
|
|
65
68
|
forDocument(doc) {
|
|
66
|
-
if (doc.state < DocumentState.Linked) {
|
|
67
|
-
logger.warn(`Document {doc} is not linked`, { doc: doc.uri.toString() });
|
|
68
|
-
}
|
|
69
69
|
return this.cachedParsers.get(doc);
|
|
70
70
|
}
|
|
71
71
|
createParser(doc) {
|
|
72
72
|
invariant(isLikeC4LangiumDocument(doc), `Document ${doc.uri.toString()} is not a LikeC4 document`);
|
|
73
|
+
const docbasename = UriUtils.basename(doc.uri);
|
|
73
74
|
if (doc.likec4ProjectId) {
|
|
74
|
-
logger.
|
|
75
|
+
logger.trace(`create parser {projectId} document {doc}`, {
|
|
75
76
|
projectId: doc.likec4ProjectId,
|
|
76
|
-
doc:
|
|
77
|
+
doc: docbasename,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
else {
|
|
80
81
|
logger.warn(`create parser for document without project {doc}`, { doc: doc.uri.fsPath });
|
|
81
82
|
}
|
|
83
|
+
if (doc.state < DocumentState.Linked) {
|
|
84
|
+
logger.warn(`Document {doc} is not linked, state is {state}`, {
|
|
85
|
+
doc: docbasename,
|
|
86
|
+
state: doc.state,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
82
89
|
const props = {
|
|
83
90
|
c4Specification: {
|
|
84
91
|
tags: {},
|
|
@@ -111,8 +118,8 @@ export class LikeC4ModelParser {
|
|
|
111
118
|
parser.parseDeployment();
|
|
112
119
|
parser.parseViews();
|
|
113
120
|
}
|
|
114
|
-
catch (
|
|
115
|
-
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new Error(`Error parsing document ${doc.uri.fsPath}`, { cause: error });
|
|
116
123
|
}
|
|
117
124
|
return parser;
|
|
118
125
|
}
|
|
@@ -55,6 +55,10 @@ export function SpecificationParser(B) {
|
|
|
55
55
|
const tag = tagSpec.tag.name;
|
|
56
56
|
const astPath = this.getAstNodePath(tagSpec.tag);
|
|
57
57
|
const color = tagSpec.color && this.parseColorLiteral(tagSpec.color);
|
|
58
|
+
if (tag in c4Specification.tags) {
|
|
59
|
+
logger.warn(`Tag {tag} is already defined, skipping duplicate`, { tag });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
58
62
|
if (isTruthy(tag)) {
|
|
59
63
|
c4Specification.tags[tag] = {
|
|
60
64
|
astPath,
|
|
@@ -63,7 +63,9 @@ export function ViewsParser(B) {
|
|
|
63
63
|
const viewOfEl = elementRef(astNode.viewOf);
|
|
64
64
|
const _viewOf = viewOfEl && this.resolveFqn(viewOfEl);
|
|
65
65
|
if (!_viewOf) {
|
|
66
|
-
|
|
66
|
+
const viewId = astNode.name ?? 'unnamed';
|
|
67
|
+
const msg = astNode.viewOf.$cstNode?.text ?? '<unknown>';
|
|
68
|
+
logger.warn(`viewOf {viewId} not resolved {msg}`, { msg, viewId });
|
|
67
69
|
}
|
|
68
70
|
else {
|
|
69
71
|
viewOf = _viewOf;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ViewChange } from '@likec4/core';
|
|
2
|
-
import {
|
|
2
|
+
import { Range, TextEdit } from 'vscode-languageserver-types';
|
|
3
3
|
import type { ViewLocateResult } from '../model';
|
|
4
4
|
import type { LikeC4Services } from '../module';
|
|
5
5
|
import type { ChangeView } from '../protocol';
|
|
@@ -7,7 +7,7 @@ export declare class LikeC4ModelChanges {
|
|
|
7
7
|
private services;
|
|
8
8
|
private locator;
|
|
9
9
|
constructor(services: LikeC4Services);
|
|
10
|
-
applyChange(changeView: ChangeView.Params): Promise<
|
|
10
|
+
applyChange(changeView: ChangeView.Params): Promise<ChangeView.Res>;
|
|
11
11
|
protected convertToTextEdit({ lookup, change }: {
|
|
12
12
|
lookup: ViewLocateResult;
|
|
13
13
|
change: Exclude<ViewChange, ViewChange.SaveViewSnapshot | ViewChange.ResetManualLayout>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { invariant, nonexhaustive } from '@likec4/core';
|
|
2
|
-
import {
|
|
2
|
+
import { loggable, wrapError } from '@likec4/log';
|
|
3
|
+
import { Range, TextEdit } from 'vscode-languageserver-types';
|
|
3
4
|
import { logger as mainLogger } from '../logger';
|
|
4
5
|
import { changeElementStyle } from './changeElementStyle';
|
|
5
6
|
import { changeViewLayout } from './changeViewLayout';
|
|
@@ -23,7 +24,7 @@ export class LikeC4ModelChanges {
|
|
|
23
24
|
logger.debug `Applying model change ${change.op} to view ${viewId} in project ${project.id}`;
|
|
24
25
|
const lookup = this.locator.locateViewAst(viewId, project.id);
|
|
25
26
|
if (!lookup) {
|
|
26
|
-
throw new Error(`
|
|
27
|
+
throw new Error(`View ${viewId} not found in project ${project.id}`);
|
|
27
28
|
}
|
|
28
29
|
const textDocument = {
|
|
29
30
|
uri: lookup.doc.textDocument.uri,
|
|
@@ -39,11 +40,26 @@ export class LikeC4ModelChanges {
|
|
|
39
40
|
logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
const location = await this.services.likec4.ManualLayouts.write(project, change.layout);
|
|
44
|
+
result = {
|
|
45
|
+
success: true,
|
|
46
|
+
location,
|
|
47
|
+
};
|
|
43
48
|
return;
|
|
44
49
|
}
|
|
45
50
|
if (change.op === 'reset-manual-layout') {
|
|
46
|
-
|
|
51
|
+
// If there is an existing manual layout v1
|
|
52
|
+
if (lookup.view.manualLayout) {
|
|
53
|
+
// We clean it up
|
|
54
|
+
await removeManualLayoutV1(this.services, { lookup }).catch(err => {
|
|
55
|
+
logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const location = await this.services.likec4.ManualLayouts.remove(project, viewId);
|
|
59
|
+
result = {
|
|
60
|
+
success: true,
|
|
61
|
+
location,
|
|
62
|
+
};
|
|
47
63
|
return;
|
|
48
64
|
}
|
|
49
65
|
const { edits, modifiedRange } = this.convertToTextEdit({
|
|
@@ -66,15 +82,26 @@ export class LikeC4ModelChanges {
|
|
|
66
82
|
return;
|
|
67
83
|
}
|
|
68
84
|
result = {
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
success: true,
|
|
86
|
+
location: {
|
|
87
|
+
uri: textDocument.uri,
|
|
88
|
+
range: modifiedRange,
|
|
89
|
+
},
|
|
71
90
|
};
|
|
72
91
|
});
|
|
73
92
|
}
|
|
74
|
-
catch (
|
|
75
|
-
|
|
93
|
+
catch (err) {
|
|
94
|
+
const error = loggable(wrapError(err, `Failed to apply change ${changeView.change.op} ${changeView.viewId}:\n`));
|
|
95
|
+
logger.error(error);
|
|
96
|
+
result = {
|
|
97
|
+
success: false,
|
|
98
|
+
error,
|
|
99
|
+
};
|
|
76
100
|
}
|
|
77
|
-
return result
|
|
101
|
+
return result ?? {
|
|
102
|
+
success: false,
|
|
103
|
+
error: 'Unknown error applying model change',
|
|
104
|
+
};
|
|
78
105
|
}
|
|
79
106
|
convertToTextEdit({ lookup, change }) {
|
|
80
107
|
switch (change.op) {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -6,6 +6,18 @@ export declare namespace DidChangeModelNotification {
|
|
|
6
6
|
const type: NotificationType<string>;
|
|
7
7
|
type Type = typeof type;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* When the snapshot of a manual layout changes
|
|
11
|
+
* Send by the editor to the language server
|
|
12
|
+
*/
|
|
13
|
+
export declare namespace DidChangeSnapshotNotification {
|
|
14
|
+
type Params = {
|
|
15
|
+
snapshotUri: DocumentUri;
|
|
16
|
+
};
|
|
17
|
+
const Method: "likec4/onDidChangeSnapshot";
|
|
18
|
+
const type: NotificationType<Params>;
|
|
19
|
+
type Type = typeof type;
|
|
20
|
+
}
|
|
9
21
|
/**
|
|
10
22
|
* When server requests to open a likec4 preview panel
|
|
11
23
|
* (available only in the editor).
|
|
@@ -72,6 +84,7 @@ export declare namespace LayoutView {
|
|
|
72
84
|
type Params = {
|
|
73
85
|
viewId: ViewId;
|
|
74
86
|
projectId?: string | undefined;
|
|
87
|
+
layoutType?: 'auto' | 'manual' | undefined;
|
|
75
88
|
};
|
|
76
89
|
type Res = {
|
|
77
90
|
result: {
|
|
@@ -102,8 +115,8 @@ export declare namespace ValidateLayout {
|
|
|
102
115
|
};
|
|
103
116
|
}[] | null;
|
|
104
117
|
};
|
|
105
|
-
const
|
|
106
|
-
type Req = typeof
|
|
118
|
+
const req: RequestType<Params, Res, void>;
|
|
119
|
+
type Req = typeof req;
|
|
107
120
|
}
|
|
108
121
|
/**
|
|
109
122
|
* Request to reload projects.
|
|
@@ -123,7 +136,10 @@ export declare namespace FetchProjects {
|
|
|
123
136
|
projects: {
|
|
124
137
|
[projectId: ProjectId]: {
|
|
125
138
|
folder: URI;
|
|
126
|
-
config:
|
|
139
|
+
config: {
|
|
140
|
+
name: string;
|
|
141
|
+
title?: string | undefined;
|
|
142
|
+
};
|
|
127
143
|
docs: NonEmptyArray<DocumentUri>;
|
|
128
144
|
};
|
|
129
145
|
};
|
|
@@ -152,8 +168,8 @@ export declare namespace BuildDocuments {
|
|
|
152
168
|
type Params = {
|
|
153
169
|
docs: DocumentUri[];
|
|
154
170
|
};
|
|
155
|
-
const
|
|
156
|
-
type Req = typeof
|
|
171
|
+
const req: RequestType<Params, void, void>;
|
|
172
|
+
type Req = typeof req;
|
|
157
173
|
}
|
|
158
174
|
/**
|
|
159
175
|
* Request to locate an element, relation, deployment or view.
|
|
@@ -200,8 +216,8 @@ export declare namespace Locate {
|
|
|
200
216
|
projectId?: string | undefined;
|
|
201
217
|
};
|
|
202
218
|
type Res = Location | null;
|
|
203
|
-
const
|
|
204
|
-
type Req = typeof
|
|
219
|
+
const req: RequestType<Params, Res, void>;
|
|
220
|
+
type Req = typeof req;
|
|
205
221
|
}
|
|
206
222
|
/**
|
|
207
223
|
* Request to change the view
|
|
@@ -213,9 +229,16 @@ export declare namespace ChangeView {
|
|
|
213
229
|
change: ViewChange;
|
|
214
230
|
projectId?: string | undefined;
|
|
215
231
|
};
|
|
216
|
-
type Res =
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
type Res = {
|
|
233
|
+
success: true;
|
|
234
|
+
location: Location | null;
|
|
235
|
+
} | {
|
|
236
|
+
success: false;
|
|
237
|
+
location?: Location | null;
|
|
238
|
+
error: string;
|
|
239
|
+
};
|
|
240
|
+
const req: RequestType<Params, Res, void>;
|
|
241
|
+
type Req = typeof req;
|
|
219
242
|
}
|
|
220
243
|
/**
|
|
221
244
|
* Request to fetch telemetry metrics
|
package/dist/protocol.js
CHANGED
|
@@ -3,6 +3,15 @@ export var DidChangeModelNotification;
|
|
|
3
3
|
(function (DidChangeModelNotification) {
|
|
4
4
|
DidChangeModelNotification.type = new NotificationType('likec4/onDidChangeModel');
|
|
5
5
|
})(DidChangeModelNotification || (DidChangeModelNotification = {}));
|
|
6
|
+
/**
|
|
7
|
+
* When the snapshot of a manual layout changes
|
|
8
|
+
* Send by the editor to the language server
|
|
9
|
+
*/
|
|
10
|
+
export var DidChangeSnapshotNotification;
|
|
11
|
+
(function (DidChangeSnapshotNotification) {
|
|
12
|
+
DidChangeSnapshotNotification.Method = 'likec4/onDidChangeSnapshot';
|
|
13
|
+
DidChangeSnapshotNotification.type = new NotificationType(DidChangeSnapshotNotification.Method);
|
|
14
|
+
})(DidChangeSnapshotNotification || (DidChangeSnapshotNotification = {}));
|
|
6
15
|
/**
|
|
7
16
|
* When server requests to open a likec4 preview panel
|
|
8
17
|
* (available only in the editor).
|
|
@@ -51,7 +60,7 @@ export var LayoutView;
|
|
|
51
60
|
*/
|
|
52
61
|
export var ValidateLayout;
|
|
53
62
|
(function (ValidateLayout) {
|
|
54
|
-
ValidateLayout.
|
|
63
|
+
ValidateLayout.req = new RequestType('likec4/validate-layout');
|
|
55
64
|
})(ValidateLayout || (ValidateLayout = {}));
|
|
56
65
|
/**
|
|
57
66
|
* Request to reload projects.
|
|
@@ -79,7 +88,7 @@ export var RegisterProject;
|
|
|
79
88
|
*/
|
|
80
89
|
export var BuildDocuments;
|
|
81
90
|
(function (BuildDocuments) {
|
|
82
|
-
BuildDocuments.
|
|
91
|
+
BuildDocuments.req = new RequestType('likec4/build');
|
|
83
92
|
})(BuildDocuments || (BuildDocuments = {}));
|
|
84
93
|
/**
|
|
85
94
|
* Request to locate an element, relation, deployment or view.
|
|
@@ -87,7 +96,7 @@ export var BuildDocuments;
|
|
|
87
96
|
*/
|
|
88
97
|
export var Locate;
|
|
89
98
|
(function (Locate) {
|
|
90
|
-
Locate.
|
|
99
|
+
Locate.req = new RequestType('likec4/locate');
|
|
91
100
|
})(Locate || (Locate = {}));
|
|
92
101
|
// #endregion
|
|
93
102
|
/**
|
|
@@ -96,7 +105,7 @@ export var Locate;
|
|
|
96
105
|
*/
|
|
97
106
|
export var ChangeView;
|
|
98
107
|
(function (ChangeView) {
|
|
99
|
-
ChangeView.
|
|
108
|
+
ChangeView.req = new RequestType('likec4/change-view');
|
|
100
109
|
})(ChangeView || (ChangeView = {}));
|
|
101
110
|
/**
|
|
102
111
|
* Request to fetch telemetry metrics
|
|
@@ -14,10 +14,9 @@ function pack({ nodes, edges, ...rest }) {
|
|
|
14
14
|
b: [x, y, width, height],
|
|
15
15
|
c: isCompound,
|
|
16
16
|
})),
|
|
17
|
-
edges: mapValues(edges, ({ points, controlPoints, labelBBox,
|
|
17
|
+
edges: mapValues(edges, ({ points, controlPoints, labelBBox, ...e }) => ({
|
|
18
18
|
...!!controlPoints && { cp: controlPoints },
|
|
19
19
|
...!!labelBBox && { l: labelBBox },
|
|
20
|
-
...!!dotpos && { dp: dotpos },
|
|
21
20
|
...e,
|
|
22
21
|
p: points,
|
|
23
22
|
})),
|
|
@@ -36,10 +35,9 @@ function unpack({ nodes, edges, autoLayout, ...rest }) {
|
|
|
36
35
|
isCompound: c,
|
|
37
36
|
...n,
|
|
38
37
|
})),
|
|
39
|
-
edges: mapValues(edges, ({ p, cp, l,
|
|
38
|
+
edges: mapValues(edges, ({ p, cp, l, ...e }) => ({
|
|
40
39
|
...!!cp && { controlPoints: cp },
|
|
41
40
|
...!!l && { labelBBox: l },
|
|
42
|
-
...!!dp && { dotpos: dp },
|
|
43
41
|
...e,
|
|
44
42
|
points: p,
|
|
45
43
|
})),
|
|
@@ -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,6 +75,8 @@ export class DefaultLikeC4ManualLayouts {
|
|
|
70
75
|
const { manualLayout: _, ...rest } = layouted;
|
|
71
76
|
layouted = rest;
|
|
72
77
|
}
|
|
78
|
+
// Normalize icon paths before writing
|
|
79
|
+
layouted = this.normalizeIconPathsForWrite(layouted, project.folderUri);
|
|
73
80
|
const content = JSON5.stringify(layouted, {
|
|
74
81
|
space: 2,
|
|
75
82
|
quote: '\'',
|
|
@@ -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
|
}
|