@likec4/language-server 1.2.2 → 1.3.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 (36) hide show
  1. package/package.json +19 -8
  2. package/src/ast.ts +2 -0
  3. package/src/generated/ast.ts +157 -123
  4. package/src/generated/grammar.ts +2 -2
  5. package/src/generated/module.ts +1 -1
  6. package/src/like-c4.langium +53 -34
  7. package/src/logger.ts +21 -7
  8. package/src/lsp/CompletionProvider.ts +7 -0
  9. package/src/lsp/SemanticTokenProvider.ts +78 -17
  10. package/src/lsp/index.ts +1 -0
  11. package/src/model/model-builder.ts +3 -39
  12. package/src/model/model-parser.ts +19 -4
  13. package/src/model-change/ModelChanges.ts +58 -53
  14. package/src/model-change/changeElementStyle.ts +5 -6
  15. package/src/model-change/saveManualLayout.ts +43 -0
  16. package/src/model-graph/LikeC4ModelGraph.ts +304 -0
  17. package/src/model-graph/compute-view/__test__/fixture.ts +438 -0
  18. package/src/model-graph/compute-view/compute.ts +430 -0
  19. package/src/model-graph/compute-view/index.ts +33 -0
  20. package/src/model-graph/compute-view/predicates.ts +404 -0
  21. package/src/model-graph/dynamic-view/__test__/fixture.ts +56 -0
  22. package/src/model-graph/dynamic-view/compute.ts +198 -0
  23. package/src/model-graph/dynamic-view/index.ts +29 -0
  24. package/src/model-graph/index.ts +3 -0
  25. package/src/model-graph/utils/applyElementCustomProperties.ts +49 -0
  26. package/src/model-graph/utils/applyViewRuleStyles.ts +68 -0
  27. package/src/model-graph/utils/buildComputeNodes.ts +61 -0
  28. package/src/model-graph/utils/sortNodes.ts +105 -0
  29. package/src/module.ts +3 -0
  30. package/src/protocol.ts +3 -18
  31. package/src/references/scope-computation.ts +29 -11
  32. package/src/references/scope-provider.ts +22 -16
  33. package/src/validation/view.ts +9 -4
  34. package/src/view-utils/manual-layout.ts +93 -0
  35. package/contrib/likec4.monarch.ts +0 -41
  36. package/src/lsp/DocumentLinkProvider.test.ts +0 -66
