@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.
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,430 @@
1
+ import type {
2
+ ComputedEdge,
3
+ ComputedElementView,
4
+ EdgeId,
5
+ Element,
6
+ ElementView,
7
+ Relation,
8
+ ViewRuleExpression
9
+ } from '@likec4/core'
10
+ import {
11
+ ancestorsFqn,
12
+ commonAncestor,
13
+ compareRelations,
14
+ Expr,
15
+ invariant,
16
+ isAncestor,
17
+ isStrictElementView,
18
+ isViewRuleAutoLayout,
19
+ isViewRuleExpression,
20
+ nonexhaustive,
21
+ parentFqn
22
+ } from '@likec4/core'
23
+ import { hasAtLeast, isTruthy, unique } from 'remeda'
24
+ import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
25
+ import { applyElementCustomProperties } from '../utils/applyElementCustomProperties'
26
+ import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
27
+ import { buildComputeNodes } from '../utils/buildComputeNodes'
28
+ import { sortNodes } from '../utils/sortNodes'
29
+ import {
30
+ excludeElementKindOrTag,
31
+ excludeElementRef,
32
+ excludeIncomingExpr,
33
+ excludeInOutExpr,
34
+ excludeOutgoingExpr,
35
+ excludeRelationExpr,
36
+ excludeWildcardRef,
37
+ includeCustomElement,
38
+ includeElementKindOrTag,
39
+ includeElementRef,
40
+ includeExpandedElementExpr,
41
+ includeIncomingExpr,
42
+ includeInOutExpr,
43
+ includeOutgoingExpr,
44
+ includeRelationExpr,
45
+ includeWildcardRef
46
+ } from './predicates'
47
+
48
+ // eslint-disable-next-line @typescript-eslint/no-namespace
49
+ export namespace ComputeCtx {
50
+ // Intermediate ComputedEdge
51
+ export interface Edge {
52
+ source: Element
53
+ target: Element
54
+ relations: Relation[]
55
+ }
56
+ }
57
+
58
+ function compareEdges(a: ComputeCtx.Edge, b: ComputeCtx.Edge) {
59
+ return compareRelations(
60
+ { source: a.source.id, target: a.target.id },
61
+ { source: b.source.id, target: b.target.id }
62
+ )
63
+ }
64
+
65
+ // If there is only one relation and it has same source and target as edge
66
+ function isDirectEdge({ relations: [rel, ...tail], source, target }: ComputeCtx.Edge) {
67
+ if (rel && tail.length === 0) {
68
+ return rel.source === source.id && rel.target === target.id
69
+ }
70
+ return false
71
+ }
72
+
73
+ export class ComputeCtx {
74
+ // Intermediate state
75
+ private explicits = new Set<Element>()
76
+ private implicits = new Set<Element>()
77
+ private ctxEdges = [] as ComputeCtx.Edge[]
78
+
79
+ public static elementView(view: ElementView, graph: LikeC4ModelGraph) {
80
+ return new ComputeCtx(view, graph).compute()
81
+ }
82
+
83
+ private constructor(
84
+ protected view: ElementView,
85
+ protected graph: LikeC4ModelGraph
86
+ ) {}
87
+
88
+ protected compute(): ComputedElementView {
89
+ // reset ctx
90
+ this.reset()
91
+ const { rules, ...view } = this.view
92
+
93
+ const viewPredicates = rules.filter(isViewRuleExpression)
94
+ if (this.root && viewPredicates.length == 0) {
95
+ this.addElement(this.graph.element(this.root))
96
+ }
97
+ this.processPredicates(viewPredicates)
98
+ this.removeRedundantImplicitEdges()
99
+
100
+ const elements = [...this.includedElements]
101
+ const nodesMap = buildComputeNodes(elements)
102
+
103
+ const edges = this.computedEdges.reduce((acc, edge) => {
104
+ const source = nodesMap.get(edge.source)
105
+ const target = nodesMap.get(edge.target)
106
+ invariant(source, `Source node ${edge.source} not found`)
107
+ invariant(target, `Target node ${edge.target} not found`)
108
+ while (edge.parent && !nodesMap.has(edge.parent)) {
109
+ edge.parent = parentFqn(edge.parent)
110
+ }
111
+ source.outEdges.push(edge.id)
112
+ target.inEdges.push(edge.id)
113
+ // Process edge source ancestors
114
+ for (const sourceAncestor of ancestorsFqn(edge.source)) {
115
+ if (sourceAncestor === edge.parent) {
116
+ break
117
+ }
118
+ nodesMap.get(sourceAncestor)?.outEdges.push(edge.id)
119
+ }
120
+ // Process target hierarchy
121
+ for (const targetAncestor of ancestorsFqn(edge.target)) {
122
+ if (targetAncestor === edge.parent) {
123
+ break
124
+ }
125
+ nodesMap.get(targetAncestor)?.inEdges.push(edge.id)
126
+ }
127
+ acc.push(edge)
128
+ return acc
129
+ }, [] as ComputedEdge[])
130
+
131
+ // nodesMap sorted hierarchically,
132
+ // but we need to keep the initial sort
133
+ const initialSort = elements.flatMap(e => nodesMap.get(e.id) ?? [])
134
+
135
+ const nodes = applyElementCustomProperties(
136
+ rules,
137
+ applyViewRuleStyles(
138
+ rules,
139
+ // Build graph and apply postorder sort
140
+ sortNodes({
141
+ nodes: initialSort,
142
+ edges
143
+ })
144
+ )
145
+ )
146
+
147
+ const edgesMap = new Map<EdgeId, ComputedEdge>(edges.map(e => [e.id, e]))
148
+
149
+ const sortedEdges = new Set([
150
+ ...nodes.flatMap(n => n.children.length === 0 ? n.outEdges.flatMap(id => edgesMap.get(id) ?? []) : []),
151
+ ...edges
152
+ ])
153
+
154
+ const autoLayoutRule = this.view.rules.findLast(isViewRuleAutoLayout)
155
+ return {
156
+ ...view,
157
+ autoLayout: autoLayoutRule?.autoLayout ?? 'TB',
158
+ nodes,
159
+ edges: [...sortedEdges]
160
+ }
161
+ }
162
+
163
+ protected get root() {
164
+ return isStrictElementView(this.view) ? this.view.viewOf : null
165
+ }
166
+
167
+ protected get computedEdges(): ComputedEdge[] {
168
+ return this.ctxEdges.map((e): ComputedEdge => {
169
+ invariant(hasAtLeast(e.relations, 1), 'Edge must have at least one relation')
170
+ const relations = [...e.relations].sort(compareRelations)
171
+ const source = e.source.id
172
+ const target = e.target.id
173
+
174
+ const edge: ComputedEdge = {
175
+ id: `${source}:${target}` as EdgeId,
176
+ parent: commonAncestor(source, target),
177
+ source,
178
+ target,
179
+ label: null,
180
+ relations: relations.map(r => r.id)
181
+ }
182
+
183
+ let relation
184
+ if (relations.length === 1) {
185
+ relation = relations[0]
186
+ } else {
187
+ relation = relations.find(r => r.source === source && r.target === target)
188
+ relation ??= relations.find(r => r.source === source || r.target === target)
189
+ }
190
+
191
+ // This edge represents mutliple relations
192
+ // We use label if only it is the same for all relations
193
+ if (!relation) {
194
+ const labels = unique(relations.flatMap(r => (isTruthy(r.title) ? r.title : [])))
195
+ if (hasAtLeast(labels, 1)) {
196
+ if (labels.length === 1) {
197
+ edge.label = labels[0]
198
+ } else {
199
+ edge.label = '[...]'
200
+ }
201
+ }
202
+ return edge
203
+ }
204
+
205
+ return Object.assign(
206
+ edge,
207
+ isTruthy(relation.title) && { label: relation.title },
208
+ relation.color && { color: relation.color },
209
+ relation.line && { line: relation.line },
210
+ relation.head && { head: relation.head },
211
+ relation.tail && { tail: relation.tail }
212
+ )
213
+ })
214
+ }
215
+
216
+ protected get includedElements() {
217
+ return new Set([
218
+ ...this.explicits,
219
+ ...this.ctxEdges.flatMap(e => [e.source, e.target])
220
+ ]) as ReadonlySet<Element>
221
+ }
222
+
223
+ protected get resolvedElements() {
224
+ return new Set([
225
+ ...this.explicits,
226
+ ...this.implicits,
227
+ ...this.ctxEdges.flatMap(e => [e.source, e.target])
228
+ ]) as ReadonlySet<Element>
229
+ }
230
+
231
+ protected get edges() {
232
+ return this.ctxEdges
233
+ }
234
+
235
+ protected addEdges(edges: ComputeCtx.Edge[]) {
236
+ for (const e of edges) {
237
+ if (!hasAtLeast(e.relations, 1)) {
238
+ continue
239
+ }
240
+ const existing = this.ctxEdges.find(
241
+ _e => _e.source.id === e.source.id && _e.target.id === e.target.id
242
+ )
243
+ if (existing) {
244
+ existing.relations = unique([...existing.relations, ...e.relations])
245
+ continue
246
+ }
247
+ this.ctxEdges.push(e)
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Add element explicitly
253
+ * Included even without relationships
254
+ */
255
+ protected addElement(...el: Element[]) {
256
+ for (const r of el) {
257
+ this.explicits.add(r)
258
+ this.implicits.add(r)
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Add element implicitly
264
+ * Included if only has relationships
265
+ */
266
+ protected addImplicit(...el: Element[]) {
267
+ for (const r of el) {
268
+ this.implicits.add(r)
269
+ }
270
+ }
271
+
272
+ protected excludeElement(...excludes: Element[]) {
273
+ for (const el of excludes) {
274
+ this.ctxEdges = this.ctxEdges.filter(e => e.source.id !== el.id && e.target.id !== el.id)
275
+ this.explicits.delete(el)
276
+ this.implicits.delete(el)
277
+ }
278
+ }
279
+
280
+ protected excludeImplicit(...excludes: Element[]) {
281
+ for (const el of excludes) {
282
+ this.implicits.delete(el)
283
+ }
284
+ }
285
+
286
+ protected excludeRelation(...relations: Relation[]) {
287
+ const excludedImplicits = new Set<Element>()
288
+ for (const relation of relations) {
289
+ let edge
290
+ while ((edge = this.ctxEdges.find(e => e.relations.includes(relation)))) {
291
+ if (edge.relations.length === 1) {
292
+ excludedImplicits.add(edge.source)
293
+ excludedImplicits.add(edge.target)
294
+ this.ctxEdges.splice(this.ctxEdges.indexOf(edge), 1)
295
+ continue
296
+ }
297
+ edge.relations = edge.relations.filter(r => r !== relation)
298
+ }
299
+ }
300
+ if (excludedImplicits.size === 0) {
301
+ return
302
+ }
303
+ const remaining = this.includedElements
304
+ if (remaining.size === 0) {
305
+ this.implicits.clear()
306
+ return
307
+ }
308
+ for (const el of excludedImplicits) {
309
+ if (!remaining.has(el)) {
310
+ this.implicits.delete(el)
311
+ }
312
+ }
313
+ }
314
+
315
+ protected reset() {
316
+ this.explicits.clear()
317
+ this.implicits.clear()
318
+ this.ctxEdges = []
319
+ }
320
+
321
+ // Filter out edges if there are edges between descendants
322
+ // i.e. remove implicit edges, derived from childs
323
+ protected removeRedundantImplicitEdges() {
324
+ const processedRelations = new Set<Relation>()
325
+
326
+ // Returns relations, that are not processed/included
327
+ const excludeProcessed = (relations: Relation[]) =>
328
+ relations.reduce((acc, rel) => {
329
+ if (!processedRelations.has(rel)) {
330
+ acc.push(rel)
331
+ processedRelations.add(rel)
332
+ }
333
+ return acc
334
+ }, [] as Relation[])
335
+
336
+ // Returns predicate
337
+ const isNestedEdgeOf = (parent: ComputeCtx.Edge) => {
338
+ const { source, target } = parent
339
+ // Checks if edge is between descendants of source and target of the parent edge
340
+ return (edge: ComputeCtx.Edge) => {
341
+ const isSameSource = source.id === edge.source.id
342
+ const isSameTarget = target.id === edge.target.id
343
+ if (isSameSource && isSameTarget) {
344
+ return true
345
+ }
346
+ const isSourceNested = isAncestor(source.id, edge.source.id)
347
+ const isTargetNested = isAncestor(target.id, edge.target.id)
348
+ return (
349
+ (isSourceNested && isTargetNested)
350
+ || (isSameSource && isTargetNested)
351
+ || (isSameTarget && isSourceNested)
352
+ )
353
+ }
354
+ }
355
+
356
+ // Sort edges from bottom to top (i.e. from more specific edges to implicit or between ancestors)
357
+ const edges = [...this.ctxEdges].sort(compareEdges).reverse()
358
+ this.ctxEdges = edges.reduce((acc, e) => {
359
+ const relations = excludeProcessed(e.relations)
360
+ if (relations.length === 0) {
361
+ return acc
362
+ }
363
+ // If there is an edge between descendants of current edge,
364
+ // then we don't add this edge
365
+ if (acc.length > 0 && acc.some(isNestedEdgeOf(e))) {
366
+ return acc
367
+ }
368
+ acc.push({
369
+ source: e.source,
370
+ target: e.target,
371
+ relations
372
+ })
373
+ return acc
374
+ }, [] as ComputeCtx.Edge[])
375
+ }
376
+
377
+ protected processPredicates(viewRules: ViewRuleExpression[]): this {
378
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
379
+ for (const rule of viewRules) {
380
+ const isInclude = 'include' in rule
381
+ const exprs = rule.include ?? rule.exclude
382
+ for (const expr of exprs) {
383
+ if (Expr.isCustomElement(expr)) {
384
+ if (isInclude) {
385
+ includeCustomElement.call(this, expr)
386
+ }
387
+ continue
388
+ }
389
+ if (Expr.isExpandedElementExpr(expr)) {
390
+ if (isInclude) {
391
+ includeExpandedElementExpr.call(this, expr)
392
+ }
393
+ continue
394
+ }
395
+ if (Expr.isElementKindExpr(expr) || Expr.isElementTagExpr(expr)) {
396
+ isInclude
397
+ ? includeElementKindOrTag.call(this, expr)
398
+ : excludeElementKindOrTag.call(this, expr)
399
+ continue
400
+ }
401
+ if (Expr.isElementRef(expr)) {
402
+ isInclude ? includeElementRef.call(this, expr) : excludeElementRef.call(this, expr)
403
+ continue
404
+ }
405
+ if (Expr.isWildcard(expr)) {
406
+ isInclude ? includeWildcardRef.call(this, expr) : excludeWildcardRef.call(this, expr)
407
+ continue
408
+ }
409
+ if (Expr.isIncoming(expr)) {
410
+ isInclude ? includeIncomingExpr.call(this, expr) : excludeIncomingExpr.call(this, expr)
411
+ continue
412
+ }
413
+ if (Expr.isOutgoing(expr)) {
414
+ isInclude ? includeOutgoingExpr.call(this, expr) : excludeOutgoingExpr.call(this, expr)
415
+ continue
416
+ }
417
+ if (Expr.isInOut(expr)) {
418
+ isInclude ? includeInOutExpr.call(this, expr) : excludeInOutExpr.call(this, expr)
419
+ continue
420
+ }
421
+ if (Expr.isRelation(expr)) {
422
+ isInclude ? includeRelationExpr.call(this, expr) : excludeRelationExpr.call(this, expr)
423
+ continue
424
+ }
425
+ nonexhaustive(expr)
426
+ }
427
+ }
428
+ return this
429
+ }
430
+ }
@@ -0,0 +1,33 @@
1
+ import { type ComputedElementView, type ElementView } from '@likec4/core'
2
+ import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
3
+ import { ComputeCtx } from './compute'
4
+
5
+ export function computeElementView(view: ElementView, graph: LikeC4ModelGraph) {
6
+ return ComputeCtx.elementView(view, graph)
7
+ }
8
+
9
+ type ComputeViewResult =
10
+ | {
11
+ isSuccess: true
12
+ view: ComputedElementView
13
+ }
14
+ | {
15
+ isSuccess: false
16
+ error: Error
17
+ view: undefined
18
+ }
19
+
20
+ export function computeView(view: ElementView, graph: LikeC4ModelGraph): ComputeViewResult {
21
+ try {
22
+ return {
23
+ isSuccess: true,
24
+ view: computeElementView(view, graph)
25
+ }
26
+ } catch (e) {
27
+ return {
28
+ isSuccess: false,
29
+ error: e instanceof Error ? e : new Error(`Unknown error: ${e}`),
30
+ view: undefined
31
+ }
32
+ }
33
+ }