@likec4/language-server 1.14.0 → 1.15.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 (43) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +1 -1
  3. package/dist/browser.d.cts +2 -2
  4. package/dist/browser.d.mts +2 -2
  5. package/dist/browser.d.ts +2 -2
  6. package/dist/browser.mjs +2 -2
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +2 -2
  12. package/dist/likec4lib.cjs +0 -1
  13. package/dist/likec4lib.mjs +0 -1
  14. package/dist/model-graph/index.cjs +1 -1
  15. package/dist/model-graph/index.mjs +1 -1
  16. package/dist/shared/{language-server.CbDa016p.cjs → language-server.80ITEDo5.cjs} +272 -64
  17. package/dist/shared/{language-server.CITj0XN3.cjs → language-server.BUtiWTKg.cjs} +188 -30
  18. package/dist/shared/{language-server.DFLaUdYu.mjs → language-server.DXC9g4_f.mjs} +274 -66
  19. package/dist/shared/{language-server.CO6aEDQm.d.mts → language-server.DfMwkd2l.d.mts} +46 -15
  20. package/dist/shared/{language-server.Cqyh6-9S.d.cts → language-server.U2piOAVt.d.cts} +46 -15
  21. package/dist/shared/{language-server.BI99piRy.d.ts → language-server.j-ShR6as.d.ts} +46 -15
  22. package/dist/shared/{language-server.DZYziEuF.mjs → language-server.zY53FGJE.mjs} +189 -31
  23. package/package.json +9 -9
  24. package/src/ast.ts +1 -0
  25. package/src/generated/ast.ts +103 -19
  26. package/src/generated/grammar.ts +1 -1
  27. package/src/generated-lib/icons.ts +0 -1
  28. package/src/like-c4.langium +32 -9
  29. package/src/lsp/CompletionProvider.ts +70 -2
  30. package/src/model/model-builder.ts +0 -1
  31. package/src/model/model-parser.ts +71 -20
  32. package/src/model-graph/compute-view/__test__/fixture.ts +45 -4
  33. package/src/model-graph/compute-view/compute.ts +223 -40
  34. package/src/model-graph/compute-view/predicates.ts +12 -6
  35. package/src/model-graph/utils/applyCustomElementProperties.ts +18 -4
  36. package/src/model-graph/utils/applyCustomRelationProperties.ts +2 -1
  37. package/src/model-graph/utils/applyViewRuleStyles.ts +30 -25
  38. package/src/model-graph/utils/buildComputeNodes.ts +69 -17
  39. package/src/model-graph/utils/elementExpressionToPredicate.ts +3 -1
  40. package/src/model-graph/utils/sortNodes.ts +11 -7
  41. package/src/references/scope-computation.ts +3 -2
  42. package/src/validation/index.ts +2 -2
  43. package/src/validation/specification.ts +4 -4