@@ -0,0 +1,404 @@
1
+ import type { Element } from '@likec4/core'
2
+ import { Expr, invariant, nonexhaustive, parentFqn } from '@likec4/core'
3
+ import type { Predicate } from 'rambdax'
4
+ import { isNullish as isNil } from 'remeda'
5
+ import type { ComputeCtx } from './compute'
6
+
7
+ export function includeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr) {
8
+ // Get the elements that are already in the Ctx before any mutations
9
+ // Because we need to add edges between them and the new elements
10
+ const currentElements = [...this.resolvedElements]
11
+
12
+ const elements = expr.isDescedants === true
13
+ ? this.graph.childrenOrElement(expr.element)
14
+ : [this.graph.element(expr.element)]
15
+
16
+ this.addElement(...elements)
17
+
18
+ if (elements.length > 1) {
19
+ this.addEdges(this.graph.edgesWithin(elements))
20
+ }
21
+
22
+ if (currentElements.length > 0 && elements.length > 0) {
23
+ for (const el of elements) {
24
+ this.addEdges(this.graph.anyEdgesBetween(el, currentElements))
25
+ }
26
+ }
27
+ }
28
+
29
+ export function excludeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr) {
30
+ const elements = expr.isDescedants === true
31
+ ? this.graph.children(expr.element)
32
+ : [this.graph.element(expr.element)]
33
+ this.excludeElement(...elements)
34
+ }
35
+
36
+ export function includeExpandedElementExpr(this: ComputeCtx, expr: Expr.ExpandedElementExpr) {
37
+ const currentElements = [...this.resolvedElements]
38
+
39
+ // Always add parent
40
+ const parent = this.graph.element(expr.expanded)
41
+ this.addElement(parent)
42
+ const anyEdgesBetween = this.graph.anyEdgesBetween(parent, currentElements)
43
+
44
+ this.addEdges(anyEdgesBetween)
45
+
46
+ const expanded = [] as Element[]
47
+
48
+ for (const el of this.graph.children(expr.expanded)) {
49
+ this.addImplicit(el)
50
+ if (anyEdgesBetween.length > 0) {
51
+ const edges = this.graph.anyEdgesBetween(el, currentElements)
52
+ if (edges.length > 0) {
53
+ this.addEdges(edges)
54
+ expanded.push(el)
55
+ }
56
+ }
57
+ }
58
+ if (expanded.length > 1) {
59
+ this.addEdges(this.graph.edgesWithin(expanded))
60
+ }
61
+ }
62
+
63
+ export function includeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr) {
64
+ const root = this.root
65
+ if (root) {
66
+ const currentElements = [...this.resolvedElements]
67
+ const _elRoot = this.graph.element(root)
68
+ this.addElement(_elRoot)
69
+
70
+ const children = this.graph.children(root)
71
+ const hasChildren = children.length > 0
72
+ if (hasChildren) {
73
+ this.addElement(...children)
74
+ this.addEdges(this.graph.edgesWithin(children))
75
+ } else {
76
+ children.push(_elRoot)
77
+ }
78
+
79
+ // All neighbours that may have relations with root or its children
80
+ const neighbours = [
81
+ ...currentElements,
82
+ ...this.graph.siblings(root),
83
+ ...this.graph.ancestors(root).flatMap(a => this.graph.siblings(a.id))
84
+ ]
85
+
86
+ for (const el of children) {
87
+ this.addEdges(this.graph.anyEdgesBetween(el, neighbours))
88
+ }
89
+
90
+ // If root has no children
91
+ if (!hasChildren) {
92
+ // Any edges with siblings?
93
+ const edgesWithSiblings = this.graph.anyEdgesBetween(_elRoot, this.graph.siblings(root))
94
+ if (edgesWithSiblings.length === 0) {
95
+ // If no edges with siblings, i.e. root is orphan
96
+ // Lets add parent for better view
97
+ const _parentId = parentFqn(root)
98
+ const parent = _parentId && this.graph.element(_parentId)
99
+ if (parent) {
100
+ this.addElement(parent)
101
+ }
102
+ }
103
+ }
104
+ } else {
105
+ // Take root elements
106
+ this.addElement(...this.graph.rootElements)
107
+ this.addEdges(this.graph.edgesWithin(this.graph.rootElements))
108
+ }
109
+ }
110
+
111
+ export function excludeWildcardRef(this: ComputeCtx, _expr: Expr.WildcardExpr) {
112
+ const root = this.root
113
+ if (root) {
114
+ this.excludeElement(
115
+ this.graph.element(root),
116
+ ...this.graph.children(root)
117
+ )
118
+ this.excludeRelation(
119
+ ...this.graph.internal(root),
120
+ ...this.graph.incoming(root),
121
+ ...this.graph.outgoing(root)
122
+ )
123
+ } else {
124
+ this.reset()
125
+ }
126
+ }
127
+
128
+ const asElementPredicate = (
129
+ expr: Expr.ElementKindExpr | Expr.ElementTagExpr
130
+ ): Predicate<Element> => {
131
+ if (expr.isEqual) {
132
+ if (Expr.isElementKindExpr(expr)) {
133
+ return e => e.kind === expr.elementKind
134
+ } else {
135
+ return ({ tags }) => !!tags && tags.includes(expr.elementTag)
136
+ }
137
+ } else {
138
+ if (Expr.isElementKindExpr(expr)) {
139
+ return e => e.kind !== expr.elementKind
140
+ } else {
141
+ return ({ tags }) => isNil(tags) || tags.length === 0 || !tags.includes(expr.elementTag)
142
+ }
143
+ }
144
+ }
145
+ export function includeElementKindOrTag(
146
+ this: ComputeCtx,
147
+ expr: Expr.ElementKindExpr | Expr.ElementTagExpr
148
+ ) {
149
+ const elements = this.graph.elements.filter(asElementPredicate(expr))
150
+ if (elements.length > 0) {
151
+ const currentElements = [...this.resolvedElements]
152
+ this.addElement(...elements)
153
+ this.addEdges(this.graph.edgesWithin(elements))
154
+ for (const el of elements) {
155
+ this.addEdges(this.graph.anyEdgesBetween(el, currentElements))
156
+ }
157
+ }
158
+ }
159
+
160
+ export function excludeElementKindOrTag(
161
+ this: ComputeCtx,
162
+ expr: Expr.ElementKindExpr | Expr.ElementTagExpr
163
+ ) {
164
+ const elements = this.graph.elements.filter(asElementPredicate(expr))
165
+ if (elements.length > 0) {
166
+ this.excludeElement(...elements)
167
+ }
168
+ }
169
+
170
+ function resolveNeighbours(this: ComputeCtx, expr: Expr.ElementExpression): Element[] {
171
+ if (Expr.isElementRef(expr)) {
172
+ return this.graph.ascendingSiblings(expr.element)
173
+ }
174
+ return this.root ? this.graph.ascendingSiblings(this.root) : this.graph.rootElements
175
+ }
176
+
177
+ function resolveElements(this: ComputeCtx, expr: Expr.ElementExpression): Element[] {
178
+ if (Expr.isWildcard(expr)) {
179
+ if (this.root) {
180
+ return [...this.graph.children(this.root), this.graph.element(this.root)]
181
+ } else {
182
+ return this.graph.rootElements
183
+ }
184
+ }
185
+ if (Expr.isElementKindExpr(expr)) {
186
+ return this.graph.elements.filter(el => {
187
+ if (expr.isEqual) {
188
+ return el.kind === expr.elementKind
189
+ }
190
+ return el.kind !== expr.elementKind
191
+ })
192
+ }
193
+ if (Expr.isElementTagExpr(expr)) {
194
+ return this.graph.elements.filter(el => {
195
+ const tags = el.tags
196
+ if (expr.isEqual) {
197
+ return !!tags && tags.includes(expr.elementTag)
198
+ }
199
+ return isNil(tags) || tags.length === 0 || !tags.includes(expr.elementTag)
200
+ })
201
+ }
202
+ if (Expr.isExpandedElementExpr(expr)) {
203
+ return [this.graph.element(expr.expanded)]
204
+ }
205
+
206
+ // Type guard
207
+ if (!Expr.isElementRef(expr)) {
208
+ return nonexhaustive(expr)
209
+ }
210
+
211
+ if (this.root === expr.element && expr.isDescedants !== true) {
212
+ return [...this.graph.children(this.root), this.graph.element(this.root)]
213
+ }
214
+
215
+ if (expr.isDescedants) {
216
+ return this.graph.childrenOrElement(expr.element)
217
+ } else {
218
+ return [this.graph.element(expr.element)]
219
+ }
220
+ }
221
+
222
+ // --------------------------------
223
+ // Incoming Expr
224
+
225
+ function edgesIncomingExpr(this: ComputeCtx, expr: Expr.ElementExpression) {
226
+ if (Expr.isWildcard(expr)) {
227
+ if (!this.root) {
228
+ return []
229
+ }
230
+ const sources = this.graph.ascendingSiblings(this.root)
231
+ const targets = [...this.graph.children(this.root), this.graph.element(this.root)]
232
+ return this.graph.edgesBetween(sources, targets)
233
+ }
234
+ const targets = resolveElements.call(this, expr)
235
+ if (targets.length === 0) {
236
+ return []
237
+ }
238
+ const currentElements = [...this.resolvedElements]
239
+ if (currentElements.length === 0) {
240
+ currentElements.push(...resolveNeighbours.call(this, expr))
241
+ }
242
+ return this.graph.edgesBetween(currentElements, targets)
243
+ }
244
+
245
+ export function includeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr) {
246
+ const edges = edgesIncomingExpr.call(this, expr.incoming)
247
+ this.addEdges(edges)
248
+ this.addImplicit(...edges.map(e => e.target))
249
+ }
250
+ export function excludeIncomingExpr(this: ComputeCtx, expr: Expr.IncomingExpr) {
251
+ const edges = edgesIncomingExpr.call(this, expr.incoming)
252
+ this.excludeRelation(...edges.flatMap(e => e.relations))
253
+ }
254
+
255
+ // --------------------------------
256
+ // Outgoing Expr
257
+
258
+ function edgesOutgoingExpr(this: ComputeCtx, expr: Expr.ElementExpression) {
259
+ if (Expr.isWildcard(expr)) {
260
+ if (!this.root) {
261
+ return []
262
+ }
263
+ const targets = this.graph.ascendingSiblings(this.root)
264
+ const sources = [...this.graph.children(this.root), this.graph.element(this.root)]
265
+ return this.graph.edgesBetween(sources, targets)
266
+ }
267
+ const sources = resolveElements.call(this, expr)
268
+ if (sources.length === 0) {
269
+ return []
270
+ }
271
+ const targets = [...this.resolvedElements]
272
+ if (targets.length === 0) {
273
+ targets.push(...resolveNeighbours.call(this, expr))
274
+ }
275
+ return this.graph.edgesBetween(sources, targets)
276
+ }
277
+
278
+ export function includeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr) {
279
+ const edges = edgesOutgoingExpr.call(this, expr.outgoing)
280
+ this.addEdges(edges)
281
+ this.addImplicit(...edges.map(e => e.source))
282
+ }
283
+ export function excludeOutgoingExpr(this: ComputeCtx, expr: Expr.OutgoingExpr) {
284
+ const edges = edgesOutgoingExpr.call(this, expr.outgoing)
285
+ this.excludeRelation(...edges.flatMap(e => e.relations))
286
+ }
287
+
288
+ // --------------------------------
289
+ // InOut Expr
290
+ type EdgePredicateResult = {
291
+ implicits: Element[]
292
+ edges: ComputeCtx.Edge[]
293
+ }
294
+
295
+ namespace EdgePredicateResult {
296
+ export const Empty: EdgePredicateResult = {
297
+ implicits: [],
298
+ edges: []
299
+ }
300
+ }
301
+ function edgesInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr): EdgePredicateResult {
302
+ if (Expr.isWildcard(expr.inout)) {
303
+ if (!this.root) {
304
+ return EdgePredicateResult.Empty
305
+ }
306
+ const neighbours = this.graph.ascendingSiblings(this.root)
307
+ return {
308
+ edges: this.graph.anyEdgesBetween(this.graph.element(this.root), neighbours),
309
+ implicits: []
310
+ }
311
+ }
312
+ const elements = resolveElements.call(this, expr.inout)
313
+ if (elements.length === 0) {
314
+ return EdgePredicateResult.Empty
315
+ }
316
+ const currentElements = [...this.resolvedElements]
317
+ if (currentElements.length === 0) {
318
+ currentElements.push(...resolveNeighbours.call(this, expr.inout))
319
+ }
320
+ return elements.reduce<EdgePredicateResult>((acc, el) => {
321
+ const edges = this.graph.anyEdgesBetween(el, currentElements)
322
+ if (edges.length > 0) {
323
+ acc.implicits.push(el)
324
+ acc.edges.push(...edges)
325
+ }
326
+ return acc
327
+ }, { implicits: [], edges: [] })
328
+ }
329
+
330
+ export function includeInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr) {
331
+ const { implicits, edges } = edgesInOutExpr.call(this, expr)
332
+ this.addEdges(edges)
333
+ this.addImplicit(...implicits)
334
+ }
335
+ export function excludeInOutExpr(this: ComputeCtx, expr: Expr.InOutExpr) {
336
+ const { edges } = edgesInOutExpr.call(this, expr)
337
+ this.excludeRelation(...edges.flatMap(e => e.relations))
338
+ }
339
+
340
+ /**
341
+ * Expand element to its children and itself, if it is the root (and not ".*")
342
+ * Example:
343
+ *
344
+ * view of api {
345
+ * include some -> api
346
+ * }
347
+ *
348
+ * Transform to:
349
+ *
350
+ * view of api {
351
+ * include some -> api.*, some -> api
352
+ * }
353
+ */
354
+ function resolveRelationExprElements(this: ComputeCtx, expr: Expr.ElementExpression) {
355
+ if (Expr.isElementRef(expr) && this.root === expr.element && expr.isDescedants !== true) {
356
+ return [...this.graph.children(expr.element), this.graph.element(expr.element)]
357
+ }
358
+ return resolveElements.call(this, expr)
359
+ }
360
+
361
+ export function includeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr) {
362
+ let sources, targets
363
+ if (Expr.isWildcard(expr.source) && !Expr.isWildcard(expr.target)) {
364
+ sources = resolveNeighbours.call(this, expr.target)
365
+ targets = resolveRelationExprElements.call(this, expr.target)
366
+ } else if (!Expr.isWildcard(expr.source) && Expr.isWildcard(expr.target)) {
367
+ sources = resolveRelationExprElements.call(this, expr.source)
368
+ targets = resolveNeighbours.call(this, expr.source)
369
+ } else {
370
+ sources = resolveRelationExprElements.call(this, expr.source)
371
+ targets = resolveRelationExprElements.call(this, expr.target)
372
+ }
373
+ const edges = this.graph.edgesBetween(sources, targets)
374
+ if (expr.isBidirectional === true) {
375
+ edges.push(...this.graph.edgesBetween(targets, sources))
376
+ }
377
+ this.addEdges(edges)
378
+ }
379
+
380
+ export function excludeRelationExpr(this: ComputeCtx, expr: Expr.RelationExpr) {
381
+ const sources = resolveRelationExprElements.call(this, expr.source)
382
+ const targets = resolveRelationExprElements.call(this, expr.target)
383
+ const edges = this.graph.edgesBetween(sources, targets)
384
+ if (expr.isBidirectional === true) {
385
+ edges.push(...this.graph.edgesBetween(targets, sources))
386
+ }
387
+
388
+ const relations = new Set(edges.flatMap(e => e.relations))
389
+ this.excludeRelation(...relations)
390
+ }
391
+
392
+ export function includeCustomElement(this: ComputeCtx, expr: Expr.CustomElementExpr) {
393
+ // Get the elements that are already in the Ctx before any mutations
394
+ // Because we need to add edges between them and the new elements
395
+ const currentElements = [...this.resolvedElements]
396
+
397
+ const el = this.graph.element(expr.custom.element)
398
+
399
+ this.addElement(el)
400
+
401
+ if (currentElements.length > 0) {
402
+ this.addEdges(this.graph.anyEdgesBetween(el, currentElements))
403
+ }
404
+ }
@@ -0,0 +1,56 @@
1
+ import type { DynamicViewRule, DynamicViewStep, Fqn, ViewID, ViewRuleExpression } from '@likec4/core'
2
+ import { partition } from 'remeda'
3
+ import { type FakeElementIds, fakeModel } from '../../compute-view/__test__/fixture'
4
+ import { computeDynamicView } from '../index'
5
+
6
+ const emptyView = {
7
+ __: 'dynamic' as const,
8
+ id: 'index' as ViewID,
9
+ title: null,
10
+ description: null,
11
+ tags: null,
12
+ links: null,
13
+ rules: []
14
+ }
15
+
16
+ type StepExpr = `${FakeElementIds} ${'->' | '<-'} ${FakeElementIds}`
17
+
18
+ export function $step(expr: StepExpr, title?: string): DynamicViewStep {
19
+ if (expr.includes(' -> ')) {
20
+ const [source, target] = expr.split(' -> ')
21
+ return {
22
+ source: source as Fqn,
23
+ target: target as Fqn,
24
+ title: title ?? null
25
+ }
26
+ }
27
+ if (expr.includes(' <- ')) {
28
+ const [target, source] = expr.split(' <- ')
29
+ return {
30
+ source: source as Fqn,
31
+ target: target as Fqn,
32
+ title: title ?? null,
33
+ isBackward: true
34
+ }
35
+ }
36
+ throw new Error(`Invalid step expression: ${expr}`)
37
+ }
38
+
39
+ export function compute(stepsAndRules: (DynamicViewStep | ViewRuleExpression)[]) {
40
+ const [steps, rules] = partition(stepsAndRules, (s): s is DynamicViewStep => 'source' in s)
41
+ const result = computeDynamicView(
42
+ {
43
+ ...emptyView,
44
+ steps,
45
+ rules: rules as DynamicViewRule[]
46
+ },
47
+ fakeModel
48
+ )
49
+ if (!result.isSuccess) {
50
+ throw result.error
51
+ }
52
+ return Object.assign(result.view, {
53
+ nodeIds: result.view.nodes.map((node) => node.id) as string[],
54
+ edgeIds: result.view.edges.map((edge) => edge.id) as string[]
55
+ })
56
+ }
@@ -0,0 +1,198 @@
1
+ import type { ComputedDynamicView, ComputedEdge, DynamicView, EdgeId, Element, RelationID } from '@likec4/core'
2
+ import {
3
+ ancestorsFqn,
4
+ commonAncestor,
5
+ DefaultArrowType,
6
+ DefaultLineStyle,
7
+ DefaultRelationshipColor,
8
+ invariant,
9
+ isCustomElement,
10
+ isDynamicViewIncludeRule,
11
+ isElementRef,
12
+ isExpandedElementExpr,
13
+ isViewRuleAutoLayout,
14
+ nonNullable,
15
+ parentFqn,
16
+ StepEdgeId
17
+ } from '@likec4/core'
18
+ import { isTruthy, unique } from 'remeda'
19
+ import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
20
+ import { applyElementCustomProperties } from '../utils/applyElementCustomProperties'
21
+ import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
22
+ import { buildComputeNodes } from '../utils/buildComputeNodes'
23
+
24
+ export namespace DynamicViewComputeCtx {
25
+ export interface Step {
26
+ source: Element
27
+ target: Element
28
+ title: string | null
29
+ relations: RelationID[]
30
+ isBackward: boolean
31
+ }
32
+ }
33
+
34
+ export class DynamicViewComputeCtx {
35
+ // Intermediate state
36
+ private explicits = new Set<Element>()
37
+ private steps = [] as DynamicViewComputeCtx.Step[]
38
+
39
+ public static compute(view: DynamicView, graph: LikeC4ModelGraph): ComputedDynamicView {
40
+ return new DynamicViewComputeCtx(view, graph).compute()
41
+ }
42
+
43
+ private constructor(
44
+ protected view: DynamicView,
45
+ protected graph: LikeC4ModelGraph
46
+ ) {}
47
+
48
+ protected compute(): ComputedDynamicView {
49
+ // reset ctx
50
+ const { rules, steps, ...view } = this.view
51
+
52
+ // const sources = new Set<Element>()
53
+ // const stepsStack = new Set<string>()
54
+
55
+ // const sourcesOf = new Map<Fqn, Set<Element>>()
56
+
57
+ for (let step of steps) {
58
+ const source = this.graph.element(step.source)
59
+ const target = this.graph.element(step.target)
60
+
61
+ this.explicits.add(source)
62
+ this.explicits.add(target)
63
+
64
+ const { title, relations } = this.findRelations(source, target)
65
+
66
+ this.steps.push({
67
+ source,
68
+ target,
69
+ title: step.title ?? title,
70
+ relations,
71
+ isBackward: step.isBackward ?? false
72
+ })
73
+ }
74
+
75
+ for (const rule of rules) {
76
+ if (isDynamicViewIncludeRule(rule)) {
77
+ for (const expr of rule.include) {
78
+ if (isElementRef(expr)) {
79
+ this.explicits.add(this.graph.element(expr.element))
80
+ continue
81
+ }
82
+ if (isExpandedElementExpr(expr)) {
83
+ this.explicits.add(this.graph.element(expr.expanded))
84
+ continue
85
+ }
86
+ if (isCustomElement(expr)) {
87
+ this.explicits.add(this.graph.element(expr.custom.element))
88
+ continue
89
+ }
90
+ console.warn('Unsupported include expression: ', expr)
91
+ }
92
+ }
93
+ }
94
+
95
+ const elements = [...this.explicits]
96
+ const nodesMap = buildComputeNodes(elements)
97
+
98
+ const edges = this.steps.map(({ source, target, relations, ...step }, index) => {
99
+ const sourceNode = nodesMap.get(source.id)
100
+ const targetNode = nodesMap.get(target.id)
101
+ invariant(sourceNode, `Source node ${source.id} not found`)
102
+ invariant(targetNode, `Target node ${target.id} not found`)
103
+ const stepNum = index + 1
104
+ const edge: ComputedEdge = {
105
+ id: StepEdgeId(stepNum),
106
+ parent: commonAncestor(source.id, target.id),
107
+ source: source.id,
108
+ target: target.id,
109
+ label: step.title,
110
+ relations,
111
+ color: DefaultRelationshipColor,
112
+ line: DefaultLineStyle,
113
+ head: DefaultArrowType
114
+ }
115
+ if (step.isBackward) {
116
+ edge.dir = 'back'
117
+ }
118
+
119
+ while (edge.parent && !nodesMap.has(edge.parent)) {
120
+ edge.parent = parentFqn(edge.parent)
121
+ }
122
+ sourceNode.outEdges.push(edge.id)
123
+ targetNode.inEdges.push(edge.id)
124
+ // Process edge source ancestors
125
+ for (const sourceAncestor of ancestorsFqn(edge.source)) {
126
+ if (sourceAncestor === edge.parent) {
127
+ break
128
+ }
129
+ nodesMap.get(sourceAncestor)?.outEdges.push(edge.id)
130
+ }
131
+ // Process target hierarchy
132
+ for (const targetAncestor of ancestorsFqn(edge.target)) {
133
+ if (targetAncestor === edge.parent) {
134
+ break
135
+ }
136
+ nodesMap.get(targetAncestor)?.inEdges.push(edge.id)
137
+ }
138
+ return edge
139
+ })
140
+
141
+ const nodes = applyElementCustomProperties(
142
+ rules,
143
+ applyViewRuleStyles(
144
+ rules,
145
+ // Keep order of elements
146
+ elements.map(e => nonNullable(nodesMap.get(e.id)))
147
+ )
148
+ )
149
+
150
+ const autoLayoutRule = rules.findLast(isViewRuleAutoLayout)
151
+
152
+ return {
153
+ ...view,
154
+ autoLayout: autoLayoutRule?.autoLayout ?? 'LR',
155
+ nodes,
156
+ edges
157
+ }
158
+ }
159
+
160
+ private findRelations(source: Element, target: Element): {
161
+ title: string | null
162
+ relations: RelationID[]
163
+ } {
164
+ const relationships = this.graph.edgesBetween(source, target).flatMap(e => e.relations)
165
+ const relations = unique(relationships.map(r => r.id))
166
+ if (relationships.length === 0) {
167
+ return { title: null, relations }
168
+ }
169
+ let relation
170
+ if (relationships.length === 1) {
171
+ relation = relationships[0]
172
+ } else {
173
+ relation = relationships.find(r => r.source === source.id && r.target === target.id)
174
+ }
175
+
176
+ if (relation && isTruthy(relation.title)) {
177
+ return {
178
+ title: relation.title,
179
+ relations
180
+ }
181
+ }
182
+
183
+ // This edge represents mutliple relations
184
+ // We use label if only it is the same for all relations
185
+ const labels = unique(relationships.flatMap(r => (isTruthy(r.title) ? r.title : [])))
186
+ if (labels.length === 1) {
187
+ return {
188
+ title: labels[0]!,
189
+ relations
190
+ }
191
+ }
192
+
193
+ return {
194
+ title: null,
195
+ relations
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,29 @@
1
+ import { type ComputedDynamicView, type DynamicView } from '@likec4/core'
2
+ import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
3
+ import { DynamicViewComputeCtx } from './compute'
4
+
5
+ type ComputeViewResult =
6
+ | {
7
+ isSuccess: true
8
+ view: ComputedDynamicView
9
+ }
10
+ | {
11
+ isSuccess: false
12
+ error: Error
13
+ view: undefined
14
+ }
15
+
16
+ export function computeDynamicView(view: DynamicView, graph: LikeC4ModelGraph): ComputeViewResult {
17
+ try {
18
+ return {
19
+ isSuccess: true,
20
+ view: DynamicViewComputeCtx.compute(view, graph)
21
+ }
22
+ } catch (e) {
23
+ return {
24
+ isSuccess: false,
25
+ error: e instanceof Error ? e : new Error(`Unknown error: ${e}`),
26
+ view: undefined
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ export * from './compute-view'
2
+ export * from './dynamic-view'
3
+ export * from './LikeC4ModelGraph'