@likec4/language-server 1.44.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/LikeC4LanguageServices.d.ts +4 -15
  2. package/dist/LikeC4LanguageServices.js +4 -32
  3. package/dist/Rpc.js +44 -21
  4. package/dist/ast.d.ts +10 -0
  5. package/dist/ast.js +13 -2
  6. package/dist/browser.js +2 -2
  7. package/dist/bundled.js +2 -0
  8. package/dist/bundled.mjs +3838 -4059
  9. package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
  10. package/dist/filesystem/ChokidarWatcher.js +29 -18
  11. package/dist/filesystem/LikeC4FileSystem.js +4 -0
  12. package/dist/filesystem/index.d.ts +2 -0
  13. package/dist/generated/ast.d.ts +46 -9
  14. package/dist/generated/ast.js +56 -4
  15. package/dist/generated/grammar.js +1 -1
  16. package/dist/generated-lib/icons.js +1 -1
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +5 -3
  19. package/dist/lsp/DocumentSymbolProvider.js +12 -1
  20. package/dist/mcp/server/StdioLikeC4MCPServer.js +10 -6
  21. package/dist/mcp/server/StreamableLikeC4MCPServer.js +97 -97
  22. package/dist/mcp/server/WithMCPServer.js +4 -6
  23. package/dist/mcp/tools/read-deployment.js +18 -0
  24. package/dist/mcp/tools/read-element.js +24 -0
  25. package/dist/mcp/tools/search-element.js +5 -5
  26. package/dist/mcp/utils.js +1 -1
  27. package/dist/model/builder/buildModel.js +70 -1
  28. package/dist/model/deployments-index.js +2 -2
  29. package/dist/model/fqn-index.d.ts +1 -2
  30. package/dist/model/fqn-index.js +21 -18
  31. package/dist/model/model-builder.js +0 -2
  32. package/dist/model/model-parser.d.ts +3 -0
  33. package/dist/model/model-parser.js +41 -27
  34. package/dist/model/parser/Base.js +8 -3
  35. package/dist/model/parser/GlobalsParser.d.ts +1 -0
  36. package/dist/model/parser/ModelParser.d.ts +2 -1
  37. package/dist/model/parser/ModelParser.js +45 -1
  38. package/dist/model/parser/SpecificationParser.js +4 -0
  39. package/dist/model/parser/ViewsParser.d.ts +1 -0
  40. package/dist/model/parser/ViewsParser.js +16 -1
  41. package/dist/model-change/ModelChanges.d.ts +2 -2
  42. package/dist/model-change/ModelChanges.js +41 -11
  43. package/dist/protocol.d.ts +33 -10
  44. package/dist/protocol.js +13 -4
  45. package/dist/validation/index.d.ts +1 -1
  46. package/dist/validation/index.js +11 -1
  47. package/dist/validation/relation.d.ts +1 -0
  48. package/dist/validation/relation.js +87 -1
  49. package/dist/validation/view-checks.d.ts +4 -0
  50. package/dist/validation/view-checks.js +46 -0
  51. package/dist/view-utils/manual-layout.js +2 -4
  52. package/dist/views/LikeC4ManualLayouts.d.ts +16 -2
  53. package/dist/views/LikeC4ManualLayouts.js +100 -23
  54. package/dist/views/LikeC4Views.d.ts +26 -5
  55. package/dist/views/LikeC4Views.js +49 -33
  56. package/dist/workspace/AstNodeDescriptionProvider.js +6 -3
  57. package/dist/workspace/IndexManager.js +1 -1
  58. package/dist/workspace/LangiumDocuments.d.ts +3 -2
  59. package/dist/workspace/LangiumDocuments.js +29 -15
  60. package/dist/workspace/ProjectsManager.d.ts +45 -16
  61. package/dist/workspace/ProjectsManager.js +227 -45
  62. package/dist/workspace/WorkspaceManager.js +43 -0
  63. package/package.json +22 -21
@@ -1,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
- private manualLayouts;
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 logger = rootLogger.getChild('manual-layouts');
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
- manualLayouts = new WeakMap();
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 getOrCreate(this.manualLayouts, project.folderUri, async (_) => {
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
- ...JSON5.parse(content),
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(layouted, {
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 projectCaches = await this.read(project);
90
- if (projectCaches) {
91
- logger.debug `update snapshot cache of ${layouted.id} in project ${project.id}`;
92
- projectCaches[layouted.id] = layouted;
93
- }
94
- else {
95
- this.manualLayouts.delete(project.folderUri);
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 projectCaches = await this.read(project);
119
- if (projectCaches) {
120
- logger.debug `clean cache of ${view} in project ${project.id}`;
121
- delete projectCaches[view];
122
- }
123
- else {
124
- this.manualLayouts.delete(project.folderUri);
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.manualLayouts = new WeakMap();
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
- * Returns all layouted views (without manual layout)
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 (from sources, i.e. without manual layout)
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(viewId: ViewId, projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<GraphvizOut | null>;
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: ViewId, projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<GraphvizOut | null>;
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 { applyLayoutDriftReasons, applyManualLayout } from '@likec4/core/model';
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 { unique, values } from 'remeda';
5
+ import { isTruthy, values } from 'remeda';
6
6
  import { logger as rootLogger, logWarnError } from '../logger';
7
7
  import { performanceMark } from '../utils';
8
8
  const viewsLogger = rootLogger.getChild('views');
@@ -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 project = this.services.shared.workspace.ProjectsManager.getProject(projectId);
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 result = await this.layouter.layout({
105
+ const out = this.cache.get(view) ?? await this.layouter.layout({
117
106
  view,
118
107
  styles: model.$styles,
119
108
  });
120
- logger.debug(`layout {viewId} ready in ${m0.pretty}`, { viewId });
121
- return this.viewSucceed(view, model, result);
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
- const manualLayout = likeC4Model.$data.manualLayouts?.[diagram.id];
135
- if (manualLayout) {
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
- const doc = document ?? AstUtils.getDocument(node);
10
+ document ??= AstUtils.getDocument(node);
10
11
  const description = super.createDescription(node, name, document);
11
- doc.likec4ProjectId ??= this.services.shared.workspace.ProjectsManager.belongsTo(doc.uri);
12
- description.likec4ProjectId = doc.likec4ProjectId;
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.uri);
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
- if (!isLikeC4LangiumDocument(doc) || isLikeC4Builtin(doc.uri)) {
37
- return false;
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: scalar.ProjectId<string>;
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
- * Checks if the provided file system entry is a valid project config file.
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 | undefined>;
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: URI | {
77
- config: LikeC4ProjectConfigInput;
91
+ registerProject(opts: {
92
+ config: LikeC4ProjectConfig | LikeC4ProjectConfigInput;
78
93
  folderUri: URI | string;
79
94
  }): Promise<ProjectData>;
80
95
  /**
81
- * Registers (or reloads) likec4 project by config file or config object.
82
- * If there is some project registered at same folder, it will be reloaded.
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 resetProjectIds(): void;
90
- protected rebuidDocuments(cancelToken?: Cancellation.CancellationToken): Promise<void>;
91
- protected findProjectForDocument(documentUri: string): Pick<ProjectData, "id" | "exclude" | "config">;
92
- protected get mappingsToProject(): WorkspaceCache<string, Pick<ProjectData, 'id' | 'config' | 'exclude'>>;
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 {};