@likec4/language-server 1.6.1 → 1.7.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 (51) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/package.json +23 -19
  3. package/src/Rpc.ts +1 -1
  4. package/src/ast.ts +34 -9
  5. package/src/{browser/index.ts → browser.ts} +4 -1
  6. package/src/generated/ast.ts +498 -152
  7. package/src/generated/grammar.ts +2 -2
  8. package/src/generated/module.ts +1 -1
  9. package/src/index.ts +1 -1
  10. package/src/like-c4.langium +116 -44
  11. package/src/logger.ts +76 -55
  12. package/src/lsp/DocumentLinkProvider.ts +1 -1
  13. package/src/lsp/DocumentSymbolProvider.ts +1 -1
  14. package/src/lsp/HoverProvider.ts +1 -1
  15. package/src/lsp/SemanticTokenProvider.ts +54 -26
  16. package/src/model/model-builder.ts +11 -8
  17. package/src/model/model-locator.ts +12 -25
  18. package/src/model/model-parser-where.ts +75 -0
  19. package/src/model/model-parser.ts +168 -68
  20. package/src/model-change/ModelChanges.ts +2 -3
  21. package/src/model-change/changeElementStyle.ts +4 -1
  22. package/src/model-change/changeViewLayout.ts +8 -8
  23. package/src/model-change/saveManualLayout.ts +4 -6
  24. package/src/model-graph/LikeC4ModelGraph.ts +50 -48
  25. package/src/model-graph/compute-view/__test__/fixture.ts +41 -16
  26. package/src/model-graph/compute-view/compute.ts +135 -69
  27. package/src/model-graph/compute-view/predicates.ts +232 -136
  28. package/src/model-graph/dynamic-view/__test__/fixture.ts +5 -1
  29. package/src/model-graph/dynamic-view/compute.ts +50 -41
  30. package/src/model-graph/utils/applyCustomElementProperties.ts +31 -29
  31. package/src/model-graph/utils/applyCustomRelationProperties.ts +52 -15
  32. package/src/model-graph/utils/elementExpressionToPredicate.ts +8 -3
  33. package/src/module.ts +4 -18
  34. package/src/{node/index.ts → node.ts} +1 -1
  35. package/src/protocol.ts +2 -2
  36. package/src/shared/NodeKindProvider.ts +4 -2
  37. package/src/test/setup.ts +13 -0
  38. package/src/test/testServices.ts +1 -1
  39. package/src/validation/dynamic-view-rule.ts +12 -12
  40. package/src/validation/index.ts +6 -6
  41. package/src/validation/relation.ts +1 -1
  42. package/src/validation/view-predicates/{custom-element-expr.ts → element-with.ts} +11 -10
  43. package/src/validation/view-predicates/expanded-element.ts +2 -10
  44. package/src/validation/view-predicates/incoming.ts +1 -1
  45. package/src/validation/view-predicates/index.ts +2 -2
  46. package/src/validation/view-predicates/outgoing.ts +1 -1
  47. package/src/validation/view-predicates/{custom-relation-expr.ts → relation-with.ts} +2 -2
  48. package/src/validation/view.ts +8 -9
  49. package/src/view-utils/manual-layout.ts +65 -72
  50. package/src/view-utils/resolve-relative-paths.ts +28 -17
  51. package/src/view-utils/view-hash.ts +33 -0
@@ -1,18 +1,35 @@
1
- import type { Element } from '@likec4/core'
2
- import { Expr, invariant, nonexhaustive, parentFqn } from '@likec4/core'
3
- import { isNullish as isNil } from 'remeda'
1
+ import type { Element, Relation } from '@likec4/core'
2
+ import { Expr, isAncestor, nonexhaustive, parentFqn } from '@likec4/core'
3
+ import { allPass, filter as remedaFilter, flatMap, isNullish as isNil, map, pipe } from 'remeda'
4
+ import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
4
5
  import type { ComputeCtx } from './compute'
5
6
 
