@likec4/language-server 1.2.2 → 1.4.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.
- package/package.json +19 -8
- package/src/ast.ts +2 -0
- package/src/generated/ast.ts +157 -123
- package/src/generated/grammar.ts +2 -2
- package/src/generated/module.ts +1 -1
- package/src/like-c4.langium +53 -34
- package/src/logger.ts +21 -7
- package/src/lsp/CompletionProvider.ts +7 -0
- package/src/lsp/SemanticTokenProvider.ts +78 -17
- package/src/lsp/index.ts +1 -0
- package/src/model/model-builder.ts +3 -39
- package/src/model/model-parser.ts +19 -4
- package/src/model-change/ModelChanges.ts +58 -53
- package/src/model-change/changeElementStyle.ts +5 -6
- package/src/model-change/saveManualLayout.ts +43 -0
- package/src/model-graph/LikeC4ModelGraph.ts +304 -0
- package/src/model-graph/compute-view/__test__/fixture.ts +438 -0
- package/src/model-graph/compute-view/compute.ts +430 -0
- package/src/model-graph/compute-view/index.ts +33 -0
- package/src/model-graph/compute-view/predicates.ts +404 -0
- package/src/model-graph/dynamic-view/__test__/fixture.ts +56 -0
- package/src/model-graph/dynamic-view/compute.ts +198 -0
- package/src/model-graph/dynamic-view/index.ts +29 -0
- package/src/model-graph/index.ts +3 -0
- package/src/model-graph/utils/applyElementCustomProperties.ts +49 -0
- package/src/model-graph/utils/applyViewRuleStyles.ts +68 -0
- package/src/model-graph/utils/buildComputeNodes.ts +61 -0
- package/src/model-graph/utils/sortNodes.ts +105 -0
- package/src/module.ts +3 -0
- package/src/protocol.ts +3 -18
- package/src/references/scope-computation.ts +29 -11
- package/src/references/scope-provider.ts +22 -16
- package/src/validation/view.ts +9 -4
- package/src/view-utils/manual-layout.ts +93 -0
- package/contrib/likec4.monarch.ts +0 -41
- 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
|
+
}
|