@likec4/language-server 1.8.1 → 1.10.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 (85) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +21 -0
  3. package/dist/browser.d.cts +22 -0
  4. package/dist/browser.d.mts +22 -0
  5. package/dist/browser.d.ts +22 -0
  6. package/dist/browser.mjs +19 -0
  7. package/dist/index.cjs +10 -0
  8. package/dist/index.d.cts +18 -0
  9. package/dist/index.d.mts +18 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.mjs +1 -0
  12. package/dist/likec4lib.cjs +961 -0
  13. package/dist/likec4lib.d.cts +6 -0
  14. package/dist/likec4lib.d.mts +6 -0
  15. package/dist/likec4lib.d.ts +6 -0
  16. package/dist/likec4lib.mjs +957 -0
  17. package/dist/model-graph/index.cjs +10 -0
  18. package/dist/model-graph/index.d.cts +79 -0
  19. package/dist/model-graph/index.d.mts +79 -0
  20. package/dist/model-graph/index.d.ts +79 -0
  21. package/dist/model-graph/index.mjs +1 -0
  22. package/dist/node.cjs +18 -0
  23. package/dist/node.d.cts +20 -0
  24. package/dist/node.d.mts +20 -0
  25. package/dist/node.d.ts +20 -0
  26. package/dist/node.mjs +16 -0
  27. package/dist/protocol.cjs +25 -0
  28. package/dist/protocol.d.cts +43 -0
  29. package/dist/protocol.d.mts +43 -0
  30. package/dist/protocol.d.ts +43 -0
  31. package/dist/protocol.mjs +17 -0
  32. package/dist/shared/language-server.CjFzaJwI.d.cts +1223 -0
  33. package/dist/shared/language-server.CtKHXJDD.d.ts +1223 -0
  34. package/dist/shared/language-server.D-84I33F.d.mts +1223 -0
  35. package/dist/shared/language-server.DBJJUUgF.mjs +5737 -0
  36. package/dist/shared/language-server.DtBRb9os.mjs +1656 -0
  37. package/dist/shared/language-server.DwyCJvXm.cjs +1669 -0
  38. package/dist/shared/language-server.JWkqVjGv.cjs +5748 -0
  39. package/package.json +36 -20
  40. package/src/ast.ts +48 -36
  41. package/src/browser.ts +0 -3
  42. package/src/elementRef.ts +1 -1
  43. package/src/formatting/LikeC4Formatter.ts +388 -0
  44. package/src/formatting/utils.ts +26 -0
  45. package/src/generated/ast.ts +170 -12
  46. package/src/generated/grammar.ts +1 -1
  47. package/src/generated-lib/icons.ts +1 -1
  48. package/src/like-c4.langium +49 -8
  49. package/src/likec4lib.ts +2 -3
  50. package/src/logger.ts +9 -1
  51. package/src/lsp/DocumentLinkProvider.ts +27 -15
  52. package/src/lsp/RenameProvider.ts +8 -0
  53. package/src/lsp/SemanticTokenProvider.ts +20 -2
  54. package/src/lsp/index.ts +1 -0
  55. package/src/model/fqn-computation.ts +33 -23
  56. package/src/model/fqn-index.ts +5 -21
  57. package/src/model/model-builder.ts +180 -112
  58. package/src/model/model-locator.ts +1 -1
  59. package/src/model/model-parser-where.ts +3 -2
  60. package/src/model/model-parser.ts +99 -39
  61. package/src/model-graph/LikeC4ModelGraph.ts +42 -21
  62. package/src/model-graph/compute-view/__test__/fixture.ts +16 -14
  63. package/src/model-graph/compute-view/compute.ts +110 -81
  64. package/src/model-graph/compute-view/predicates.ts +6 -8
  65. package/src/model-graph/dynamic-view/__test__/fixture.ts +1 -0
  66. package/src/model-graph/dynamic-view/compute.ts +98 -61
  67. package/src/model-graph/utils/buildElementNotations.ts +1 -1
  68. package/src/model-graph/utils/elementExpressionToPredicate.ts +1 -1
  69. package/src/model-graph/utils/sortNodes.ts +2 -6
  70. package/src/module.ts +21 -4
  71. package/src/protocol.ts +4 -5
  72. package/src/references/scope-computation.ts +10 -1
  73. package/src/references/scope-provider.ts +2 -1
  74. package/src/shared/NodeKindProvider.ts +73 -34
  75. package/src/test/setup.ts +3 -8
  76. package/src/test/testServices.ts +27 -7
  77. package/src/utils/graphlib.ts +11 -0
  78. package/src/validation/index.ts +2 -1
  79. package/src/validation/property-checks.ts +13 -1
  80. package/src/validation/specification.ts +3 -3
  81. package/src/view-utils/manual-layout.ts +1 -1
  82. package/src/view-utils/resolve-extended-views.ts +19 -10
  83. package/src/view-utils/resolve-relative-paths.ts +19 -24
  84. package/src/view-utils/view-hash.ts +1 -1
  85. package/src/reset.d.ts +0 -2
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ Color,
2
3
  ComputedEdge,
