@likec4/language-server 1.31.0 → 1.32.1

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 (82) hide show
  1. package/dist/LikeC4LanguageServices.d.ts +2 -1
  2. package/dist/LikeC4LanguageServices.js +4 -3
  3. package/dist/Rpc.js +11 -4
  4. package/dist/ast.d.ts +12 -15
  5. package/dist/ast.js +4 -1
  6. package/dist/bundled.mjs +2545 -2476
  7. package/dist/formatting/LikeC4Formatter.js +7 -6
  8. package/dist/generated/ast.d.ts +60 -19
  9. package/dist/generated/ast.js +79 -24
  10. package/dist/generated/grammar.js +1 -1
  11. package/dist/lsp/CompletionProvider.js +1 -1
  12. package/dist/lsp/DocumentLinkProvider.js +4 -3
  13. package/dist/lsp/HoverProvider.js +9 -1
  14. package/dist/lsp/SemanticTokenProvider.js +18 -4
  15. package/dist/mcp/LikeC4MCPServerFactory.d.ts +5 -2
  16. package/dist/mcp/LikeC4MCPServerFactory.js +2 -72
  17. package/dist/mcp/LikeC4MCPTools.js +5 -3
  18. package/dist/mcp/sseserver/MCPServerFactory.d.ts +8 -0
  19. package/dist/mcp/sseserver/MCPServerFactory.js +78 -0
  20. package/dist/mcp/sseserver/with-mcp-server.d.ts +2 -0
  21. package/dist/mcp/sseserver/with-mcp-server.js +5 -1
  22. package/dist/mcp/utils.d.ts +5 -4
  23. package/dist/mcp/utils.js +2 -2
  24. package/dist/model/builder/MergedExtends.d.ts +4 -4
  25. package/dist/model/builder/MergedExtends.js +1 -1
  26. package/dist/model/builder/MergedSpecification.d.ts +4 -3
  27. package/dist/model/builder/MergedSpecification.js +9 -8
  28. package/dist/model/builder/assignTagColors.d.ts +7 -0
  29. package/dist/model/builder/assignTagColors.js +51 -0
  30. package/dist/model/builder/buildModel.d.ts +2 -2
  31. package/dist/model/builder/buildModel.js +40 -18
  32. package/dist/model/deployments-index.js +2 -2
  33. package/dist/model/fqn-index.d.ts +1 -1
  34. package/dist/model/fqn-index.js +6 -6
  35. package/dist/model/model-builder.d.ts +10 -4
  36. package/dist/model/model-builder.js +22 -19
  37. package/dist/model/model-locator.d.ts +10 -2
  38. package/dist/model/model-locator.js +65 -9
  39. package/dist/model/model-parser-where.d.ts +1 -1
  40. package/dist/model/model-parser-where.js +1 -1
  41. package/dist/model/model-parser.d.ts +19 -11
  42. package/dist/model/model-parser.js +11 -7
  43. package/dist/model/parser/Base.d.ts +1 -0
  44. package/dist/model/parser/Base.js +24 -3
  45. package/dist/model/parser/DeploymentModelParser.d.ts +2 -1
  46. package/dist/model/parser/DeploymentModelParser.js +7 -5
  47. package/dist/model/parser/DeploymentViewParser.d.ts +4 -3
  48. package/dist/model/parser/DeploymentViewParser.js +2 -1
  49. package/dist/model/parser/FqnRefParser.d.ts +2 -1
  50. package/dist/model/parser/FqnRefParser.js +2 -5
  51. package/dist/model/parser/GlobalsParser.d.ts +24 -24
  52. package/dist/model/parser/GlobalsParser.js +4 -4
  53. package/dist/model/parser/ImportsParser.d.ts +1 -0
  54. package/dist/model/parser/ModelParser.d.ts +2 -1
  55. package/dist/model/parser/ModelParser.js +1 -1
  56. package/dist/model/parser/PredicatesParser.d.ts +13 -12
  57. package/dist/model/parser/SpecificationParser.d.ts +6 -2
  58. package/dist/model/parser/SpecificationParser.js +21 -31
  59. package/dist/model/parser/ViewsParser.d.ts +24 -23
  60. package/dist/model/parser/ViewsParser.js +12 -19
  61. package/dist/model-change/changeElementStyle.d.ts +2 -2
  62. package/dist/model-change/changeElementStyle.js +6 -2
  63. package/dist/module.d.ts +1 -1
  64. package/dist/module.js +5 -2
  65. package/dist/protocol.d.ts +24 -3
  66. package/dist/protocol.js +4 -0
  67. package/dist/references/scope-computation.js +2 -3
  68. package/dist/test/testServices.d.ts +15 -0
  69. package/dist/test/testServices.js +1 -1
  70. package/dist/validation/index.d.ts +1 -1
  71. package/dist/validation/index.js +10 -3
  72. package/dist/validation/property-checks.d.ts +1 -0
  73. package/dist/validation/property-checks.js +62 -0
  74. package/dist/validation/relation.js +1 -1
  75. package/dist/validation/specification.js +1 -1
  76. package/dist/view-utils/assignNavigateTo.d.ts +1 -1
  77. package/dist/view-utils/assignNavigateTo.js +3 -3
  78. package/dist/views/likec4-views.d.ts +1 -3
  79. package/dist/views/likec4-views.js +68 -44
  80. package/package.json +23 -21
  81. /package/dist/mcp/sseserver/{SSELikeC4MCPServer.d.ts → MCPServer.d.ts} +0 -0
  82. /package/dist/mcp/sseserver/{SSELikeC4MCPServer.js → MCPServer.js} +0 -0
