@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,438 @@
1
+ import type {
2
+ BorderStyle,
3
+ ComputedView,
4
+ Element,
5
+ ElementExpression as C4ElementExpression,
6
+ ElementKind,
7
+ ElementShape,
8
+ Expression as C4Expression,
9
+ Fqn,
10
+ Relation,
11
+ RelationID,
12
+ Tag,
13
+ ThemeColor,
14
+ ViewID,
15
+ ViewRule,
16
+ ViewRuleExpression,
17
+ ViewRuleStyle
18
+ } from '@likec4/core'
19
+ import { pluck } from 'rambdax'
20
+ import { indexBy, isString, pick } from 'remeda'
21
+ import { LikeC4ModelGraph } from '../../LikeC4ModelGraph'
22
+ import { computeElementView } from '../index'
23
+
24
+ /**
25
+ ┌──────────────────────────────────────────────────┐
26
+ │ cloud │
27
+ │ ┌───────────────────────────────────────────┐ │
28
+ │ │ frontend │ │
29
+ ┏━━━━━━━━━━┓ │ │ ┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━┓ │ │ ┏━━━━━━━━━━━┓
30
+ ┃ ┃ │ │ ┃ ┃ ┃ ┃ │ │ ┃ ┃
31
+ ┃ customer ┃──┼──┼──▶┃ dashboard ┃ ┃ adminpanel ┃◀───┼───┼───┃ support ┃
32
+ ┃ ┃ │ │ ┃ ┃ ┃ ┃ │ │ ┃ ┃
33
+ ┗━━━━━━━━━━┛ │ │ ┗━━━━━━┳━━━━━━┛ ┗━━━━━━━━┳━━━━━━━┛ │ │ ┗━━━━━━━━━━━┛
34
+ │ └──────────┼───────────────────┼────────────┘ │
35
+ │ ├───────────────────┘ │
36
+ │ │ │
37
+ │ ┌──────────┼────────────────────────────────┐ │
38
+ │ │ ▼ backend │ │
39
+ │ │ ┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━┓ │ │
40
+ │ │ ┃ ┃ ┃ ┃ │ │
41
+ │ │ ┃ graphlql ┃──────▶┃ storage ┃ │ │
42
+ │ │ ┃ ┃ ┃ ┃ │ │
43
+ │ │ ┗━━━━━━━━━━━━━┛ ┗━━━━━━┳━━━━━━┛ │ │
44
+ │ └────────────────────────────────┼──────────┘ │
45
+ └───────────────────────────────────┼──────────────┘
46
+
47
+ ┌─────────┼─────────┐
48
+ │ amazon │ │
49
+ │ ▼ │
50
+ │ ┏━━━━━━━━━━━━━━┓ │
51
+ │ ┃ ┃ │
52
+ │ ┃ s3 ┃ │
53
+ │ ┃ ┃ │
54
+ │ ┗━━━━━━━━━━━━━━┛ │
55
+ └───────────────────┘
56
+
57
+ specification {
58
+ element actor
59
+ element system
60
+ element container
61
+ element component
62
+
63
+ tag old
64
+ }
65
+
66
+ model {
67
+
68
+ actor customer
69
+ actor support
70
+
71
+ system cloud {
72
+ container backend {
73
+ component graphql
74
+ component storage {
75
+ #old
76
+ }
77
+
78
+ graphql -> storage
79
+ }
80
+
81
+ container frontend {
82
+ component dashboard {
83
+ -> graphql
84
+ }
85
+ component adminPanel {
86
+ #old
87
+ -> graphql
88
+ }
89
+ }
90
+ }
91
+
92
+ customer -> dashboard
93
+ support -> adminPanel
94
+
95
+ system amazon {
96
+ component s3
97
+
98
+ cloud.backend.storage -> s3
99
+ }
100
+
101
+ }
102
+
103
+ */
104
+ const el = ({
105
+ id,
106
+ kind,
107
+ title,
108
+ style,
109
+ ...props
110
+ }: Partial<Omit<Element, 'id' | 'kind'>> & { id: string; kind: string }): Element => ({
111
+ id: id as Fqn,
112
+ kind: kind as ElementKind,
113
+ title: title ?? id,
114
+ description: null,
115
+ technology: null,
116
+ tags: null,
117
+ links: null,
118
+ style: {
119
+ ...style
120
+ },
121
+ ...props
122
+ })
123
+
124
+ export const fakeElements = {
125
+ 'customer': el({
126
+ id: 'customer',
127
+ kind: 'actor',
128
+ title: 'customer',
129
+ shape: 'person'
130
+ }),
131
+ 'support': el({
132
+ id: 'support',
133
+ kind: 'actor',
134
+ title: 'support',
135
+ shape: 'person'
136
+ }),
137
+ 'cloud': el({
138
+ id: 'cloud',
139
+ kind: 'system',
140
+ title: 'cloud'
141
+ }),
142
+ 'cloud.backend': el({
143
+ id: 'cloud.backend',
144
+ kind: 'container',
145
+ title: 'backend'
146
+ }),
147
+ 'cloud.frontend': el({
148
+ id: 'cloud.frontend',
149
+ kind: 'container',
150
+ title: 'frontend',
151
+ shape: 'browser'
152
+ }),
153
+ 'cloud.backend.graphql': el({
154
+ id: 'cloud.backend.graphql',
155
+ kind: 'component',
156
+ title: 'graphql'
157
+ }),
158
+ 'email': el({
159
+ id: 'email',
160
+ kind: 'system',
161
+ title: 'email'
162
+ }),
163
+ 'cloud.backend.storage': el({
164
+ id: 'cloud.backend.storage',
165
+ kind: 'component',
166
+ title: 'storage',
167
+ tags: ['old' as Tag]
168
+ }),
169
+ 'cloud.frontend.adminPanel': el({
170
+ id: 'cloud.frontend.adminPanel',
171
+ kind: 'component',
172
+ title: 'adminPanel',
173
+ tags: ['old' as Tag]
174
+ }),
175
+ 'cloud.frontend.dashboard': el({
176
+ id: 'cloud.frontend.dashboard',
177
+ kind: 'component',
178
+ title: 'dashboard'
179
+ }),
180
+ 'amazon': el({
181
+ id: 'amazon',
182
+ kind: 'system',
183
+ title: 'amazon'
184
+ }),
185
+ 'amazon.s3': el({
186
+ id: 'amazon.s3',
187
+ kind: 'component',
188
+ title: 's3',
189
+ shape: 'storage'
190
+ })
191
+ } satisfies Record<string, Element>
192
+
193
+ export type FakeElementIds = keyof typeof fakeElements
194
+
195
+ const rel = ({
196
+ source,
197
+ target,
198
+ title
199
+ }: {
200
+ source: FakeElementIds
201
+ target: FakeElementIds
202
+ title?: string
203
+ }): Relation => ({
204
+ id: `${source}:${target}` as RelationID,
205
+ title: title ?? '',
206
+ source: source as Fqn,
207
+ target: target as Fqn
208
+ })
209
+
210
+ export const fakeRelations = [
211
+ rel({
212
+ source: 'customer',
213
+ target: 'cloud.frontend.dashboard',
214
+ title: 'opens in browser'
215
+ }),
216
+ rel({
217
+ source: 'support',
218
+ target: 'cloud.frontend.adminPanel',
219
+ title: 'manages'
220
+ }),
221
+ rel({
222
+ source: 'cloud.backend.storage',
223
+ target: 'amazon.s3',
224
+ title: 'uploads'
225
+ }),
226
+ rel({
227
+ source: 'customer',
228
+ target: 'cloud',
229
+ title: 'uses'
230
+ }),
231
+ rel({
232
+ source: 'cloud.backend.graphql',
233
+ target: 'cloud.backend.storage',
234
+ title: 'stores'
235
+ }),
236
+ // rel({
237
+ // source: 'cloud.backend',
238
+ // target: 'cloud.email',
239
+ // title: 'schedule emails'
240
+ // }),
241
+ // rel({
242
+ // source: 'cloud.email',
243
+ // target: 'customer',
244
+ // title: 'send emails'
245
+ // }),
246
+ rel({
247
+ source: 'cloud.frontend',
248
+ target: 'cloud.backend',
249
+ title: 'requests'
250
+ }),
251
+ rel({
252
+ source: 'cloud.frontend.dashboard',
253
+ target: 'cloud.backend.graphql',
254
+ title: 'requests'
255
+ }),
256
+ rel({
257
+ source: 'cloud.frontend.adminPanel',
258
+ target: 'cloud.backend.graphql',
259
+ title: 'fetches'
260
+ }),
261
+ rel({
262
+ source: 'cloud',
263
+ target: 'amazon',
264
+ title: 'uses'
265
+ }),
266
+ rel({
267
+ source: 'cloud.backend',
268
+ target: 'email',
269
+ title: 'schedule'
270
+ }),
271
+ rel({
272
+ source: 'cloud',
273
+ target: 'email',
274
+ title: 'uses'
275
+ }),
276
+ rel({
277
+ source: 'email',
278
+ target: 'cloud',
279
+ title: 'notifies'
280
+ })
281
+ ]
282
+
283
+ export type FakeRelationIds = keyof typeof fakeRelations
284
+
285
+ export const fakeModel = new LikeC4ModelGraph({
286
+ elements: fakeElements,
287
+ relations: indexBy(fakeRelations, r => r.id)
288
+ })
289
+
290
+ const emptyView = {
291
+ __: 'element' as const,
292
+ id: 'index' as ViewID,
293
+ title: null,
294
+ description: null,
295
+ tags: null,
296
+ links: null,
297
+ rules: []
298
+ }
299
+
300
+ export const includeWildcard = {
301
+ include: [
302
+ {
303
+ wildcard: true
304
+ }
305
+ ]
306
+ } satisfies ViewRule
307
+
308
+ export type ElementRefExpr = '*' | FakeElementIds | `${FakeElementIds}.*` | `${FakeElementIds}._`
309
+
310
+ type InOutExpr = `-> ${ElementRefExpr} ->`
311
+ type IncomingExpr = `-> ${ElementRefExpr}`
312
+ type OutgoingExpr = `${ElementRefExpr} ->`
313
+ type RelationKeyword = '->' | '<->'
314
+ type RelationExpr = `${ElementRefExpr} ${RelationKeyword} ${ElementRefExpr}`
315
+
316
+ type CustomExpr = {
317
+ custom: {
318
+ element: FakeElementIds
319
+ title?: string
320
+ description?: string
321
+ technology?: string
322
+ shape?: ElementShape
323
+ color?: ThemeColor
324
+ border?: BorderStyle
325
+ icon?: string
326
+ opacity?: number
327
+ navigateTo?: string
328
+ }
329
+ }
330
+
331
+ type Expression =
332
+ | ElementRefExpr
333
+ | InOutExpr
334
+ | IncomingExpr
335
+ | OutgoingExpr
336
+ | RelationExpr
337
+ | CustomExpr
338
+
339
+ function toExpression(expr: Expression): C4Expression {
340
+ if (!isString(expr)) {
341
+ return expr as C4Expression
342
+ }
343
+ if (expr === '*') {
344
+ return { wildcard: true }
345
+ }
346
+ if (expr.startsWith('->')) {
347
+ if (expr.endsWith('->')) {
348
+ return {
349
+ inout: toExpression(expr.replace(/->/g, '').trim() as ElementRefExpr) as any
350
+ }
351
+ }
352
+ return {
353
+ incoming: toExpression(expr.replace('-> ', '') as ElementRefExpr) as any
354
+ }
355
+ }
356
+ if (expr.endsWith(' ->')) {
357
+ return {
358
+ outgoing: toExpression(expr.replace(' ->', '') as ElementRefExpr) as any
359
+ }
360
+ }
361
+ if (expr.includes(' <-> ')) {
362
+ const [source, target] = expr.split(' <-> ')
363
+ return {
364
+ source: toExpression(source as ElementRefExpr) as any,
365
+ target: toExpression(target as ElementRefExpr) as any,
366
+ isBidirectional: true
367
+ }
368
+ }
369
+ if (expr.includes(' -> ')) {
370
+ const [source, target] = expr.split(' -> ')
371
+ return {
372
+ source: toExpression(source as ElementRefExpr) as any,
373
+ target: toExpression(target as ElementRefExpr) as any
374
+ }
375
+ }
376
+ if (expr.endsWith('._')) {
377
+ return {
378
+ expanded: expr.replace('._', '') as Fqn
379
+ }
380
+ }
381
+ if (expr.endsWith('.*')) {
382
+ return {
383
+ element: expr.replace('.*', '') as Fqn,
384
+ isDescedants: true
385
+ }
386
+ }
387
+ return {
388
+ element: expr as Fqn,
389
+ isDescedants: false
390
+ }
391
+ }
392
+
393
+ export function $include(expr: Expression): ViewRuleExpression {
394
+ return {
395
+ include: [toExpression(expr)]
396
+ }
397
+ }
398
+ export function $exclude(expr: Expression): ViewRuleExpression {
399
+ return {
400
+ exclude: [toExpression(expr)]
401
+ }
402
+ }
403
+
404
+ export function $style(element: ElementRefExpr, style: ViewRuleStyle['style']): ViewRuleStyle {
405
+ return {
406
+ targets: [toExpression(element) as C4ElementExpression],
407
+ style: Object.assign({}, style)
408
+ }
409
+ }
410
+
411
+ export function computeView(
412
+ ...args: [FakeElementIds, ViewRule | ViewRule[]] | [ViewRule | ViewRule[]]
413
+ ) {
414
+ let result: ComputedView
415
+ if (args.length === 1) {
416
+ result = computeElementView(
417
+ {
418
+ ...emptyView,
419
+ rules: [args[0]].flat()
420
+ },
421
+ fakeModel
422
+ )
423
+ } else {
424
+ result = computeElementView(
425
+ {
426
+ ...emptyView,
427
+ id: 'index' as ViewID,
428
+ viewOf: args[0] as Fqn,
429
+ rules: [args[1]].flat()
430
+ },
431
+ fakeModel
432
+ )
433
+ }
434
+ return Object.assign(result, {
435
+ nodeIds: pluck('id', result.nodes) as string[],
436
+ edgeIds: pluck('id', result.edges) as string[]
437
+ })
438
+ }