3
4
  ComputedElementView,
4
5
  EdgeId,
@@ -12,7 +13,7 @@ import type {
12
13
  RelationshipKind,
13
14
  RelationshipLineType,
14
15
  Tag,
15
- ThemeColor,
16
+ ViewID,
16
17
  ViewRulePredicate
17
18
  } from '@likec4/core'
18
19
  import {
@@ -29,7 +30,7 @@ import {
29
30
  parentFqn,
30
31
  whereOperatorAsPredicate
31
32
  } from '@likec4/core'
32
- import { first, flatMap, hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
33
+ import { filter, flatMap, hasAtLeast, isNonNull, isTruthy, map, omit, only, pipe, reduce, sort, unique } from 'remeda'
33
34
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
34
35
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
35
36
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
@@ -191,7 +192,7 @@ export class ComputeCtx {
191
192
  protected computeEdges(): ComputedEdge[] {
192
193
  return this.ctxEdges.map((e): ComputedEdge => {
193
194
  invariant(hasAtLeast(e.relations, 1), 'Edge must have at least one relation')
194
- const relations = e.relations.toSorted(compareRelations)
195
+ const relations = sort(e.relations, compareRelations)
195
196
  const source = e.source.id
196
197
  const target = e.target.id
197
198
 
@@ -204,67 +205,78 @@ export class ComputeCtx {
204
205
  relations: relations.map(r => r.id)
205
206
  }
206
207
 
207
- let relation:
208
- | Pick<Relation, 'title' | 'kind' | 'description' | 'technology' | 'color' | 'line' | 'head' | 'tail' | 'tags'>
209
- | {
210
- // TODO refactor with type-fest
211
- title: string
212
- description?: string | undefined
213
- technology?: string | undefined
214
- kind?: RelationshipKind | undefined
215
- color?: ThemeColor | undefined
216
- line?: RelationshipLineType | undefined
217
- head?: RelationshipArrowType | undefined
218
- tail?: RelationshipArrowType | undefined
219
- tags?: NonEmptyArray<Tag>
220
- }
221
- | undefined
222
- if (relations.length === 1) {
223
- relation = relations[0]
224
- } else {
225
- relation = relations.find(r => r.source === source && r.target === target)
226
- // relation ??= relations.toReversed().find(r => r.source === source || r.target === target)
227
- }
208
+ let relation: {
209
+ // TODO refactor with type-fest
210
+ title: string
211
+ description?: string | undefined
212
+ technology?: string | undefined
213
+ kind?: RelationshipKind | undefined
214
+ color?: Color | undefined
215
+ line?: RelationshipLineType | undefined
216
+ head?: RelationshipArrowType | undefined
217
+ tail?: RelationshipArrowType | undefined
218
+ tags?: NonEmptyArray<Tag>
219
+ navigateTo?: ViewID | undefined
220
+ } | undefined
221
+ relation = relations.length === 1 ? relations[0] : relations.find(r => r.source === source && r.target === target)
228
222
 
229
223
  // This edge represents mutliple relations
230
224
  // We use label if only it is the same for all relations
231
225
  if (!relation) {
232
- relation = relations.reduce((acc, r) => {
233
- if (r.color && acc.color !== r.color) {
234
- acc.color = undefined
235
- }
236
- if (r.kind && acc.kind !== r.kind) {
237
- acc.kind = undefined
238
- }
239
- if (r.head && acc.head !== r.head) {
240
- acc.head = undefined
241
- }
242
- if (r.tail && acc.tail !== r.tail) {
243
- acc.tail = undefined
244
- }
245
- if (r.line && acc.line !== r.line) {
246
- acc.line = undefined
247
- }
248
- if (r.description && acc.description !== r.description) {
249
- acc.description = undefined
250
- }
251
- if (r.technology && acc.technology !== r.technology) {
252
- acc.technology = undefined
253
- }
254
- if (isTruthy(r.title) && acc.title !== r.title) {
255
- acc.title = '[...]'
256
- }
257
- return acc
258
- }, {
259
- title: first(flatMap(relations, r => isTruthy(r.title) ? r.title : [])) ?? '[...]',
260
- description: first(flatMap(relations, r => isTruthy(r.description) ? r.description : [])),
261
- technology: first(flatMap(relations, r => isTruthy(r.technology) ? r.technology : [])),
262
- kind: first(flatMap(relations, r => isTruthy(r.kind) ? r.kind : [])),
263
- head: first(flatMap(relations, r => isTruthy(r.head) ? r.head : [])),
264
- tail: first(flatMap(relations, r => isTruthy(r.tail) ? r.tail : [])),
265
- color: first(flatMap(relations, r => isTruthy(r.color) ? r.color : [])),
266
- line: first(flatMap(relations, r => isTruthy(r.line) ? r.line : []))
267
- })
226
+ const allprops = pipe(
227
+ relations,
228
+ reduce((acc, r) => {
229
+ if (isTruthy(r.title) && !acc.title.includes(r.title)) {
230
+ acc.title.push(r.title)
231
+ }
232
+ if (isTruthy(r.description) && !acc.description.includes(r.description)) {
233
+ acc.description.push(r.description)
234
+ }
235
+ if (isTruthy(r.technology) && !acc.technology.includes(r.technology)) {
236
+ acc.technology.push(r.technology)
237
+ }
238
+ if (isTruthy(r.kind) && !acc.kind.includes(r.kind)) {
239
+ acc.kind.push(r.kind)
240
+ }
241
+ if (isTruthy(r.color) && !acc.color.includes(r.color)) {
242
+ acc.color.push(r.color)
243
+ }
244
+ if (isTruthy(r.line) && !acc.line.includes(r.line)) {
245
+ acc.line.push(r.line)
246
+ }
247
+ if (isTruthy(r.head) && !acc.head.includes(r.head)) {
248
+ acc.head.push(r.head)
249
+ }
250
+ if (isTruthy(r.tail) && !acc.tail.includes(r.tail)) {
251
+ acc.tail.push(r.tail)
252
+ }
253
+ if (isTruthy(r.navigateTo) && !acc.navigateTo.includes(r.navigateTo)) {
254
+ acc.navigateTo.push(r.navigateTo)
255
+ }
256
+ return acc
257
+ }, {
258
+ title: [] as string[],
259
+ description: [] as string[],
260
+ technology: [] as string[],
261
+ kind: [] as RelationshipKind[],
262
+ head: [] as RelationshipArrowType[],
263
+ tail: [] as RelationshipArrowType[],
264
+ color: [] as Color[],
265
+ line: [] as RelationshipLineType[],
266
+ navigateTo: [] as ViewID[]
267
+ })
268
+ )
269
+ relation = {
270
+ title: only(allprops.title) ?? '[...]',
271
+ description: only(allprops.description),
272
+ technology: only(allprops.technology),
273
+ kind: only(allprops.kind),
274
+ head: only(allprops.head),
275
+ tail: only(allprops.tail),
276
+ color: only(allprops.color),
277
+ line: only(allprops.line),
278
+ navigateTo: only(allprops.navigateTo)
279
+ }
268
280
  }
269
281
 
270
282
  const tags = unique(flatMap(relations, r => r.tags ?? []))
@@ -273,12 +285,13 @@ export class ComputeCtx {
273
285
  edge,
274
286
  this.getEdgeLabel(relation),
275
287
  isTruthy(relation.description) && { description: relation.description },
276
- isTruthy(relation.technology) && { description: relation.technology },
288
+ isTruthy(relation.technology) && { technology: relation.technology },
277
289
  isTruthy(relation.kind) && { kind: relation.kind },
278
290
  relation.color && { color: relation.color },
279
291
  relation.line && { line: relation.line },
280
292
  relation.head && { head: relation.head },
281
293
  relation.tail && { tail: relation.tail },
294
+ relation.navigateTo && { navigateTo: relation.navigateTo },
282
295
  hasAtLeast(tags, 1) && { tags }
283
296
  )
284
297
  })
@@ -348,29 +361,40 @@ export class ComputeCtx {
348
361
  }
349
362
  }
350
363
 
351
- protected excludeImplicit(...excludes: Element[]) {
352
- for (const el of excludes) {
353
- this.implicits.delete(el)
354
- }
355
- }
364
+ // protected excludeImplicit(...excludes: Element[]) {
365
+ // for (const el of excludes) {
366
+ // this.implicits.delete(el)
367
+ // }
368
+ // }
356
369
 
357
370
  protected excludeRelation(...relations: Relation[]) {
371
+ if (relations.length === 0) {
372
+ return
373
+ }
358
374
  const excludedImplicits = new Set<Element>()
359
- for (const relation of relations) {
360
- let edge
361
- while ((edge = this.ctxEdges.find(e => e.relations.includes(relation)))) {
362
- if (edge.relations.length === 1) {
375
+ const ctxEdges = pipe(
376
+ this.ctxEdges,
377
+ map(edge => {
378
+ const edgerelations = edge.relations.filter(r => !relations.includes(r))
379
+ if (edgerelations.length === 0) {
363
380
  excludedImplicits.add(edge.source)
364
381
  excludedImplicits.add(edge.target)
365
- this.ctxEdges.splice(this.ctxEdges.indexOf(edge), 1)
366
- continue
382
+ return null
367
383
  }
368
- edge.relations = edge.relations.filter(r => r !== relation)
369
- }
370
- }
384
+ if (edgerelations.length !== edge.relations.length) {
385
+ return {
386
+ ...edge,
387
+ relations: edgerelations
388
+ }
389
+ }
390
+ return edge
391
+ }),
392
+ filter(isNonNull)
393
+ )
371
394
  if (excludedImplicits.size === 0) {
372
395
  return
373
396
  }
397
+ this.ctxEdges = ctxEdges
374
398
  const remaining = this.includedElements
375
399
  if (remaining.size === 0) {
376
400
  this.implicits.clear()
@@ -537,19 +561,24 @@ export class ComputeCtx {
537
561
  return this
538
562
  }
539
563
  nonexhaustive(expr)
540
- }
564
+ }
541
565
 
542
- protected getEdgeLabel(relation: { title: String | undefined, technology?: String | undefined }): { label: String } | false {
543
- const labelParts: String[] = []
566
+ protected getEdgeLabel(
567
+ relation: {
568
+ title: string
569
+ technology?: string | undefined
570
+ }
571
+ ) {
572
+ const labelParts: string[] = []
544
573
 
545
- if(isTruthy(relation.title)) {
574
+ if (isTruthy(relation.title)) {
546
575
  labelParts.push(relation.title)
547
576
  }
548
577
 
549
- if(isTruthy(relation.technology)) {
578
+ if (isTruthy(relation.technology)) {
550
579
  labelParts.push(`[${relation.technology}]`)
551
580
  }
552
581
 
553
- return labelParts.length > 0 && { label: labelParts.join('\n') }
582
+ return labelParts.length > 0 ? { label: labelParts.join('\n') } : {}
554
583
  }
555
584
  }
@@ -1,6 +1,6 @@
1
1
  import type { Element, Relation } from '@likec4/core'
2
2
  import { Expr, isAncestor, nonexhaustive, parentFqn } from '@likec4/core'
3
- import { allPass, filter as remedaFilter, flatMap, isNullish as isNil, map, pipe } from 'remeda'
3
+ import { allPass, filter as remedaFilter, flatMap, isNullish as isNil, map, pipe, unique } from 'remeda'
4
4
  import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
5
5
  import type { ComputeCtx } from './compute'
6
6
 
@@ -306,25 +306,23 @@ function edgesIncomingExpr(this: ComputeCtx, expr: Expr.ElementExpression) {
306
306
  return this.graph.edgesBetween(sources, targets)
307
307
  }
308
308
 
309
- const filterEdges = (edges: ComputeCtx.Edge[], where?: RelationPredicateFn) => {
309
+ const filterEdges = (edges: ReadonlyArray<ComputeCtx.Edge>, where?: RelationPredicateFn) => {
310
310
  if (!where) {
311
- return edges
311
+ return edges as ComputeCtx.Edge[]
312
312
  }
313
313
  return pipe(
314
314
  edges,
315
315
  map(e => ({ ...e, relations: e.relations.filter(where) })),
316
316
  remedaFilter(e => e.relations.length > 0)
317
- )
317
+ ) as ComputeCtx.Edge[]
318
318
  }
319
319
 
320
320
  const filterRelations = (edges: ComputeCtx.Edge[], where?: RelationPredicateFn) => {
321
- if (!where) {
322
- return edges.flatMap(e => e.relations)
323
- }
324
321
  return pipe(
325
322
  edges,
326
323
  flatMap(e => e.relations),
327
- remedaFilter(where)
324
+ where ? remedaFilter(where) : Identity,
325
+ unique()
328
326
  )
329
327
  }
330
328
 
@@ -10,6 +10,7 @@ const emptyView = {
10
10
  description: null,
11
11
  tags: null,
12
12
  links: null,
13
+ customColorDefinitions: {},
13
14
  rules: []
14
15
  }
15
16
 
@@ -1,14 +1,16 @@
1
1
  import type {
2
+ Color,
2
3
  ComputedDynamicView,
3
4
  ComputedEdge,
4
5
  DynamicView,
6
+ DynamicViewStep,
5
7
  Element,
6
8
  NonEmptyArray,
7
9
  RelationID,
8
10
  RelationshipArrowType,
9
11
  RelationshipLineType,
10
12
  Tag,
11
- ThemeColor
13
+ ViewID
12
14
  } from '@likec4/core'
13
15
  import {
14
16
  ancestorsFqn,
@@ -17,12 +19,13 @@ import {
17
19
  DefaultLineStyle,
18
20
  DefaultRelationshipColor,
19
21
  isDynamicViewIncludeRule,
22
+ isDynamicViewParallelSteps,
20
23
  isViewRuleAutoLayout,
21
24
  nonNullable,
22
25
  parentFqn,
23
26
  StepEdgeId
24
27
  } from '@likec4/core'
25
- import { hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
28
+ import { filter, flatMap, hasAtLeast, isTruthy, map, omit, only, pipe, unique } from 'remeda'
26
29
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
27
30
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
28
31
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
@@ -33,17 +36,19 @@ import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
33
36
 
34
37
  export namespace DynamicViewComputeCtx {
35
38
  export interface Step {
39
+ id: StepEdgeId
36
40
  source: Element
37
41
  target: Element
38
42
  title: string | null
39
43
  description?: string
40
44
  technology?: string
41
- color?: ThemeColor
45
+ color?: Color
42
46
  line?: RelationshipLineType
43
47
  head?: RelationshipArrowType
44
48
  tail?: RelationshipArrowType
45
49
  relations: RelationID[]
46
50
  isBackward: boolean
51
+ navigateTo?: ViewID
47
52
  tags?: NonEmptyArray<Tag>
48
53
  }
49
54
  }
@@ -62,6 +67,47 @@ export class DynamicViewComputeCtx {
62
67
  protected graph: LikeC4ModelGraph
63
68
  ) {}
64
69
 
70
+ private addStep(
71
+ {
72
+ source: stepSource,
73
+ target: stepTarget,
74
+ title: stepTitle,
75
+ isBackward,
76
+ navigateTo: stepNavigateTo,
77
+ ...step
78
+ }: DynamicViewStep,
79
+ index: number,
80
+ parent?: number
81
+ ) {
82
+ const id = parent ? StepEdgeId(parent, index) : StepEdgeId(index)
83
+ const source = this.graph.element(stepSource)
84
+ const target = this.graph.element(stepTarget)
85
+
86
+ this.explicits.add(source)
87
+ this.explicits.add(target)
88
+
89
+ const {
90
+ title,
91
+ relations,
92
+ tags,
93
+ navigateTo: derivedNavigateTo
94
+ } = this.findRelations(source, target)
95
+
96
+ const navigateTo = isTruthy(stepNavigateTo) && stepNavigateTo !== this.view.id ? stepNavigateTo : derivedNavigateTo
97
+
98
+ this.steps.push({
99
+ id,
100
+ ...step,
101
+ source,
102
+ target,
103
+ title: stepTitle ?? title,
104
+ relations: relations ?? [],
105
+ isBackward: isBackward ?? false,
106
+ ...(navigateTo ? { navigateTo } : {}),
107
+ ...(tags ? { tags } : {})
108
+ })
109
+ }
110
+
65
111
  protected compute(): ComputedDynamicView {
66
112
  const {
67
113
  docUri: _docUri, // exclude docUri
@@ -70,32 +116,21 @@ export class DynamicViewComputeCtx {
70
116
  ...view
71
117
  } = this.view
72
118
 
73
- for (
74
- let {
75
- source: stepSource,
76
- target: stepTarget,
77
- title: stepTitle,
78
- isBackward,
79
- ...step
80
- } of viewSteps
81
- ) {
82
- const source = this.graph.element(stepSource)
83
- const target = this.graph.element(stepTarget)
84
-
85
- this.explicits.add(source)
86
- this.explicits.add(target)
87
-
88
- const { title, relations, tags } = this.findRelations(source, target)
89
-
90
- this.steps.push({
91
- ...step,
92
- source,
93
- target,
94
- title: isTruthy(stepTitle) ? stepTitle : title,
95
- relations: relations ?? [],
96
- isBackward: isBackward ?? false,
97
- ...(tags ? { tags } : {})
98
- })
119
+ let stepNum = 1
120
+ for (const step of viewSteps) {
121
+ if (isDynamicViewParallelSteps(step)) {
122
+ if (step.__parallel.length === 0) {
123
+ continue
124
+ }
125
+ if (step.__parallel.length === 1) {
126
+ this.addStep(step.__parallel[0]!, stepNum)
127
+ } else {
128
+ step.__parallel.forEach((s, i) => this.addStep(s, i + 1, stepNum))
129
+ }
130
+ } else {
131
+ this.addStep(step, stepNum)
132
+ }
133
+ stepNum++
99
134
  }
100
135
 
101
136
  for (const rule of rules) {
@@ -110,12 +145,10 @@ export class DynamicViewComputeCtx {
110
145
  const elements = [...this.explicits]
111
146
  const nodesMap = buildComputeNodes(elements)
112
147
 
113
- const edges = this.steps.map(({ source, target, relations, title, isBackward, ...step }, index) => {
148
+ const edges = this.steps.map(({ source, target, relations, title, isBackward, ...step }) => {
114
149
  const sourceNode = nonNullable(nodesMap.get(source.id), `Source node ${source.id} not found`)
115
150
  const targetNode = nonNullable(nodesMap.get(target.id), `Target node ${target.id} not found`)
116
- const stepNum = index + 1
117
151
  const edge: ComputedEdge = {
118
- id: StepEdgeId(stepNum),
119
152
  parent: commonAncestor(source.id, target.id),
120
153
  source: source.id,
121
154
  target: target.id,
@@ -182,49 +215,53 @@ export class DynamicViewComputeCtx {
182
215
  title: string | null
183
216
  tags: NonEmptyArray<Tag> | null
184
217
  relations: NonEmptyArray<RelationID> | null
218
+ navigateTo: ViewID | null
185
219
  } {
186
220
  const relationships = unique(this.graph.edgesBetween(source, target).flatMap(e => e.relations))
187
- const alltags = unique(relationships.flatMap(r => r.tags ?? []))
188
- const tags = hasAtLeast(alltags, 1) ? alltags : null
189
-
190
- const relations = hasAtLeast(relationships, 1) ? map(relationships, r => r.id) : null
191
221
  if (relationships.length === 0) {
192
222
  return {
193
223
  title: null,
194
- tags,
195
- relations
224
+ tags: null,
225
+ relations: null,
226
+ navigateTo: null
196
227
  }
197
228
  }
198
- let relation
199
- if (relationships.length === 1) {
200
- relation = relationships[0]
201
- } else {
202
- relation = relationships.find(r => r.source === source.id && r.target === target.id)
203
- }
229
+ const alltags = pipe(
230
+ relationships,
231
+ flatMap(r => r.tags),
232
+ filter(isTruthy),
233
+ unique()
234
+ )
235
+ const tags = hasAtLeast(alltags, 1) ? alltags : null
236
+ const relations = hasAtLeast(relationships, 1) ? map(relationships, r => r.id) : null
204
237
 
205
- if (relation && isTruthy(relation.title)) {
206
- return {
207
- title: relation.title,
208
- tags,
209
- relations
210
- }
211
- }
238
+ // Most closest relation
239
+ const relation = only(relationships) || relationships.find(r => r.source === source.id && r.target === target.id)
212
240
 
213
241
  // This edge represents mutliple relations
214
242
  // We use label if only it is the same for all relations
215
- const labels = unique(relationships.flatMap(r => (isTruthy(r.title) ? r.title : [])))
216
- if (labels.length === 1) {
217
- return {
218
- title: labels[0]!,
219
- tags,
220
- relations
221
- }
222
- }
243
+ const title = isTruthy(relation?.title) ? relation.title : pipe(
244
+ relationships,
245
+ map(r => r.title),
246
+ filter(isTruthy),
247
+ unique(),
248
+ only()
249
+ )
250
+
251
+ const navigateTo = !!relation?.navigateTo && relation.navigateTo !== this.view.id ? relation.navigateTo : pipe(
252
+ relationships,
253
+ map(r => r.navigateTo),
254
+ filter(isTruthy),
255
+ filter(v => v !== this.view.id),
256
+ unique(),
257
+ only()
258
+ )
223
259
 
224
260
  return {
225
- title: null,
261
+ title: title ?? null,
226
262
  tags,
227
- relations
263
+ relations,
264
+ navigateTo: navigateTo ?? null
228
265
  }
229
266
  }
230
267
  }
@@ -52,8 +52,8 @@ export function buildElementNotations(nodes: ComputedNode[]): ElementNotation[]
52
52
  }))
53
53
  ),
54
54
  sortBy(
55
- prop('title'),
56
55
  prop('shape'),
56
+ prop('title'),
57
57
  [
58
58
  n => n.kinds.length,
59
59
  'desc'
@@ -1,5 +1,5 @@
1
1
  import { Expr, nonexhaustive, parentFqn } from '@likec4/core'
2
- import { type Element, whereOperatorAsPredicate } from '@likec4/core/types'
2
+ import { type Element, whereOperatorAsPredicate } from '@likec4/core'
3
3
  import { isNullish } from 'remeda'
4
4
 
5
5
  type Predicate<T> = (x: T) => boolean
@@ -1,4 +1,3 @@
1
- import pkg from '@dagrejs/graphlib'
2
1
  import {
3
2
  compareByFqnHierarchically,
4
3
  compareRelations,
@@ -10,10 +9,7 @@ import {
10
9
  nonNullable
11
10
  } from '@likec4/core'
12
11
  import { difference, filter, map, pipe, sort, take } from 'remeda'
13
-
14
- // '@dagrejs/graphlib' is a CommonJS module
15
- // Here is a workaround to import it
16
- const { Graph, alg } = pkg
12
+ import { Graph, postorder } from '../../utils/graphlib'
17
13
 
18
14
  // side effect
19
15
  function sortChildren(nodes: readonly ComputedNode[]) {
@@ -92,7 +88,7 @@ export function sortNodes({
92
88
  map(n => n.id)
93
89
  )
94
90
  }
95
- const orderedIds = alg.postorder(g, sources).reverse() as Fqn[]
91
+ const orderedIds = postorder(g, sources).reverse() as Fqn[]
96
92
  const sorted = orderedIds.map(getNode)
97
93
  if (sorted.length < nodes.length) {
98
94
  const unsorted = difference(nodes, sorted)