6
7
  type Predicate<T> = (x: T) => boolean
7
-
8
- export function includeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr) {
8
+ export type ElementPredicateFn = Predicate<Element>
9
+ export type RelationPredicateFn = Predicate<Relation>
10
+ // // type ElementPredicate = Predicate<Element>
11
+ // type ElementPredicates<T> = T extends Element[] ? (x: T) => T : (T extends Element ? (x: T) => (Element | null) : never)
12
+ // type ElementPredicate = ElementPredicates<Element | Element[]>
13
+
14
+ const NoFilter: ElementPredicateFn = () => true
15
+ const Identity = <T>(x: T) => x
16
+ const filterBy = <T>(pred: Predicate<T>) => pred === NoFilter ? Identity : remedaFilter(pred)
17
+ const filterOne = <T>(pred: Predicate<T>) => pred === NoFilter ? Identity : (x: T) => pred(x) ? x : null
18
+
19
+ export function includeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr, where = NoFilter) {
9
20
  // Get the elements that are already in the Ctx before any mutations
10
21
  // Because we need to add edges between them and the new elements
11
22
  const currentElements = [...this.resolvedElements]
23
+ const filter = filterBy(where)
12
24
 
13
- const elements = expr.isDescedants === true
14
- ? this.graph.childrenOrElement(expr.element)
15
- : [this.graph.element(expr.element)]
25
+ const elements = filter(
26
+ expr.isDescedants === true
27
+ ? this.graph.childrenOrElement(expr.element)
28
+ : [this.graph.element(expr.element)]
29
+ )
30
+ if (elements.length === 0) {
31
+ return
32
+ }
16
33
 
17
34
  this.addElement(...elements)
18
35
 
@@ -27,89 +44,84 @@ export function includeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr) {
27
44
  }
28
45
  }
29
46
 
30
- export function excludeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr) {
31
- const elements = expr.isDescedants === true
32
- ? this.graph.children(expr.element)
33
- : [this.graph.element(expr.element)]
47
+ export function excludeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr, where = NoFilter) {
48
+ const elements = [...this.resolvedElements].filter(allPass([
49
+ elementExprToPredicate(expr),
50
+ where
51
+ ]))
34
52
  this.excludeElement(...elements)
35
53
  }
36
54
 