@@ -1,36 +1,59 @@
1
- import type {
2
- Color,
3
- ComputedEdge,
4
- ComputedElementView,
5
- EdgeId,
6
- Element,
7
- ElementPredicateExpression,
8
- ElementView,
9
- NonEmptyArray,
10
- Relation,
11
- RelationPredicateExpression,
12
- RelationshipArrowType,
13
- RelationshipKind,
14
- RelationshipLineType,
15
- Tag,
16
- ViewID,
17
- ViewRulePredicate
1
+ import {
2
+ type Color,
3
+ type ComputedEdge,
4
+ type ComputedElementView,
5
+ type ComputedNode,
6
+ type EdgeId,
7
+ type Element,
8
+ ElementKind,
9
+ type ElementPredicateExpression,
10
+ type ElementView,
11
+ type NodeId,
12
+ type NonEmptyArray,
13
+ type Relation,
14
+ type RelationPredicateExpression,
15
+ type RelationshipArrowType,
16
+ type RelationshipKind,
17
+ type RelationshipLineType,
18
+ type Tag,
19
+ type ViewID,
20
+ type ViewRuleGroup,
21
+ type ViewRulePredicate
18
22
  } from '@likec4/core'
19
23
  import {
20
- ancestorsFqn,
21
24
  commonAncestor,
25
+ commonHead,
22
26
  compareRelations,
23
27
  Expr,
24
28
  invariant,
25
29
  isAncestor,
26
30
  isScopedElementView,
27
31
  isViewRuleAutoLayout,
32
+ isViewRuleGroup,
28
33
  isViewRulePredicate,
29
34
  nonexhaustive,
30
- parentFqn,
35
+ nonNullable,
31
36
  whereOperatorAsPredicate
32
37
  } from '@likec4/core'
33
- import { filter, hasAtLeast, isNonNull, isTruthy, map, omit, only, pipe, reduce, sort, unique } from 'remeda'
38
+ import {
39
+ anyPass,
40
+ concat,
41
+ difference,
42
+ filter,
43
+ first,
44
+ hasAtLeast,
45
+ isNonNull,
46
+ isTruthy,
47
+ last,
48
+ map,
49
+ omit,
50
+ only,
51
+ pipe,
52
+ reduce,
53
+ reverse,
54
+ sort,
55
+ unique
56
+ } from 'remeda'
34
57
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
35
58
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
36
59
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
@@ -86,12 +109,78 @@ function isDirectEdge({ relations: [rel, ...tail], source, target }: ComputeCtx.
86
109
  return false
87
110
  }
88
111
 
112
+ export class NodesGroup {
113
+ static readonly kind = ElementKind.Group
114
+
115
+ static root() {
116
+ return new NodesGroup('@root' as NodeId, { title: null, groupRules: [] })
117
+ }
118
+
119
+ static is(node: ComputedNode) {
120
+ return node.kind === NodesGroup.kind
121
+ }
122
+
123
+ readonly explicits = new Set<Element>()
124
+ readonly implicits = new Set<Element>()
125
+
126
+ constructor(
127
+ public id: NodeId,
128
+ public viewRule: ViewRuleGroup,
129
+ public parent: NodeId | null = null
130
+ ) {
131
+ }
132
+
133
+ /**
134
+ * Add element explicitly
135
+ * Included even without relationships
136
+ */
137
+ addElement(...el: Element[]) {
138
+ for (const r of el) {
139
+ this.explicits.add(r)
140
+ this.implicits.add(r)
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Add element implicitly
146
+ * Included if only has relationships
147
+ */
148
+ addImplicit(...el: Element[]) {
149
+ for (const r of el) {
150
+ this.implicits.add(r)
151
+ }
152
+ }
153
+
154
+ excludeElement(...excludes: Element[]) {
155
+ for (const el of excludes) {
156
+ this.explicits.delete(el)
157
+ this.implicits.delete(el)
158
+ }
159
+ }
160
+
161
+ isEmpty() {
162
+ return this.explicits.size === 0 && this.implicits.size === 0
163
+ }
164
+ }
165
+
89
166
  export class ComputeCtx {
90
167
  // Intermediate state
91
168
  private explicits = new Set<Element>()
92
169
  private implicits = new Set<Element>()
93
170
  private ctxEdges = [] as ComputeCtx.Edge[]
94
171
 
172
+ /**
173
+ * Root group - not included in the groups
174
+ * But used to accumulate elements that are not in any group
175
+ */
176
+ private __rootGroup = NodesGroup.root()
177
+ private groups = [] as NodesGroup[]
178
+ private activeGroupStack = [] as NodesGroup[]
179
+
180
+ protected get activeGroup() {
181
+ return this.activeGroupStack[0] ?? this.__rootGroup
182
+ }
183
+
95
184
  public static elementView(view: ElementView, graph: LikeC4ModelGraph) {
96
185
  return new ComputeCtx(view, graph).compute()
97
186
  }
@@ -102,7 +191,6 @@ export class ComputeCtx {
102
191
  ) {}
103
192
 
104
193
  protected compute(): ComputedElementView {
105
- // reset ctx
106
194
  this.reset()
107
195
  const {
108
196
  docUri: _docUri, // exclude docUri
@@ -110,15 +198,34 @@ export class ComputeCtx {
110
198
  ...view
111
199
  } = this.view
112
200
 
113
- const viewPredicates = rules.filter(isViewRulePredicate)
201
+ const viewPredicates = rules.filter(anyPass([isViewRulePredicate, isViewRuleGroup])) as Array<
202
+ ViewRulePredicate | ViewRuleGroup
203
+ >
114
204
  if (this.root && viewPredicates.length == 0) {
115
205
  this.addElement(this.graph.element(this.root))
116
206
  }
117
207
  this.processPredicates(viewPredicates)
118
208
  this.removeRedundantImplicitEdges()
209
+ if (this.groups.length > 0) {
210
+ this.cleanGroupElements()
211
+ }
119
212
 
120
213
  const elements = [...this.includedElements]
121
- const nodesMap = buildComputeNodes(elements)
214
+ const nodesMap = buildComputeNodes(elements, this.groups)
215
+
216
+ const ancestorsOf = (node: ComputedNode) => {
217
+ const ancestors = [] as ComputedNode[]
218
+ let parent = node.parent
219
+ while (parent) {
220
+ const parentNode = nodesMap.get(parent)
221
+ if (!parentNode) {
222
+ break
223
+ }
224
+ ancestors.push(parentNode)
225
+ parent = parentNode.parent
226
+ }
227
+ return ancestors
228
+ }
122
229
 
123
230
  const edgesMap = new Map<EdgeId, ComputedEdge>()
124
231
  const edges = this.computeEdges()
@@ -128,30 +235,45 @@ export class ComputeCtx {
128
235
  const target = nodesMap.get(edge.target)
129
236
  invariant(source, `Source node ${edge.source} not found`)
130
237
  invariant(target, `Target node ${edge.target} not found`)
131
- while (edge.parent && !nodesMap.has(edge.parent)) {
132
- edge.parent = parentFqn(edge.parent)
133
- }
238
+ // These ancestors are reversed: from bottom to top
239
+ const sourceAncestors = ancestorsOf(source)
240
+ const targetAncestors = ancestorsOf(target)
241
+
242
+ const edgeParent = last(
243
+ commonHead(
244
+ reverse(sourceAncestors),
245
+ reverse(targetAncestors)
246
+ )
247
+ )
248
+ edge.parent = edgeParent?.id ?? null
134
249
  source.outEdges.push(edge.id)
135
250
  target.inEdges.push(edge.id)
136
251
  // Process edge source ancestors
137
- for (const sourceAncestor of ancestorsFqn(edge.source)) {
138
- if (sourceAncestor === edge.parent) {
252
+ for (const sourceAncestor of sourceAncestors) {
253
+ if (sourceAncestor === edgeParent) {
139
254
  break
140
255
  }
141
- nodesMap.get(sourceAncestor)?.outEdges.push(edge.id)
256
+ sourceAncestor.outEdges.push(edge.id)
142
257
  }
143
258
  // Process target hierarchy
144
- for (const targetAncestor of ancestorsFqn(edge.target)) {
145
- if (targetAncestor === edge.parent) {
259
+ for (const targetAncestor of targetAncestors) {
260
+ if (targetAncestor === edgeParent) {
146
261
  break
147
262
  }
148
- nodesMap.get(targetAncestor)?.inEdges.push(edge.id)
263
+ targetAncestor.inEdges.push(edge.id)
149
264
  }
150
265
  }
151
266
 
152
267
  // nodesMap sorted hierarchically,
153
268
  // but we need to keep the initial sort
154
- const initialSort = elements.flatMap(e => nodesMap.get(e.id) ?? [])
269
+ let initialSort = elements.map(e => nonNullable(nodesMap.get(e.id), `Node ${e.id} not found in nodesMap`))
270
+
271
+ if (this.groups.length > 0) {
272
+ initialSort = concat(
273
+ this.groups.map(g => nonNullable(nodesMap.get(g.id), `Node ${g.id} not found in nodesMap`)),
274
+ initialSort
275
+ )
276
+ }
155
277
 
156
278
  const nodes = applyCustomElementProperties(
157
279
  rules,
@@ -326,6 +448,7 @@ export class ComputeCtx {
326
448
  }
327
449
 
328
450
  protected addEdges(edges: ComputeCtx.Edge[]) {
451
+ const added = [] as ComputeCtx.Edge[]
329
452
  for (const e of edges) {
330
453
  if (!hasAtLeast(e.relations, 1)) {
331
454
  continue
@@ -335,10 +458,13 @@ export class ComputeCtx {
335
458
  )
336
459
  if (existing) {
337
460
  existing.relations = unique([...existing.relations, ...e.relations])
461
+ added.push(existing)
338
462
  continue
339
463
  }
464
+ added.push(e)
340
465
  this.ctxEdges.push(e)
341
466
  }
467
+ return added
342
468
  }
343
469
 
344
470
  /**
@@ -347,6 +473,14 @@ export class ComputeCtx {
347
473
  */
348
474
  protected addElement(...el: Element[]) {
349
475
  for (const r of el) {
476
+ if (!this.explicits.has(r)) {
477
+ this.activeGroup.addElement(r)
478
+ } else if (this.activeGroup !== this.__rootGroup) {
479
+ this.groups.forEach(g => {
480
+ g.implicits.delete(r)
481
+ })
482
+ this.activeGroup.addImplicit(r)
483
+ }
350
484
  this.explicits.add(r)
351
485
  this.implicits.add(r)
352
486
  }
@@ -359,7 +493,14 @@ export class ComputeCtx {
359
493
  protected addImplicit(...el: Element[]) {
360
494
  for (const r of el) {
361
495
  this.implicits.add(r)
496
+ // Remove implicits from other groups
497
+ if (this.activeGroup !== this.__rootGroup) {
498
+ this.groups.forEach(g => {
499
+ g.implicits.delete(r)
500
+ })
501
+ }
362
502
  }
503
+ this.activeGroup.addImplicit(...el)
363
504
  }
364
505
 
365
506
  protected excludeElement(...excludes: Element[]) {
@@ -368,14 +509,12 @@ export class ComputeCtx {
368
509
  this.explicits.delete(el)
369
510
  this.implicits.delete(el)
370
511
  }
512
+ this.__rootGroup.excludeElement(...excludes)
513
+ this.groups.forEach(g => {
514
+ g.excludeElement(...excludes)
515
+ })
371
516
  }
372
517
 
373
- // protected excludeImplicit(...excludes: Element[]) {
374
- // for (const el of excludes) {
375
- // this.implicits.delete(el)
376
- // }
377
- // }
378
-
379
518
  protected excludeRelation(...relations: Relation[]) {
380
519
  if (relations.length === 0) {
381
520
  return
@@ -407,11 +546,15 @@ export class ComputeCtx {
407
546
  const remaining = this.includedElements
408
547
  if (remaining.size === 0) {
409
548
  this.implicits.clear()
549
+ this.__rootGroup.implicits.clear()
550
+ this.groups.forEach(g => g.implicits.clear())
410
551
  return
411
552
  }
412
553
  for (const el of excludedImplicits) {
413
554
  if (!remaining.has(el)) {
414
555
  this.implicits.delete(el)
556
+ this.__rootGroup.implicits.delete(el)
557
+ this.groups.forEach(g => g.implicits.delete(el))
415
558
  }
416
559
  }
417
560
  }
@@ -420,6 +563,10 @@ export class ComputeCtx {
420
563
  this.explicits.clear()
421
564
  this.implicits.clear()
422
565
  this.ctxEdges = []
566
+ // Reset groups
567
+ this.__rootGroup = NodesGroup.root()
568
+ this.groups = []
569
+ this.activeGroupStack = []
423
570
  }
424
571
 
425
572
  // Filter out edges if there are edges between descendants
@@ -478,9 +625,45 @@ export class ComputeCtx {
478
625
  }, [] as ComputeCtx.Edge[])
479
626
  }
480
627
 
481
- protected processPredicates(viewRules: ViewRulePredicate[]): this {
628
+ protected cleanGroupElements() {
629
+ const unprocessed = new Set<Element>(difference(
630
+ [...this.includedElements],
631
+ [...this.__rootGroup.explicits]
632
+ ))
633
+ // Step 1 - Process explicits
634
+ for (const group of this.groups) {
635
+ const explicits = [...group.explicits]
636
+ group.explicits.clear()
637
+ for (const el of explicits) {
638
+ if (unprocessed.delete(el)) {
639
+ group.explicits.add(el)
640
+ }
641
+ }
642
+ }
643
+ // Step 2 - Process implicits
644
+ for (const group of this.groups) {
645
+ for (const el of group.implicits) {
646
+ if (unprocessed.delete(el)) {
647
+ group.explicits.add(el)
648
+ }
649
+ }
650
+ group.implicits.clear()
651
+ }
652
+ }
653
+
654
+ protected processPredicates(viewRules: Array<ViewRulePredicate | ViewRuleGroup>): this {
482
655
  // eslint-disable-next-line @typescript-eslint/no-this-alias
483
656
  for (const rule of viewRules) {
657
+ if (isViewRuleGroup(rule)) {
658
+ const parent = first(this.activeGroupStack)
659
+ const groupId = `@gr${this.groups.length + 1}` as NodeId
660
+ const group = new NodesGroup(groupId, rule, parent?.id ?? null)
661
+ this.groups.push(group)
662
+ this.activeGroupStack.unshift(group)
663
+ this.processPredicates(rule.groupRules)
664
+ this.activeGroupStack.shift()
665
+ continue
666
+ }
484
667
  const isInclude = 'include' in rule
485
668
  const exprs = rule.include ?? rule.exclude
486
669
  for (const expr of exprs) {
@@ -97,7 +97,9 @@ export function includeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr, w
97
97
  ]
98
98
 
99
99
  for (const el of children) {
100
- this.addEdges(this.graph.anyEdgesBetween(el, neighbours))
100
+ this.addEdges(this.graph.anyEdgesBetween(el, neighbours)).forEach(edge => {
101
+ this.addImplicit(edge.source, edge.target)
102
+ })
101
103
  }
102
104
 
103
105
  // If root has no children
@@ -331,8 +333,9 @@ export function includeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr, w
331
333
  if (edges.length === 0) {
332
334
  return
333
335
  }
334
- this.addEdges(edges)
335
- this.addImplicit(...edges.map(e => e.target))
336
+ this.addEdges(edges).forEach(edge => {
337
+ this.addImplicit(edge.target)
338
+ })
336
339
  }
337
340
  export function excludeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr, where?: RelationPredicateFn) {
338
341
  let relations = filterRelations(edgesIncomingExpr.call(this, expr.incoming), where)
@@ -375,8 +378,9 @@ export function includeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr, w
375
378
  if (edges.length === 0) {
376
379
  return
377
380
  }
378
- this.addEdges(edges)
379
- this.addImplicit(...edges.map(e => e.source))
381
+ this.addEdges(edges).forEach(edge => {
382
+ this.addImplicit(edge.source)
383
+ })
380
384
  }
381
385
  export function excludeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr, where?: RelationPredicateFn) {
382
386
  const relations = filterRelations(edgesOutgoingExpr.call(this, expr.outgoing), where)
@@ -484,7 +488,9 @@ export function includeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr, w
484
488
  if (expr.isBidirectional === true) {
485
489
  edges.push(...this.graph.edgesBetween(targets, sources))
486
490
  }
487
- this.addEdges(filterEdges(edges, where))
491
+ this.addEdges(filterEdges(edges, where)).forEach(edge => {
492
+ this.activeGroup.addImplicit(edge.source, edge.target)
493
+ })
488
494
  }
489
495
 
490
496
  export function excludeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr, where?: RelationPredicateFn) {
@@ -1,10 +1,24 @@
1
- import type { ComputedNode, ViewRule } from '@likec4/core'
2
- import { Expr } from '@likec4/core'
1
+ import { ComputedNode, type Expression, type ViewRule } from '@likec4/core'
2
+ import { Expr, isViewRuleGroup, isViewRulePredicate } from '@likec4/core'
3
3
  import { isEmpty, isNullish, omitBy } from 'remeda'
4
+ import { NodesGroup } from '../compute-view/compute'
4
5
  import { elementExprToPredicate } from './elementExpressionToPredicate'
5
6
 
7
+ export function flattenGroupRules<T extends Expression>(guard: (expr: Expression) => expr is T) {
8
+ return (rule: ViewRule): Array<T> => {
9
+ if (isViewRuleGroup(rule)) {
10
+ return rule.groupRules.flatMap(flattenGroupRules(guard))
11
+ }
12
+ if (isViewRulePredicate(rule)) {
13
+ return 'include' in rule ? rule.include.filter(guard) : []
14
+ }
15
+
16
+ return []
17
+ }
18
+ }
19
+
6
20
  export function applyCustomElementProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
7
- const rules = _rules.flatMap(r => ('include' in r ? r.include.filter(Expr.isCustomElement) : []))
21
+ const rules = _rules.flatMap(flattenGroupRules(Expr.isCustomElement))
8
22
  if (rules.length === 0) {
9
23
  return _nodes
10
24
  }
@@ -18,7 +32,7 @@ export function applyCustomElementProperties(_rules: ViewRule[], _nodes: Compute
18
32
  const notEmpty = !isEmpty(rest)
19
33
  const satisfies = elementExprToPredicate(expr)
20
34
  nodes.forEach((node, i) => {
21
- if (!satisfies(node)) {
35
+ if (ComputedNode.isNodesGroup(node) || !satisfies(node)) {
22
36
  return
23
37
  }
24
38
  if (notEmpty) {
@@ -1,6 +1,7 @@
1
1
  import type { ComputedEdge, ComputedNode, Element, ViewRule } from '@likec4/core'
2
2
  import { Expr, nonexhaustive } from '@likec4/core'
3
3
  import { isNullish, omitBy } from 'remeda'
4
+ import { flattenGroupRules } from './applyCustomElementProperties'
4
5
  import { elementExprToPredicate } from './elementExpressionToPredicate'
5
6
 
6
7
  function relationExpressionToPredicates(
@@ -39,7 +40,7 @@ export function applyCustomRelationProperties(
39
40
  nodes: ComputedNode[],
40
41
  _edges: Iterable<ComputedEdge>
41
42
  ): ComputedEdge[] {
42
- const rules = _rules.flatMap(r => ('include' in r ? r.include.filter(Expr.isCustomRelationExpr) : []))
43
+ const rules = _rules.flatMap(flattenGroupRules(Expr.isCustomRelationExpr))
43
44
  const edges = Array.from(_edges)
44
45
  if (rules.length === 0 || edges.length === 0) {
45
46
  return edges
@@ -1,6 +1,6 @@
1
- import type { ComputedNode, ViewRule } from '@likec4/core'
1
+ import { ComputedNode, type ViewRule } from '@likec4/core'
2
2
  import { Expr, isViewRuleStyle } from '@likec4/core'
3
- import { anyPass, filter, isDefined } from 'remeda'
3
+ import { anyPass, filter, forEach, isDefined, isNot, pipe } from 'remeda'
4
4
  import { elementExprToPredicate } from './elementExpressionToPredicate'
5
5
 
6
6
  type Predicate<T> = (x: T) => boolean
@@ -17,31 +17,36 @@ export function applyViewRuleStyles(_rules: ViewRule[], nodes: ComputedNode[]) {
17
17
  predicates.push(() => true)
18
18
  break
19
19
  }
20
- predicates.push(elementExprToPredicate(target))
20
+ predicates.push(elementExprToPredicate(target) as Predicate<ComputedNode>)
21
21
  }
22
- filter(nodes, anyPass(predicates)).forEach(n => {
23
- n.shape = rule.style.shape ?? n.shape
24
- n.color = rule.style.color ?? n.color
25
- if (isDefined(rule.style.icon)) {
26
- n.icon = rule.style.icon
27
- }
28
- if (isDefined(rule.notation)) {
29
- n.notation = rule.notation
30
- }
31
- let styleOverride: ComputedNode['style'] | undefined
32
- if (isDefined(rule.style.border)) {
33
- styleOverride = { border: rule.style.border }
34
- }
35
- if (isDefined(rule.style.opacity)) {
36
- styleOverride = { ...styleOverride, opacity: rule.style.opacity }
37
- }
38
- if (styleOverride) {
39
- n.style = {
40
- ...n.style,
41
- ...styleOverride
22
+ pipe(
23
+ nodes,
24
+ filter(isNot(ComputedNode.isNodesGroup)),
25
+ filter(anyPass(predicates)),
26
+ forEach(n => {
27
+ n.shape = rule.style.shape ?? n.shape
28
+ n.color = rule.style.color ?? n.color
29
+ if (isDefined(rule.style.icon)) {
30
+ n.icon = rule.style.icon
42
31
  }
43
- }
44
- })
32
+ if (isDefined(rule.notation)) {
33
+ n.notation = rule.notation
34
+ }
35
+ let styleOverride: ComputedNode['style'] | undefined
36
+ if (isDefined(rule.style.border)) {
37
+ styleOverride = { border: rule.style.border }
38
+ }
39
+ if (isDefined(rule.style.opacity)) {
40
+ styleOverride = { ...styleOverride, opacity: rule.style.opacity }
41
+ }
42
+ if (styleOverride) {
43
+ n.style = {
44
+ ...n.style,
45
+ ...styleOverride
46
+ }
47
+ }
48
+ })
49
+ )
45
50
  }
46
51
 
47
52
  return nodes
@@ -1,5 +1,13 @@
1
- import type { ComputedNode, Element, Fqn } from '@likec4/core'
2
- import { compareByFqnHierarchically, DefaultElementShape, DefaultThemeColor, parentFqn } from '@likec4/core'
1
+ import type { ComputedNode, Element, Fqn, NodeId } from '@likec4/core'
2
+ import {
3
+ compareByFqnHierarchically,
4
+ DefaultElementShape,
5
+ DefaultThemeColor,
6
+ ElementKind,
7
+ nonNullable,
8
+ parentFqn
9
+ } from '@likec4/core'
10
+ import { NodesGroup } from '../compute-view/compute'
3
11
 
4
12
  function updateDepthOfAncestors(node: ComputedNode, nodes: ReadonlyMap<Fqn, ComputedNode>) {
5
13
  let parentNd
@@ -14,48 +22,92 @@ function updateDepthOfAncestors(node: ComputedNode, nodes: ReadonlyMap<Fqn, Comp
14
22
  }
15
23
  }
16
24
 
17
- export function buildComputeNodes(elements: Iterable<Element>) {
25
+ export function buildComputeNodes(elements: Iterable<Element>, groups?: NodesGroup[]) {
26
+ const nodesMap = new Map<Fqn, ComputedNode>()
27
+
28
+ const elementToGroup = new Map<Fqn, NodeId>()
29
+
30
+ groups?.forEach(({ id, parent, viewRule, explicits }) => {
31
+ if (parent) {
32
+ nonNullable(nodesMap.get(parent), `Parent group node ${parent} not found`).children.push(id)
33
+ }
34
+ nodesMap.set(id, {
35
+ id,
36
+ parent,
37
+ kind: ElementKind.Group,
38
+ title: viewRule.title ?? '',
39
+ color: viewRule.color ?? 'muted',
40
+ shape: 'rectangle',
41
+ children: [],
42
+ inEdges: [],
43
+ outEdges: [],
44
+ level: 0,
45
+ depth: 0,
46
+ description: null,
47
+ technology: null,
48
+ tags: null,
49
+ links: null,
50
+ style: {
51
+ border: viewRule.border ?? 'dashed',
52
+ opacity: viewRule.opacity ?? 0
53
+ }
54
+ })
55
+ for (const e of explicits) {
56
+ elementToGroup.set(e.id, id)
57
+ }
58
+ })
59
+
18
60
  return (
19
61
  Array.from(elements)
20
62
  // Sort from Top to Bottom
21
63
  // So we can ensure that parent nodes are created before child nodes
22
64
  .sort(compareByFqnHierarchically)
23
- .reduce((map, { id, color, shape, style, ...el }) => {
65
+ .reduce((map, { id, color, shape, style, kind, title, ...el }) => {
24
66
  let parent = parentFqn(id)
25
67
  let level = 0
68
+ let parentNd: ComputedNode | undefined
26
69
  // Find the first ancestor that is already in the map
27
70
  while (parent) {
28
- const parentNd = map.get(parent)
71
+ parentNd = map.get(parent)
29
72
  if (parentNd) {
30
- // if parent has no children and we are about to add first one
31
- // we need to set its depth to 1
32
- if (parentNd.children.length == 0) {
33
- parentNd.depth = 1
34
- // go up the tree and update depth of all parents
35
- updateDepthOfAncestors(parentNd, map)
36
- }
37
- parentNd.children.push(id)
38
- level = parentNd.level + 1
39
73
  break
40
74
  }
41
75
  parent = parentFqn(parent)
42
76
  }
77
+ // If parent is not found in the map, check if it is in a group
78
+ if (!parentNd && elementToGroup.has(id)) {
79
+ parent = elementToGroup.get(id)!
80
+ parentNd = map.get(parent)!
81
+ }
82
+ if (parentNd) {
83
+ // if parent has no children and we are about to add first one
84
+ // we need to set its depth to 1
85
+ if (parentNd.children.length == 0) {
86
+ parentNd.depth = 1
87
+ // go up the tree and update depth of all parents
88
+ updateDepthOfAncestors(parentNd, map)
89
+ }
90
+ parentNd.children.push(id)
91
+ level = parentNd.level + 1
92
+ }
43
93
  const node: ComputedNode = {
44
- ...el,
45
94
  id,
46
95
  parent,
47
- level,
96
+ kind,
97
+ title,
48
98
  color: color ?? DefaultThemeColor,
49
99
  shape: shape ?? DefaultElementShape,
50
100
  children: [],
51
101
  inEdges: [],
52
102
  outEdges: [],
103
+ level,
104
+ ...el,
53
105
  style: {
54
106
  ...style
55
107
  }
56
108
  }
57
109
  map.set(id, node)
58
110
  return map
59
- }, new Map<Fqn, ComputedNode>()) as ReadonlyMap<Fqn, ComputedNode>
111
+ }, nodesMap) as ReadonlyMap<Fqn, ComputedNode>
60
112
  )
61
113
  }