@@ -1,6 +1,7 @@
1
+ import * as c4 from "@likec4/core";
1
2
  import {
2
3
  computeColorValues,
3
- DeploymentElement,
4
+ isDeploymentNode,
4
5
  isGlobalFqn,
5
6
  parentFqn,
6
7
  sortByFqnHierarchically
@@ -9,10 +10,12 @@ import { resolveRulesExtendedViews } from "@likec4/core/compute-view";
9
10
  import {
10
11
  filter,
11
12
  flatMap,
13
+ forEach,
12
14
  indexBy,
13
15
  isDefined,
14
16
  isNullish,
15
17
  isTruthy,
18
+ keys,
16
19
  map,
17
20
  mapValues,
18
21
  pipe,
@@ -23,14 +26,20 @@ import { logger } from "../../logger.js";
23
26
  import { resolveRelativePaths } from "../../view-utils/index.js";
24
27
  import { MergedExtends } from "./MergedExtends.js";
25
28
  import { MergedSpecification } from "./MergedSpecification.js";
26
- export function buildModelData(docs) {
29
+ export function buildModelData(projectId, docs) {
27
30
  const c4Specification = new MergedSpecification(docs);
28
- const customColorDefinitions = mapValues(
31
+ const customColors = mapValues(
29
32
  c4Specification.specs.colors,
30
33
  (c) => computeColorValues(c.color)
31
34
  );
35
+ const metadataKeys = /* @__PURE__ */ new Set();
32
36
  const elementExtends = new MergedExtends();
33
37
  const deploymentExtends = new MergedExtends();
38
+ const scanMetadataKeys = (obj) => {
39
+ if (obj?.metadata) {
40
+ keys(obj.metadata).forEach((key) => metadataKeys.add(key));
41
+ }
42
+ };
34
43
  const elements = pipe(
35
44
  docs,
36
45
  flatMap((d) => {
@@ -48,7 +57,8 @@ export function buildModelData(docs) {
48
57
  logger.debug`No parent found for ${el.id}`;
49
58
  return acc;
50
59
  }
51
- acc[el.id] = elementExtends.apply(el);
60
+ acc[el.id] = elementExtends.applyExtended(el);
61
+ scanMetadataKeys(acc[el.id]);
52
62
  return acc;
53
63
  },
54
64
  {}
@@ -59,14 +69,16 @@ export function buildModelData(docs) {
59
69
  flatMap((d) => map(d.c4Relations, c4Specification.toModelRelation)),
60
70
  filter((rel) => {
61
71
  if (!rel) return false;
62
- if (isNullish(elements[rel.source]) && !isGlobalFqn(rel.source) || isNullish(elements[rel.target]) && !isGlobalFqn(rel.target)) {
72
+ const source = c4.FqnRef.flatten(rel.source), target = c4.FqnRef.flatten(rel.target);
73
+ if (isNullish(elements[source]) && !isGlobalFqn(source) || isNullish(elements[target]) && !isGlobalFqn(target)) {
63
74
  logger.debug`Invalid relation ${rel.id}
64
- source: ${rel.source} resolved: ${!!elements[rel.source]}
65
- target: ${rel.target} resolved: ${!!elements[rel.target]}\n`;
75
+ source: ${source} resolved: ${!!elements[source]}
76
+ target: ${target} resolved: ${!!elements[target]}\n`;
66
77
  return false;
67
78
  }
68
79
  return true;
69
80
  }),
81
+ forEach(scanMetadataKeys),
70
82
  indexBy(prop("id"))
71
83
  );
72
84
  const deploymentElements = pipe(
@@ -86,7 +98,8 @@ export function buildModelData(docs) {
86
98
  logger.debug`No parent found for deployment element ${el.id}`;
87
99
  return acc;
88
100
  }
89
- acc[el.id] = DeploymentElement.isDeploymentNode(el) ? deploymentExtends.apply(el) : el;
101
+ acc[el.id] = isDeploymentNode(el) ? deploymentExtends.applyExtended(el) : el;
102
+ scanMetadataKeys(acc[el.id]);
90
103
  return acc;
91
104
  },
92
105
  {}
@@ -97,10 +110,10 @@ export function buildModelData(docs) {
97
110
  flatMap((d) => map(d.c4DeploymentRelations, c4Specification.toDeploymentRelation)),
98
111
  filter((rel) => {
99
112
  if (!rel) return false;
100
- if (isNullish(deploymentElements[rel.source.id]) || isNullish(deploymentElements[rel.target.id])) {
113
+ if (isNullish(deploymentElements[rel.source.deployment]) || isNullish(deploymentElements[rel.target.deployment])) {
101
114
  logger.debug`Invalid deployment relation ${rel.id}
102
- source: ${rel.source.id} resolved: ${!!deploymentElements[rel.source.id]}
103
- target: ${rel.target.id} resolved: ${!!deploymentElements[rel.target.id]}\n`;
115
+ source: ${rel.source.deployment} resolved: ${!!deploymentElements[rel.source.deployment]}
116
+ target: ${rel.target.deployment} resolved: ${!!deploymentElements[rel.target.deployment]}\n`;
104
117
  return false;
105
118
  }
106
119
  return true;
@@ -111,6 +124,7 @@ export function buildModelData(docs) {
111
124
  logger.debug`Duplicate deployment relation ${el.id}`;
112
125
  return acc;
113
126
  }
127
+ scanMetadataKeys(el);
114
128
  acc[el.id] = el;
115
129
  return acc;
116
130
  },
@@ -129,7 +143,7 @@ export function buildModelData(docs) {
129
143
  // model should include discriminant __
130
144
  ...model
131
145
  } = parsedAstView;
132
- if (parsedAstView.__ === "element" && isNullish(title) && "viewOf" in parsedAstView) {
146
+ if (parsedAstView[c4._type] === "element" && isNullish(title) && "viewOf" in parsedAstView) {
133
147
  title = elements[parsedAstView.viewOf]?.title ?? null;
134
148
  }
135
149
  if (isNullish(title) && id === "index") {
@@ -137,7 +151,7 @@ export function buildModelData(docs) {
137
151
  }
138
152
  return {
139
153
  ...model,
140
- customColorDefinitions,
154
+ [c4._stage]: "parsed",
141
155
  docUri,
142
156
  description,
143
157
  title,
@@ -153,13 +167,13 @@ export function buildModelData(docs) {
153
167
  );
154
168
  if (!parsedViews.some((v) => v.id === "index")) {
155
169
  parsedViews.unshift({
156
- __: "element",
170
+ [c4._stage]: "parsed",
171
+ [c4._type]: "element",
157
172
  id: "index",
158
173
  title: "Landscape view",
159
174
  description: null,
160
175
  tags: null,
161
176
  links: null,
162
- customColorDefinitions,
163
177
  rules: [
164
178
  {
165
179
  include: [
@@ -178,11 +192,19 @@ export function buildModelData(docs) {
178
192
  );
179
193
  return {
180
194
  data: {
195
+ [c4._stage]: "parsed",
196
+ projectId,
181
197
  specification: {
182
- tags: Array.from(c4Specification.specs.tags),
198
+ tags: c4Specification.tags,
183
199
  elements: c4Specification.specs.elements,
184
- relationships: c4Specification.specs.relationships,
185
- deployments: c4Specification.specs.deployments
200
+ relationships: mapValues(c4Specification.specs.relationships, ({ notation, technology, ...style }) => ({
201
+ ...notation && { notation },
202
+ ...technology && { technology },
203
+ style
204
+ })),
205
+ deployments: c4Specification.specs.deployments,
206
+ ...metadataKeys.size > 0 && { metadataKeys: [...metadataKeys].sort(c4.compareNatural) },
207
+ customColors
186
208
  },
187
209
  elements,
188
210
  relations,
@@ -1,4 +1,4 @@
1
- import { ancestorsFqn, AsFqn } from "@likec4/core";
1
+ import { ancestorsFqn, Fqn } from "@likec4/core";
2
2
  import { MultiMap } from "@likec4/core/utils";
3
3
  import { isDefined, isTruthy } from "remeda";
4
4
  import {
@@ -46,7 +46,7 @@ export class DeploymentsIndex extends FqnIndex {
46
46
  if (!isTruthy(name)) {
47
47
  return [];
48
48
  }
49
- thisFqn = AsFqn(name, parentFqn);
49
+ thisFqn = Fqn(name, parentFqn);
50
50
  const desc = createAndSaveDescription(node, name, thisFqn);
51
51
  if (!parentFqn) {
52
52
  root.push(desc);
@@ -1,4 +1,4 @@
1
- import { type Fqn, type ProjectId } from '@likec4/core/types';
1
+ import { type ProjectId, Fqn } from '@likec4/core/types';
2
2
  import { DefaultWeakMap, MultiMap } from '@likec4/core/utils';
3
3
  import { type Stream, WorkspaceCache } from 'langium';
4
4
  import { type AstNodeDescriptionWithFqn, type LikeC4LangiumDocument, ast } from '../ast';
@@ -1,5 +1,5 @@
1
1
  import { invariant, nonNullable } from "@likec4/core";
2
- import { AsFqn } from "@likec4/core/types";
2
+ import { Fqn } from "@likec4/core/types";
3
3
  import { ancestorsFqn, compareNatural, DefaultWeakMap, MultiMap, sortNaturalByFqn } from "@likec4/core/utils";
4
4
  import {
5
5
  AstUtils,
@@ -89,7 +89,7 @@ export class FqnIndex extends ADisposable {
89
89
  });
90
90
  return map;
91
91
  }, new MultiMap());
92
- return uniqueByName(allchildren).sort((a, b) => compareNatural(a.name, b.name));
92
+ return uniqueByName(allchildren);
93
93
  })
94
94
  );
95
95
  }
@@ -102,7 +102,7 @@ export class FqnIndex extends ADisposable {
102
102
  });
103
103
  return map;
104
104
  }, new MultiMap());
105
- return uniqueByName(allchildren).sort((a, b) => compareNatural(a.name, b.name));
105
+ return uniqueByName(allchildren);
106
106
  })
107
107
  );
108
108
  }
@@ -125,7 +125,7 @@ export class FqnIndex extends ADisposable {
125
125
  children: new MultiMap(),
126
126
  descendants: new MultiMap()
127
127
  });
128
- const uniqueChildren = uniqueByName(children).sort((a, b) => compareNatural(a.name, b.name));
128
+ const uniqueChildren = uniqueByName(children);
129
129
  const uniqueDescendants = [...descendants.associations()].flatMap(([_name, descs]) => descs.length === 1 && !children.has(_name) ? descs : []);
130
130
  return [
131
131
  ...uniqueChildren,
@@ -158,7 +158,7 @@ export class FqnIndex extends ADisposable {
158
158
  const traverseNode = (el, parentFqn) => {
159
159
  let thisFqn;
160
160
  if (ast.isElement(el)) {
161
- thisFqn = AsFqn(el.name, parentFqn);
161
+ thisFqn = Fqn(el.name, parentFqn);
162
162
  const desc = createAndSaveDescription(el, el.name, thisFqn);
163
163
  if (!parentFqn) {
164
164
  root.push(desc);
@@ -211,7 +211,7 @@ export class FqnIndex extends ADisposable {
211
211
  }
212
212
  }
213
213
  function uniqueByName(multimap) {
214
- return [...multimap.associations()].flatMap(([_name, descs]) => descs.length === 1 ? descs : []);
214
+ return [...multimap.associations()].flatMap(([_name, descs]) => descs.length === 1 ? descs : []).sort((a, b) => compareNatural(a.name, b.name));
215
215
  }
216
216
  export class DocumentFqnIndex {
217
217
  constructor(_rootElements, _children, _descendants, _byfqn, projectId) {
@@ -1,12 +1,13 @@
1
- import type * as c4 from '@likec4/core';
2
- import { type ViewId, LikeC4Model } from '@likec4/core';
1
+ import * as c4 from '@likec4/core';
2
+ import { type ViewId } from '@likec4/core';
3
+ import { LikeC4Model } from '@likec4/core/model';
3
4
  import { type URI, Disposable } from 'langium';
4
5
  import { CancellationToken } from 'vscode-jsonrpc';
5
6
  import type { LikeC4Services } from '../module';
6
7
  import { ADisposable } from '../utils';
7
8
  type ModelParsedListener = (docs: URI[]) => void;
8
9
  export interface LikeC4ModelBuilder {
9
- parseModel(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<c4.ParsedLikeC4ModelData | null>;
10
+ parseModel(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<LikeC4Model.Parsed | null>;
10
11
  unsafeSyncBuildModel(projectId: c4.ProjectId): LikeC4Model.Computed;
11
12
  buildLikeC4Model(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<LikeC4Model.Computed>;
12
13
  computeView(viewId: ViewId, projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<c4.ComputedView | null>;
@@ -24,10 +25,15 @@ export declare class DefaultLikeC4ModelBuilder extends ADisposable implements Li
24
25
  * WARNING:
25
26
  * This method is internal and should to be called only when all documents are known to be parsed.
26
27
  * Otherwise, the model may be incomplete.
28
+ *
29
+ * To avoid circular dependencies, we do not resolve imports here.
27
30
  */
28
31
  private unsafeSyncParseModelData;
32
+ /**
33
+ * To avoid circular dependencies, first we parse all documents and then we join them.
34
+ */
29
35
  private unsafeSyncJoinedModelData;
30
- parseModel(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<c4.ParsedLikeC4ModelData | null>;
36
+ parseModel(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<LikeC4Model.Parsed | null>;
31
37
  private previousViews;
32
38
  /**
33
39
  * WARNING:
@@ -1,7 +1,9 @@
1
+ import * as c4 from "@likec4/core";
1
2
  import {
2
- isScopedElementView,
3
- LikeC4Model
3
+ isScopedElementView
4
4
  } from "@likec4/core";
5
+ import { computeView } from "@likec4/core/compute-view";
6
+ import { LikeC4Model } from "@likec4/core/model";
5
7
  import { loggable } from "@likec4/log";
6
8
  import { deepEqual as eq } from "fast-equals";
7
9
  import {
@@ -69,6 +71,8 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
69
71
  * WARNING:
70
72
  * This method is internal and should to be called only when all documents are known to be parsed.
71
73
  * Otherwise, the model may be incomplete.
74
+ *
75
+ * To avoid circular dependencies, we do not resolve imports here.
72
76
  */
73
77
  unsafeSyncParseModelData(projectId) {
74
78
  const cache = this.cache;
@@ -85,13 +89,16 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
85
89
  return null;
86
90
  }
87
91
  log.debug`unsafeSyncBuildModelData, project ${projectId}`;
88
- return buildModelData(docs);
92
+ return buildModelData(projectId, docs);
89
93
  } catch (e) {
90
94
  logWarnError(e);
91
95
  return null;
92
96
  }
93
97
  });
94
98
  }
99
+ /**
100
+ * To avoid circular dependencies, first we parse all documents and then we join them.
101
+ */
95
102
  unsafeSyncJoinedModelData(projectId) {
96
103
  const cache = this.cache;
97
104
  const key = parsedModelCacheKey(projectId);
@@ -100,6 +107,7 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
100
107
  if (!result) {
101
108
  return null;
102
109
  }
110
+ let parsedData = result.data;
103
111
  if (result.imports.size > 0) {
104
112
  logger.debug`processing imports of ${projectId}`;
105
113
  const imports = [...result.imports.associations()].reduce((acc, [projectId2, fqns]) => {
@@ -115,12 +123,12 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
115
123
  }
116
124
  return acc;
117
125
  }, {});
118
- return {
126
+ parsedData = {
119
127
  ...result.data,
120
128
  imports
121
129
  };
122
130
  }
123
- return result.data;
131
+ return LikeC4Model.create(parsedData);
124
132
  });
125
133
  }
126
134
  async parseModel(projectId, cancelToken = CancellationToken.None) {
@@ -150,18 +158,13 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
150
158
  const cache = this.cache;
151
159
  const viewsCache = this.cache;
152
160
  return cache.get(computedModelCacheKey(projectId), () => {
153
- const parsed = this.unsafeSyncJoinedModelData(projectId);
154
- if (!parsed) {
161
+ const parsedModel = this.unsafeSyncJoinedModelData(projectId);
162
+ if (!parsedModel) {
155
163
  return LikeC4Model.EMPTY;
156
164
  }
157
- const {
158
- views: parsedViews,
159
- ...model
160
- } = parsed;
161
- const computeView = LikeC4Model.makeCompute(parsed);
162
165
  const allViews = [];
163
- for (const view of values(parsedViews)) {
164
- const result = computeView(view);
166
+ for (const view of values(parsedModel.$data.views)) {
167
+ const result = computeView(view, parsedModel);
165
168
  if (!result.isSuccess) {
166
169
  logger.warn(loggable(result.error));
167
170
  continue;
@@ -178,7 +181,8 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
178
181
  });
179
182
  this.previousViews = { ...this.previousViews, ...views };
180
183
  return LikeC4Model.create({
181
- ...model,
184
+ ...parsedModel.$data,
185
+ [c4._stage]: "computed",
182
186
  views
183
187
  });
184
188
  });
@@ -213,21 +217,20 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
213
217
  return null;
214
218
  }
215
219
  return cache.get(cacheKey, () => {
216
- const view = parsed.views[viewId];
220
+ const view = parsed.$data.views[viewId];
217
221
  if (!view) {
218
222
  log.warn`computeView: cant find view ${viewId}`;
219
223
  return null;
220
224
  }
221
225
  log.debug`computeView: ${viewId}`;
222
- const computeView = LikeC4Model.makeCompute(parsed);
223
- const result = computeView(view);
226
+ const result = computeView(view, parsed);
224
227
  if (!result.isSuccess) {
225
228
  logWarnError(result.error);
226
229
  return null;
227
230
  }
228
231
  let computedView = result.view;
229
232
  const allElementViews = pipe(
230
- parsed.views,
233
+ parsed.$data.views,
231
234
  values(),
232
235
  filter(isScopedElementView),
233
236
  filter((v) => v.id !== viewId),
@@ -1,5 +1,7 @@
1
1
  import type * as c4 from '@likec4/core';
2
- import type { Location } from 'vscode-languageserver-types';
2
+ import type { Cancellation } from 'langium';
3
+ import type { Location, Range } from 'vscode-languageserver-types';
4
+ import { URI } from 'vscode-uri';
3
5
  import type { ParsedAstElement, ParsedAstView, ParsedLikeC4LangiumDocument } from '../ast';
4
6
  import { ast } from '../ast';
5
7
  import type { LikeC4Services } from '../module';
@@ -14,7 +16,7 @@ export declare class LikeC4ModelLocator {
14
16
  private documents;
15
17
  getParsedElement(...args: [ast.Element] | [c4.Fqn] | [c4.Fqn, c4.ProjectId]): ParsedAstElement | null;
16
18
  locateElement(fqn: c4.Fqn, projectId?: c4.ProjectId | undefined): Location | null;
17
- locateDeploymentElement(fqn: c4.Fqn, projectId?: c4.ProjectId | undefined): Location | null;
19
+ locateDeploymentElement(deploymentFqn: c4.DeploymentFqn, projectId?: c4.ProjectId | undefined): Location | null;
18
20
  locateRelation(relationId: c4.RelationId, projectId?: c4.ProjectId): Location | null;
19
21
  locateViewAst(viewId: c4.ViewId, projectId?: c4.ProjectId | undefined): null | {
20
22
  doc: ParsedLikeC4LangiumDocument;
@@ -22,4 +24,10 @@ export declare class LikeC4ModelLocator {
22
24
  viewAst: ast.LikeC4View;
23
25
  };
24
26
  locateView(viewId: c4.ViewId, projectId?: c4.ProjectId): Location | null;
27
+ locateDocumentTags(documentUri: URI, cancelToken?: Cancellation.CancellationToken): Promise<Array<{
28
+ name: string;
29
+ color: string;
30
+ range: Range;
31
+ isSpecification: boolean;
32
+ }>>;
25
33
  }
@@ -1,10 +1,14 @@
1
- import { splitGlobalFqn } from "@likec4/core";
2
- import { AstUtils, GrammarUtils } from "langium";
3
- import { isString } from "remeda";
4
- import { ast } from "../ast.js";
1
+ import { ifilter, invariant, splitGlobalFqn, toArray } from "@likec4/core";
2
+ import { loggable } from "@likec4/log";
3
+ import { AstUtils, DocumentState, GrammarUtils } from "langium";
4
+ import { flatMap, isString, pipe } from "remeda";
5
+ import { ast, isLikeC4LangiumDocument } from "../ast.js";
6
+ import { logger as serverLogger } from "../logger.js";
5
7
  import { projectIdFrom } from "../utils/index.js";
8
+ import { MergedSpecification } from "./builder/MergedSpecification.js";
6
9
  const { findNodeForKeyword, findNodeForProperty } = GrammarUtils;
7
- const { getDocument } = AstUtils;
10
+ const { getDocument, streamAllContents } = AstUtils;
11
+ const logger = serverLogger.getChild("ModelLocator");
8
12
  export class LikeC4ModelLocator {
9
13
  constructor(services) {
10
14
  this.services = services;
@@ -63,10 +67,10 @@ export class LikeC4ModelLocator {
63
67
  range: docsegment.range
64
68
  };
65
69
  }
66
- locateDeploymentElement(fqn, projectId) {
67
- let [_projectId, _fqn] = splitGlobalFqn(fqn);
68
- _projectId ??= this.projects.ensureProjectId(projectId);
69
- const entry = this.deploymentsIndex.byFqn(_projectId, _fqn).head();
70
+ locateDeploymentElement(deploymentFqn, projectId) {
71
+ const _projectId = this.projects.ensureProjectId(projectId);
72
+ const fqn = deploymentFqn;
73
+ const entry = this.deploymentsIndex.byFqn(_projectId, fqn).head();
70
74
  const docsegment = entry?.nameSegment ?? entry?.selectionSegment;
71
75
  if (!entry || !docsegment) {
72
76
  return null;
@@ -92,6 +96,7 @@ export class LikeC4ModelLocator {
92
96
  }
93
97
  let targetNode = node.title ? findNodeForProperty(node.$cstNode, "title") : void 0;
94
98
  targetNode ??= node.kind ? findNodeForProperty(node.$cstNode, "kind") : void 0;
99
+ targetNode ??= findNodeForKeyword(node.$cstNode, "->");
95
100
  targetNode ??= findNodeForProperty(node.$cstNode, "target");
96
101
  targetNode ??= node.$cstNode;
97
102
  if (!targetNode) {
@@ -142,4 +147,55 @@ export class LikeC4ModelLocator {
142
147
  range: targetNode.range
143
148
  };
144
149
  }
150
+ async locateDocumentTags(documentUri, cancelToken) {
151
+ const doc = this.langiumDocuments.getDocument(documentUri);
152
+ if (!doc || !isLikeC4LangiumDocument(doc)) {
153
+ return [];
154
+ }
155
+ if (doc.state < DocumentState.Validated) {
156
+ logger.debug(`Waiting for document ${doc.uri.path} to be Validated`);
157
+ await this.services.shared.workspace.DocumentBuilder.waitUntil(DocumentState.Validated, doc.uri, cancelToken);
158
+ }
159
+ const projectId = projectIdFrom(doc);
160
+ logger.debug(`locate document tags for ${doc.uri.path} in project ${projectId}`);
161
+ try {
162
+ const tagSpecs = new MergedSpecification(this.documents(projectId).toArray()).tags;
163
+ logger.debug(`Assigned colors to tags`, { tagSpecs });
164
+ const tags = pipe(
165
+ streamAllContents(doc.parseResult.value),
166
+ ifilter((astNode) => ast.isTag(astNode) || ast.isTagRef(astNode)),
167
+ toArray(),
168
+ flatMap((tagRef) => {
169
+ let name;
170
+ let $cstNode;
171
+ try {
172
+ if (ast.isTag(tagRef)) {
173
+ name = tagRef.name;
174
+ $cstNode = tagRef.$cstNode;
175
+ } else {
176
+ name = tagRef.tag.$refText;
177
+ $cstNode = tagRef.tag.$refNode;
178
+ }
179
+ const specification = tagSpecs[name];
180
+ invariant(specification, `Tag ${name} not found in merged specification`);
181
+ invariant($cstNode, `Tag ${name} does not have a $cstNode`);
182
+ return {
183
+ name,
184
+ color: specification.color,
185
+ range: $cstNode.range,
186
+ isSpecification: ast.isTag(tagRef)
187
+ };
188
+ } catch (err) {
189
+ logger.warn(`Fail on tag ${name}`, { err });
190
+ return [];
191
+ }
192
+ })
193
+ );
194
+ logger.debug(`Found ${tags.length} tags in document ${doc.uri.path}`);
195
+ return tags;
196
+ } catch (e) {
197
+ logger.warn(loggable(e));
198
+ return [];
199
+ }
200
+ }
145
201
  }
@@ -1,3 +1,3 @@
1
1
  import type * as c4 from '@likec4/core';
2
2
  import { ast } from '../ast';
3
- export declare function parseWhereClause(astNode: ast.WhereExpression): c4.WhereOperator<string, string>;
3
+ export declare function parseWhereClause(astNode: ast.WhereExpression): c4.WhereOperator;
@@ -23,7 +23,7 @@ function parseParticipant(astNode) {
23
23
  export function parseWhereClause(astNode) {
24
24
  switch (true) {
25
25
  case ast.isWhereTagEqual(astNode): {
26
- const tag = astNode.value?.ref?.name;
26
+ const tag = astNode.value.tag.ref?.name;
27
27
  const participant = parseParticipant(astNode);
28
28
  invariant(tag, "Expected tag name");
29
29
  const tagOperator = { tag: parseEquals(astNode, tag) };