37
- export function includeExpandedElementExpr(this: ComputeCtx, expr: Expr.ExpandedElementExpr) {
38
- const currentElements = [...this.resolvedElements]
39
-
40
- // Always add parent
41
- const parent = this.graph.element(expr.expanded)
42
- this.addElement(parent)
43
- const anyEdgesBetween = this.graph.anyEdgesBetween(parent, currentElements)
44
-
45
- this.addEdges(anyEdgesBetween)
46
-
47
- const expanded = [] as Element[]
48
-
49
- for (const el of this.graph.children(expr.expanded)) {
50
- this.addImplicit(el)
51
- if (anyEdgesBetween.length > 0) {
52
- const edges = this.graph.anyEdgesBetween(el, currentElements)
53
- if (edges.length > 0) {
54
- this.addEdges(edges)
55
- expanded.push(el)
55
+ export function includeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr, where = NoFilter) {
56
+ const root = this.root
57
+ if (!root) {
58
+ // Take root elements
59
+ const elements = this.graph.rootElements.filter(where)
60
+ if (elements.length <= 0) {
61
+ return
62
+ }
63
+ const currentElements = [...this.resolvedElements]
64
+ this.addElement(...elements)
65
+ this.addEdges(this.graph.edgesWithin(elements))
66
+ if (currentElements.length > 0) {
67
+ for (const el of elements) {
68
+ this.addEdges(this.graph.anyEdgesBetween(el, currentElements))
56
69
  }
57
70
  }
71
+ return
58
72
  }
59
- if (expanded.length > 1) {
60
- this.addEdges(this.graph.edgesWithin(expanded))
61
- }
62
- }
63
73
 
64
- export function includeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr) {
65
- const root = this.root
66
- if (root) {
67
- const currentElements = [...this.resolvedElements]
68
- const _elRoot = this.graph.element(root)
74
+ const currentElements = [...this.resolvedElements]
75
+ const _elRoot = filterOne(where)(this.graph.element(root))
76
+ if (_elRoot) {
69
77
  this.addElement(_elRoot)
78
+ }
79
+ const filter = filterBy(where)
70
80
 
71
- const children = this.graph.children(root)
72
- const hasChildren = children.length > 0
73
- if (hasChildren) {
74
- this.addElement(...children)
75
- this.addEdges(this.graph.edgesWithin(children))
76
- } else {
77
- children.push(_elRoot)
78
- }
81
+ const children = filter(this.graph.children(root))
82
+ const hasChildren = children.length > 0
83
+ if (hasChildren) {
84
+ this.addElement(...children)
85
+ this.addEdges(this.graph.edgesWithin(children))
86
+ } else if (_elRoot) {
87
+ children.push(_elRoot)
88
+ }
79
89
 
80
- // All neighbours that may have relations with root or its children
81
- const neighbours = [
82
- ...currentElements,
90
+ // All neighbours that may have relations with root or its children
91
+ const neighbours = [
92
+ ...currentElements,
93
+ ...filter([
83
94
  ...this.graph.siblings(root),
84
95
  ...this.graph.ancestors(root).flatMap(a => this.graph.siblings(a.id))
85
- ]
86
-
87
- for (const el of children) {
88
- this.addEdges(this.graph.anyEdgesBetween(el, neighbours))
89
- }
90
-
91
- // If root has no children
92
- if (!hasChildren) {
93
- // Any edges with siblings?
94
- const edgesWithSiblings = this.graph.anyEdgesBetween(_elRoot, this.graph.siblings(root))
95
- if (edgesWithSiblings.length === 0) {
96
- // If no edges with siblings, i.e. root is orphan
97
- // Lets add parent for better view
98
- const _parentId = parentFqn(root)
99
- const parent = _parentId && this.graph.element(_parentId)
100
- if (parent) {
101
- this.addElement(parent)
102
- }
96
+ ])
97
+ ]
98
+
99
+ for (const el of children) {
100
+ this.addEdges(this.graph.anyEdgesBetween(el, neighbours))
101
+ }
102
+
103
+ // If root has no children
104
+ if (!hasChildren && _elRoot) {
105
+ // Any edges with siblings?
106
+ const edgesWithSiblings = this.graph.anyEdgesBetween(_elRoot, this.graph.siblings(root))
107
+ if (edgesWithSiblings.length === 0) {
108
+ // If no edges with siblings, i.e. root is orphan
109
+ // Lets add parent for better view
110
+ const _parentId = parentFqn(root)
111
+ const parent = _parentId && this.graph.element(_parentId)
112
+ if (parent && where(parent)) {
113
+ this.addElement(parent)
103
114
  }
104
115
  }
105
- } else {
106
- // Take root elements
107
- this.addElement(...this.graph.rootElements)
108
- this.addEdges(this.graph.edgesWithin(this.graph.rootElements))
109
116
  }
110
117
  }
111
118
 
112
- export function excludeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr) {
119
+ export function excludeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr, where = NoFilter) {
120
+ if (where !== NoFilter) {
121
+ const elements = [...this.resolvedElements].filter(where)
122
+ this.excludeElement(...elements)
123
+ return
124
+ }
113
125
  const root = this.root
114
126
  if (root) {
115
127
  this.excludeElement(
@@ -117,15 +129,56 @@ export function excludeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr) {
117
129
  ...this.graph.children(root)
118
130
  )
119
131
  this.excludeRelation(
120
- ...this.graph.internal(root),
121
- ...this.graph.incoming(root),
122
- ...this.graph.outgoing(root)
132
+ ...this.graph.connectedRelations(root)
123
133
  )
124
134
  } else {
125
135
  this.reset()
126
136
  }
127
137
  }
128
138
 
139
+ export function includeExpandedElementExpr(
140
+ this: ComputeCtx,
141
+ expr: Expr.ExpandedElementExpr,
142
+ where = NoFilter
143
+ ) {
144
+ const filter = filterBy(where)
145
+ const currentElements = [...this.resolvedElements]
146
+
147
+ // Always add parent
148
+ const parent = this.graph.element(expr.expanded)
149
+ if (where(parent)) {
150
+ this.addElement(parent)
151
+ const anyEdgesBetween = this.graph.anyEdgesBetween(parent, currentElements)
152
+ this.addEdges(anyEdgesBetween)
153
+ }
154
+
155
+ const expanded = [] as Element[]
156
+
157
+ for (const el of filter(this.graph.children(expr.expanded))) {
158
+ this.addImplicit(el)
159
+ const edges = this.graph.anyEdgesBetween(el, currentElements)
160
+ if (edges.length > 0) {
161
+ this.addEdges(edges)
162
+ expanded.push(el)
163
+ }
164
+ }
165
+ if (expanded.length > 1) {
166
+ this.addEdges(this.graph.edgesWithin(expanded))
167
+ }
168
+ }
169
+
170
+ export function excludeExpandedElementExpr(
171
+ this: ComputeCtx,
172
+ expr: Expr.ExpandedElementExpr,
173
+ where = NoFilter
174
+ ) {
175
+ const elements = [...this.resolvedElements].filter(allPass([
176
+ elementExprToPredicate(expr),
177
+ where
178
+ ]))
179
+ this.excludeElement(...elements)
180
+ }
181
+
129
182
  const asElementPredicate = (
130
183
  expr: Expr.ElementKindExpr | Expr.ElementTagExpr
131
184
  ): Predicate<Element> => {
@@ -145,9 +198,10 @@ const asElementPredicate = (
145
198
  }
146
199
  export function includeElementKindOrTag(
147
200
  this: ComputeCtx,
148
- expr: Expr.ElementKindExpr | Expr.ElementTagExpr
201
+ expr: Expr.ElementKindExpr | Expr.ElementTagExpr,
202
+ where = NoFilter
149
203
  ) {
150
- const elements = this.graph.elements.filter(asElementPredicate(expr))
204
+ const elements = this.graph.elements.filter(asElementPredicate(expr)).filter(where)
151
205
  if (elements.length > 0) {
152
206
  const currentElements = [...this.resolvedElements]
153
207
  this.addElement(...elements)
@@ -160,9 +214,10 @@ export function includeElementKindOrTag(
160
214
 
161
215
  export function excludeElementKindOrTag(
162
216
  this: ComputeCtx,
163
- expr: Expr.ElementKindExpr | Expr.ElementTagExpr
217
+ expr: Expr.ElementKindExpr | Expr.ElementTagExpr,
218
+ where = NoFilter
164
219
  ) {
165
- const elements = this.graph.elements.filter(asElementPredicate(expr))
220
+ const elements = [...this.resolvedElements].filter(asElementPredicate(expr)).filter(where)
166
221
  if (elements.length > 0) {
167
222
  this.excludeElement(...elements)
168
223
  }
@@ -236,21 +291,54 @@ function edgesIncomingExpr(this: ComputeCtx, expr: Expr.ElementExpression) {
236
291
  if (targets.length === 0) {
237
292
  return []
238
293
  }
239
- const currentElements = [...this.resolvedElements]
240
- if (currentElements.length === 0) {
241
- currentElements.push(...resolveNeighbours.call(this, expr))
294
+ let sources = [...this.resolvedElements]
295
+ if (Expr.isElementRef(expr) || Expr.isExpandedElementExpr(expr)) {
296
+ const exprElement = expr.element ?? expr.expanded
297
+ const isDescedants = expr.isDescedants ?? false
298
+ sources = sources.filter(el =>
299
+ // allow elements, that are not nested or are direct children
300
+ !isAncestor(exprElement, el.id) || (isDescedants && parentFqn(el.id) === exprElement)
301
+ )
242
302
  }
243
- return this.graph.edgesBetween(currentElements, targets)
303
+ if (sources.length === 0) {
304
+ sources = resolveNeighbours.call(this, expr)
305
+ }
306
+ return this.graph.edgesBetween(sources, targets)
244
307
  }
245
308
 
246
- export function includeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr) {
247
- const edges = edgesIncomingExpr.call(this, expr.incoming)
309
+ const filterEdges = (edges: ComputeCtx.Edge[], where?: RelationPredicateFn) => {
310
+ if (!where) {
311
+ return edges
312
+ }
313
+ return pipe(
314
+ edges,
315
+ map(e => ({ ...e, relations: e.relations.filter(where) })),
316
+ remedaFilter(e => e.relations.length > 0)
317
+ )
318
+ }
319
+
320
+ const filterRelations = (edges: ComputeCtx.Edge[], where?: RelationPredicateFn) => {
321
+ if (!where) {
322
+ return edges.flatMap(e => e.relations)
323
+ }
324
+ return pipe(
325
+ edges,
326
+ flatMap(e => e.relations),
327
+ remedaFilter(where)
328
+ )
329
+ }
330
+
331
+ export function includeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr, where?: RelationPredicateFn) {
332
+ const edges = filterEdges(edgesIncomingExpr.call(this, expr.incoming), where)
333
+ if (edges.length === 0) {
334
+ return
335
+ }
248
336
  this.addEdges(edges)
249
337
  this.addImplicit(...edges.map(e => e.target))
250
338
  }
251
- export function excludeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr) {
252
- const edges = edgesIncomingExpr.call(this, expr.incoming)
253
- this.excludeRelation(...edges.flatMap(e => e.relations))
339
+ export function excludeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr, where?: RelationPredicateFn) {
340
+ let relations = filterRelations(edgesIncomingExpr.call(this, expr.incoming), where)
341
+ this.excludeRelation(...relations)
254
342
  }
255
343
 
256
344
  // --------------------------------
@@ -269,21 +357,32 @@ function edgesOutgoingExpr(this: ComputeCtx, expr: Expr.ElementExpression) {
269
357
  if (sources.length === 0) {
270
358
  return []
271
359
  }
272
- const targets = [...this.resolvedElements]
360
+ let targets = [...this.resolvedElements]
361
+ if (Expr.isElementRef(expr) || Expr.isExpandedElementExpr(expr)) {
362
+ const sourceElement = expr.element ?? expr.expanded
363
+ const isDescedants = expr.isDescedants ?? false
364
+ targets = targets.filter(el =>
365
+ // allow elements, that are not nested or are direct children
366
+ !isAncestor(sourceElement, el.id) || (isDescedants && parentFqn(el.id) === sourceElement)
367
+ )
368
+ }
273
369
  if (targets.length === 0) {
274
- targets.push(...resolveNeighbours.call(this, expr))
370
+ targets = resolveNeighbours.call(this, expr)
275
371
  }
276
372
  return this.graph.edgesBetween(sources, targets)
277
373
  }
278
374
 
279
- export function includeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr) {
280
- const edges = edgesOutgoingExpr.call(this, expr.outgoing)
375
+ export function includeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr, where?: RelationPredicateFn) {
376
+ const edges = filterEdges(edgesOutgoingExpr.call(this, expr.outgoing), where)
377
+ if (edges.length === 0) {
378
+ return
379
+ }
281
380
  this.addEdges(edges)
282
381
  this.addImplicit(...edges.map(e => e.source))
283
382
  }
284
- export function excludeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr) {
285
- const edges = edgesOutgoingExpr.call(this, expr.outgoing)
286
- this.excludeRelation(...edges.flatMap(e => e.relations))
383
+ export function excludeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr, where?: RelationPredicateFn) {
384
+ const relations = filterRelations(edgesOutgoingExpr.call(this, expr.outgoing), where)
385
+ this.excludeRelation(...relations)
287
386
  }
288
387
 
289
388
  // --------------------------------
@@ -299,27 +398,36 @@ namespace EdgePredicateResult {
299
398
  edges: []
300
399
  }
301
400
  }
302
- function edgesInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr): EdgePredicateResult {
303
- if (Expr.isWildcard(expr.inout)) {
401
+ function edgesInOutExpr(this: ComputeCtx, { inout }: Expr.InOutExpr, where?: RelationPredicateFn): EdgePredicateResult {
402
+ if (Expr.isWildcard(inout)) {
304
403
  if (!this.root) {
305
404
  return EdgePredicateResult.Empty
306
405
  }
307
406
  const neighbours = this.graph.ascendingSiblings(this.root)
308
407
  return {
309
- edges: this.graph.anyEdgesBetween(this.graph.element(this.root), neighbours),
408
+ edges: filterEdges(this.graph.anyEdgesBetween(this.graph.element(this.root), neighbours), where),
310
409
  implicits: []
311
410
  }
312
411
  }
313
- const elements = resolveElements.call(this, expr.inout)
412
+ const elements = resolveElements.call(this, inout)
314
413
  if (elements.length === 0) {
315
414
  return EdgePredicateResult.Empty
316
415
  }
317
- const currentElements = [...this.resolvedElements]
416
+ let currentElements = [...this.resolvedElements]
417
+
418
+ if (Expr.isElementRef(inout) || Expr.isExpandedElementExpr(inout)) {
419
+ const exprElement = inout.element ?? inout.expanded
420
+ const isDescedants = inout.isDescedants ?? false
421
+ currentElements = currentElements.filter(el =>
422
+ // allow elements, that are not nested or are direct children
423
+ !isAncestor(exprElement, el.id) || (isDescedants && parentFqn(el.id) === exprElement)
424
+ )
425
+ }
318
426
  if (currentElements.length === 0) {
319
- currentElements.push(...resolveNeighbours.call(this, expr.inout))
427
+ currentElements = resolveNeighbours.call(this, inout)
320
428
  }
321
429
  return elements.reduce<EdgePredicateResult>((acc, el) => {
322
- const edges = this.graph.anyEdgesBetween(el, currentElements)
430
+ const edges = filterEdges(this.graph.anyEdgesBetween(el, currentElements), where)
323
431
  if (edges.length > 0) {
324
432
  acc.implicits.push(el)
325
433
  acc.edges.push(...edges)
@@ -328,13 +436,13 @@ function edgesInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr): EdgePredicateRe
328
436
  }, { implicits: [], edges: [] })
329
437
  }
330
438
 
331
- export function includeInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr) {
332
- const { implicits, edges } = edgesInOutExpr.call(this, expr)
439
+ export function includeInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr, where?: RelationPredicateFn) {
440
+ const { implicits, edges } = edgesInOutExpr.call(this, expr, where)
333
441
  this.addEdges(edges)
334
442
  this.addImplicit(...implicits)
335
443
  }
336
- export function excludeInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr) {
337
- const { edges } = edgesInOutExpr.call(this, expr)
444
+ export function excludeInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr, where?: RelationPredicateFn) {
445
+ const { edges } = edgesInOutExpr.call(this, expr, where)
338
446
  this.excludeRelation(...edges.flatMap(e => e.relations))
339
447
  }
340
448
 
@@ -356,10 +464,13 @@ function resolveRelationExprElements(this: ComputeCtx, expr: Expr.ElementExpress
356
464
  if (Expr.isElementRef(expr) && this.root === expr.element && expr.isDescedants !== true) {
357
465
  return [...this.graph.children(expr.element), this.graph.element(expr.element)]
358
466
  }
467
+ if (Expr.isExpandedElementExpr(expr) && this.root === expr.expanded) {
468
+ return [...this.graph.children(expr.expanded), this.graph.element(expr.expanded)]
469
+ }
359
470
  return resolveElements.call(this, expr)
360
471
  }
361
472
 
362
- export function includeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr) {
473
+ export function includeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr, where?: RelationPredicateFn) {
363
474
  let sources, targets
364
475
  if (Expr.isWildcard(expr.source) && !Expr.isWildcard(expr.target)) {
365
476
  sources = resolveNeighbours.call(this, expr.target)
@@ -375,35 +486,20 @@ export function includeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr) {
375
486
  if (expr.isBidirectional === true) {
376
487
  edges.push(...this.graph.edgesBetween(targets, sources))
377
488
  }
378
- this.addEdges(edges)
489
+ this.addEdges(filterEdges(edges, where))
379
490
  }
380
491
 
381
- export function excludeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr) {
382
- const sources = resolveRelationExprElements.call(this, expr.source)
383
- const targets = resolveRelationExprElements.call(this, expr.target)
384
- const edges = this.graph.edgesBetween(sources, targets)
385
- if (expr.isBidirectional === true) {
386
- edges.push(...this.graph.edgesBetween(targets, sources))
492
+ export function excludeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr, where?: RelationPredicateFn) {
493
+ const isSource = elementExprToPredicate(expr.source)
494
+ const isTarget = elementExprToPredicate(expr.target)
495
+ const satisfies = (edge: ComputeCtx.Edge) => {
496
+ let result = isSource(edge.source) && isTarget(edge.target)
497
+ if (!result && expr.isBidirectional) {
498
+ result = isSource(edge.target) && isTarget(edge.source)
499
+ }
500
+ return result
387
501
  }
388
-
389
- const relations = new Set(edges.flatMap(e => e.relations))
502
+ const edges = this.edges.filter(satisfies)
503
+ const relations = filterRelations(edges, where)
390
504
  this.excludeRelation(...relations)
391
505
  }
392
-
393
- export function includeCustomElement(this: ComputeCtx, expr: Expr.CustomElementExpr) {
394
- // Get the elements that are already in the Ctx before any mutations
395
- // Because we need to add edges between them and the new elements
396
- const currentElements = [...this.resolvedElements]
397
-
398
- const el = this.graph.element(expr.custom.element)
399
-
400
- this.addElement(el)
401
-
402
- if (currentElements.length > 0) {
403
- this.addEdges(this.graph.anyEdgesBetween(el, currentElements))
404
- }
405
- }
406
-
407
- export function includeCustomRelation(this: ComputeCtx, expr: Expr.CustomRelationExpr) {
408
- includeRelationExpr.call(this, expr.customRelation.relation)
409
- }
@@ -14,13 +14,16 @@ const emptyView = {
14
14
  }
15
15
 
16
16
  type StepExpr = `${FakeElementIds} ${'->' | '<-'} ${FakeElementIds}`
17
+ type StepProps = Omit<DynamicViewStep, 'source' | 'target' | 'isBackward'>
17
18
 
18
- export function $step(expr: StepExpr, title?: string): DynamicViewStep {
19
+ export function $step(expr: StepExpr, props?: string | Partial<StepProps>): DynamicViewStep {
20
+ const title = typeof props === 'string' ? props : props?.title
19
21
  if (expr.includes(' -> ')) {
20
22
  const [source, target] = expr.split(' -> ')
21
23
  return {
22
24
  source: source as Fqn,
23
25
  target: target as Fqn,
26
+ ...(typeof props === 'object' ? props : {}),
24
27
  title: title ?? null
25
28
  }
26
29
  }
@@ -29,6 +32,7 @@ export function $step(expr: StepExpr, title?: string): DynamicViewStep {
29
32
  return {
30
33
  source: source as Fqn,
31
34
  target: target as Fqn,
35
+ ...(typeof props === 'object' ? props : {}),
32
36
  title: title ?? null,
33
37
  isBackward: true
34
38
  }