@likec4/language-server 1.7.4 → 1.8.1
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/contrib/likec4.tmLanguage.json +1 -1
- package/package.json +9 -9
- package/src/ast.ts +34 -12
- package/src/generated/ast.ts +215 -120
- package/src/generated/grammar.ts +1 -1
- package/src/like-c4.langium +63 -23
- package/src/lsp/SemanticTokenProvider.ts +2 -2
- package/src/model/model-builder.ts +30 -13
- package/src/model/model-parser.ts +92 -22
- package/src/model-graph/compute-view/compute.ts +27 -5
- package/src/model-graph/dynamic-view/compute.ts +11 -3
- package/src/model-graph/utils/applyCustomElementProperties.ts +4 -3
- package/src/model-graph/utils/applyViewRuleStyles.ts +3 -0
- package/src/model-graph/utils/buildElementNotations.ts +63 -0
- package/src/references/scope-computation.ts +5 -5
- package/src/references/scope-provider.ts +21 -9
- package/src/validation/_shared.ts +24 -0
- package/src/validation/dynamic-view-rule.ts +0 -6
- package/src/validation/element.ts +9 -9
- package/src/validation/index.ts +2 -1
- package/src/validation/property-checks.ts +1 -1
- package/src/validation/relation.ts +45 -39
- package/src/validation/specification.ts +15 -2
- package/src/validation/view.ts +7 -0
- package/src/view-utils/view-hash.ts +12 -18
package/src/like-c4.langium
CHANGED
|
@@ -42,25 +42,37 @@ SpecificationRule:
|
|
|
42
42
|
|
|
43
43
|
SpecificationElementKind:
|
|
44
44
|
'element' kind=ElementKind ('{'
|
|
45
|
-
|
|
45
|
+
props+=(
|
|
46
|
+
SpecificationElementStringProperty |
|
|
47
|
+
ElementStyleProperty
|
|
48
|
+
)*
|
|
46
49
|
'}')?;
|
|
47
50
|
|
|
51
|
+
SpecificationElementStringProperty:
|
|
52
|
+
key=('technology' | 'notation') ':'? value=String ';'?;
|
|
53
|
+
|
|
48
54
|
SpecificationTag:
|
|
49
55
|
'tag' tag=Tag;
|
|
50
56
|
|
|
51
57
|
SpecificationRelationshipKind:
|
|
52
58
|
'relationship' kind=RelationshipKind ('{'
|
|
53
|
-
props+=
|
|
59
|
+
props+=(
|
|
60
|
+
RelationshipStyleProperty |
|
|
61
|
+
SpecificationRelationshipStringProperty
|
|
62
|
+
)*
|
|
54
63
|
'}')?
|
|
55
64
|
;
|
|
56
65
|
|
|
66
|
+
SpecificationRelationshipStringProperty:
|
|
67
|
+
key=('technology' | 'notation') ':'? value=String ';'?;
|
|
68
|
+
|
|
57
69
|
// Model -------------------------------------
|
|
58
70
|
|
|
59
71
|
Model:
|
|
60
72
|
name='model' '{'
|
|
61
73
|
elements+=(
|
|
62
74
|
ExtendElement |
|
|
63
|
-
|
|
75
|
+
Relation<true> |
|
|
64
76
|
Element
|
|
65
77
|
)*
|
|
66
78
|
'}'
|
|
@@ -85,14 +97,14 @@ ElementBody: '{'
|
|
|
85
97
|
tags=Tags?
|
|
86
98
|
props+=ElementProperty*
|
|
87
99
|
elements+=(
|
|
88
|
-
Relation |
|
|
100
|
+
Relation<false> |
|
|
89
101
|
Element
|
|
90
102
|
)*
|
|
91
103
|
'}'
|
|
92
104
|
;
|
|
93
105
|
|
|
94
106
|
ElementProperty:
|
|
95
|
-
ElementStringProperty |
|
|
107
|
+
ElementStringProperty | ElementStyleProperty | LinkProperty | IconProperty | MetadataProperty;
|
|
96
108
|
|
|
97
109
|
ElementStringProperty:
|
|
98
110
|
key=('title' | 'technology' | 'description') ':'? value=String ';'?;
|
|
@@ -103,7 +115,7 @@ ExtendElement:
|
|
|
103
115
|
|
|
104
116
|
ExtendElementBody: '{'
|
|
105
117
|
elements+=(
|
|
106
|
-
|
|
118
|
+
Relation<true> |
|
|
107
119
|
Element
|
|
108
120
|
)*
|
|
109
121
|
'}'
|
|
@@ -120,19 +132,18 @@ Tags:
|
|
|
120
132
|
(values+=[Tag:TagId])+ ({infer Tags.prev=current} ',' (values+=[Tag:TagId])*)* ';'?
|
|
121
133
|
;
|
|
122
134
|
|
|
123
|
-
Relation
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
('this' | 'it')? RelationFragment;
|
|
131
|
-
|
|
132
|
-
fragment RelationFragment:
|
|
133
|
-
('->' | '-[' kind=[RelationshipKind] ']->' | kind=[RelationshipKind:DotId] )
|
|
135
|
+
Relation<isExplicit>:
|
|
136
|
+
(<isExplicit> source=ElementRef | <!isExplicit> source=ElementRef?)
|
|
137
|
+
(
|
|
138
|
+
kind=[RelationshipKind:DotId] |
|
|
139
|
+
'-[' kind=[RelationshipKind] ']->' |
|
|
140
|
+
'->'
|
|
141
|
+
)
|
|
134
142
|
target=ElementRef
|
|
135
|
-
|
|
143
|
+
(
|
|
144
|
+
title=String
|
|
145
|
+
technology=String?
|
|
146
|
+
)?
|
|
136
147
|
tags=Tags?
|
|
137
148
|
body=RelationBody?
|
|
138
149
|
;
|
|
@@ -144,7 +155,7 @@ RelationBody: '{'
|
|
|
144
155
|
;
|
|
145
156
|
|
|
146
157
|
RelationProperty:
|
|
147
|
-
RelationStringProperty | RelationStyleProperty | LinkProperty;
|
|
158
|
+
RelationStringProperty | RelationStyleProperty | LinkProperty | MetadataProperty;
|
|
148
159
|
|
|
149
160
|
RelationStringProperty:
|
|
150
161
|
key=('title' | 'technology' | 'description') ':'? value=String ';'?;
|
|
@@ -155,6 +166,18 @@ RelationStyleProperty:
|
|
|
155
166
|
'}'
|
|
156
167
|
;
|
|
157
168
|
|
|
169
|
+
MetadataProperty:
|
|
170
|
+
'metadata' MetadataBody
|
|
171
|
+
;
|
|
172
|
+
|
|
173
|
+
MetadataBody: '{'
|
|
174
|
+
props+=(MetadataAttribute)*
|
|
175
|
+
'}'
|
|
176
|
+
;
|
|
177
|
+
|
|
178
|
+
MetadataAttribute:
|
|
179
|
+
key=IdTerminal value=String
|
|
180
|
+
;
|
|
158
181
|
|
|
159
182
|
// Views -------------------------------------
|
|
160
183
|
|
|
@@ -200,7 +223,15 @@ DynamicViewBody: '{'
|
|
|
200
223
|
;
|
|
201
224
|
|
|
202
225
|
|
|
203
|
-
type StringProperty =
|
|
226
|
+
type StringProperty =
|
|
227
|
+
ElementStringProperty |
|
|
228
|
+
ViewStringProperty |
|
|
229
|
+
RelationStringProperty |
|
|
230
|
+
MetadataAttribute |
|
|
231
|
+
SpecificationElementStringProperty |
|
|
232
|
+
SpecificationRelationshipStringProperty |
|
|
233
|
+
NotationProperty
|
|
234
|
+
;
|
|
204
235
|
|
|
205
236
|
ViewProperty:
|
|
206
237
|
ViewStringProperty | LinkProperty
|
|
@@ -376,16 +407,24 @@ DynamicViewPredicateIterator:
|
|
|
376
407
|
|
|
377
408
|
ViewRuleStyle:
|
|
378
409
|
'style' target=ElementExpressionsIterator '{'
|
|
379
|
-
props+=
|
|
410
|
+
props+=(
|
|
411
|
+
StyleProperty |
|
|
412
|
+
NotationProperty
|
|
413
|
+
)*
|
|
380
414
|
'}';
|
|
381
415
|
|
|
382
416
|
ViewRuleAutoLayout:
|
|
383
417
|
'autoLayout' direction=ViewLayoutDirection;
|
|
384
418
|
|
|
419
|
+
NotationProperty:
|
|
420
|
+
key='notation' ':'? value=String ';'?
|
|
421
|
+
;
|
|
422
|
+
|
|
385
423
|
CustomElementProperties: '{'
|
|
386
424
|
props+=(
|
|
387
425
|
NavigateToProperty |
|
|
388
426
|
ElementStringProperty |
|
|
427
|
+
NotationProperty |
|
|
389
428
|
StyleProperty
|
|
390
429
|
)*
|
|
391
430
|
'}'
|
|
@@ -394,6 +433,7 @@ CustomElementProperties: '{'
|
|
|
394
433
|
CustomRelationProperties: '{'
|
|
395
434
|
props+=(
|
|
396
435
|
RelationStringProperty |
|
|
436
|
+
NotationProperty |
|
|
397
437
|
RelationshipStyleProperty
|
|
398
438
|
)*
|
|
399
439
|
'}'
|
|
@@ -405,7 +445,7 @@ NavigateToProperty:
|
|
|
405
445
|
// Common properties -------------------------------------
|
|
406
446
|
|
|
407
447
|
LinkProperty:
|
|
408
|
-
key='link' ':'? value=Uri ';'?;
|
|
448
|
+
key='link' ':'? value=Uri title=String? ';'?;
|
|
409
449
|
ColorProperty:
|
|
410
450
|
key='color' ':'? value=ThemeColor ';'?;
|
|
411
451
|
|
|
@@ -433,7 +473,7 @@ StyleProperty:
|
|
|
433
473
|
OpacityProperty |
|
|
434
474
|
IconProperty;
|
|
435
475
|
|
|
436
|
-
|
|
476
|
+
ElementStyleProperty:
|
|
437
477
|
key='style' '{'
|
|
438
478
|
props+=StyleProperty*
|
|
439
479
|
'}';
|
|
@@ -146,7 +146,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
146
146
|
}
|
|
147
147
|
if (
|
|
148
148
|
ast.isRelationStyleProperty(node)
|
|
149
|
-
|| (ast.
|
|
149
|
+
|| (ast.isElementStyleProperty(node) && ast.isElementBody(node.$container))
|
|
150
150
|
) {
|
|
151
151
|
acceptor({
|
|
152
152
|
node,
|
|
@@ -177,7 +177,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
177
177
|
property: 'key',
|
|
178
178
|
type: SemanticTokenTypes.property
|
|
179
179
|
})
|
|
180
|
-
if ('value' in node) {
|
|
180
|
+
if ('value' in node && node.value) {
|
|
181
181
|
acceptor({
|
|
182
182
|
node,
|
|
183
183
|
property: 'value',
|
|
@@ -40,7 +40,7 @@ import type { LikeC4Services } from '../module'
|
|
|
40
40
|
import { printDocs } from '../utils/printDocs'
|
|
41
41
|
import { assignNavigateTo, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
|
|
42
42
|
|
|
43
|
-
function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]) {
|
|
43
|
+
function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.LikeC4Model {
|
|
44
44
|
const c4Specification: ParsedAstSpecification = {
|
|
45
45
|
kinds: {},
|
|
46
46
|
relationships: {}
|
|
@@ -49,8 +49,11 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
|
|
|
49
49
|
Object.assign(c4Specification.kinds, spec.kinds)
|
|
50
50
|
Object.assign(c4Specification.relationships, spec.relationships)
|
|
51
51
|
})
|
|
52
|
-
const resolveLinks = (doc: LangiumDocument, links: c4.NonEmptyArray<
|
|
53
|
-
return links.map(l =>
|
|
52
|
+
const resolveLinks = (doc: LangiumDocument, links: c4.NonEmptyArray<c4.Link>) => {
|
|
53
|
+
return links.map(l => ({
|
|
54
|
+
url: services.lsp.DocumentLinkProvider.resolveLink(doc, l.url),
|
|
55
|
+
...(l.title && { title: l.title })
|
|
56
|
+
})) as c4.NonEmptyArray<c4.Link>
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
const toModelElement = (doc: LangiumDocument) => {
|
|
@@ -68,7 +71,8 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
|
|
|
68
71
|
kind,
|
|
69
72
|
title,
|
|
70
73
|
description,
|
|
71
|
-
technology
|
|
74
|
+
technology,
|
|
75
|
+
metadata
|
|
72
76
|
}: ParsedAstElement): c4.Element | null => {
|
|
73
77
|
try {
|
|
74
78
|
const __kind = c4Specification.kinds[kind]
|
|
@@ -76,15 +80,18 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
|
|
|
76
80
|
logger.warn(`No kind '${kind}' found for ${id}`)
|
|
77
81
|
return null
|
|
78
82
|
}
|
|
79
|
-
color ??= __kind.color
|
|
80
|
-
shape ??= __kind.shape
|
|
81
|
-
icon ??= __kind.icon
|
|
82
|
-
opacity ??= __kind.opacity
|
|
83
|
-
border ??= __kind.border
|
|
83
|
+
color ??= __kind.style.color
|
|
84
|
+
shape ??= __kind.style.shape
|
|
85
|
+
icon ??= __kind.style.icon
|
|
86
|
+
opacity ??= __kind.style.opacity
|
|
87
|
+
border ??= __kind.style.border
|
|
88
|
+
technology ??= __kind.technology
|
|
84
89
|
return {
|
|
85
90
|
...(color && { color }),
|
|
86
91
|
...(shape && { shape }),
|
|
87
92
|
...(icon && { icon }),
|
|
93
|
+
...(metadata && { metadata }),
|
|
94
|
+
...(__kind.notation && { notation: __kind.notation }),
|
|
88
95
|
style: {
|
|
89
96
|
...(border && { border }),
|
|
90
97
|
...(isNumber(opacity) && { opacity })
|
|
@@ -281,11 +288,14 @@ export class LikeC4ModelBuilder {
|
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
public async buildModel(cancelToken?: Cancellation.CancellationToken): Promise<c4.LikeC4Model | null> {
|
|
291
|
+
const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4Model | null>
|
|
292
|
+
if (cache.has(RAW_MODEL_CACHE)) {
|
|
293
|
+
return cache.get(RAW_MODEL_CACHE)!
|
|
294
|
+
}
|
|
284
295
|
return await this.services.shared.workspace.WorkspaceLock.read(async () => {
|
|
285
296
|
if (cancelToken) {
|
|
286
297
|
await interruptAndCheck(cancelToken)
|
|
287
298
|
}
|
|
288
|
-
const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4Model | null>
|
|
289
299
|
return cache.get(RAW_MODEL_CACHE, () => {
|
|
290
300
|
const docs = this.documents()
|
|
291
301
|
if (docs.length === 0) {
|
|
@@ -303,6 +313,10 @@ export class LikeC4ModelBuilder {
|
|
|
303
313
|
public async buildComputedModel(
|
|
304
314
|
cancelToken?: Cancellation.CancellationToken
|
|
305
315
|
): Promise<c4.LikeC4ComputedModel | null> {
|
|
316
|
+
const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4ComputedModel | null>
|
|
317
|
+
if (cache.has(MODEL_CACHE)) {
|
|
318
|
+
return cache.get(MODEL_CACHE)!
|
|
319
|
+
}
|
|
306
320
|
const model = await this.buildModel(cancelToken)
|
|
307
321
|
if (!model) {
|
|
308
322
|
return null
|
|
@@ -311,7 +325,6 @@ export class LikeC4ModelBuilder {
|
|
|
311
325
|
if (cancelToken) {
|
|
312
326
|
await interruptAndCheck(cancelToken)
|
|
313
327
|
}
|
|
314
|
-
const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4ComputedModel | null>
|
|
315
328
|
const viewsCache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
|
|
316
329
|
return cache.get(MODEL_CACHE, () => {
|
|
317
330
|
const index = new LikeC4ModelGraph(model)
|
|
@@ -346,6 +359,11 @@ export class LikeC4ModelBuilder {
|
|
|
346
359
|
viewId: ViewID,
|
|
347
360
|
cancelToken?: Cancellation.CancellationToken
|
|
348
361
|
): Promise<c4.ComputedView | null> {
|
|
362
|
+
const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
|
|
363
|
+
const cacheKey = computedViewKey(viewId)
|
|
364
|
+
if (cache.has(cacheKey)) {
|
|
365
|
+
return cache.get(cacheKey)!
|
|
366
|
+
}
|
|
349
367
|
const model = await this.buildModel(cancelToken)
|
|
350
368
|
const view = model?.views[viewId]
|
|
351
369
|
if (!view) {
|
|
@@ -356,8 +374,7 @@ export class LikeC4ModelBuilder {
|
|
|
356
374
|
if (cancelToken) {
|
|
357
375
|
await interruptAndCheck(cancelToken)
|
|
358
376
|
}
|
|
359
|
-
|
|
360
|
-
return cache.get(computedViewKey(viewId), () => {
|
|
377
|
+
return cache.get(cacheKey, () => {
|
|
361
378
|
const index = new LikeC4ModelGraph(model)
|
|
362
379
|
const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
|
|
363
380
|
if (!result.isSuccess) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type c4, InvalidModelError, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
|
|
2
2
|
import type { AstNode, LangiumDocument } from 'langium'
|
|
3
3
|
import { AstUtils, CstUtils } from 'langium'
|
|
4
|
-
import { isDefined, isTruthy, mapToObj } from 'remeda'
|
|
4
|
+
import { filter, flatMap, isDefined, isNonNullish, isTruthy, mapToObj, pipe } from 'remeda'
|
|
5
5
|
import stripIndent from 'strip-indent'
|
|
6
6
|
import type { Writable } from 'type-fest'
|
|
7
7
|
import type {
|
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
ParsedAstElement,
|
|
12
12
|
ParsedAstElementView,
|
|
13
13
|
ParsedAstRelation,
|
|
14
|
-
ParsedLikeC4LangiumDocument
|
|
14
|
+
ParsedLikeC4LangiumDocument,
|
|
15
|
+
ParsedLink
|
|
15
16
|
} from '../ast'
|
|
16
17
|
import {
|
|
17
18
|
ast,
|
|
@@ -38,12 +39,12 @@ const { getDocument } = AstUtils
|
|
|
38
39
|
|
|
39
40
|
export type ModelParsedListener = () => void
|
|
40
41
|
|
|
41
|
-
function toSingleLine<T extends string | undefined>(str: T): T {
|
|
42
|
-
return (
|
|
42
|
+
function toSingleLine<T extends string | undefined | null>(str: T): T {
|
|
43
|
+
return (isNonNullish(str) ? removeIndent(str).split('\n').join(' ') : undefined) as T
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function removeIndent<T extends string | undefined>(str: T): T {
|
|
46
|
-
return (
|
|
46
|
+
function removeIndent<T extends string | undefined | null>(str: T): T {
|
|
47
|
+
return (isNonNullish(str) ? stripIndent(str).trim() : undefined) as T
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export type IsValidFn = ChecksFromDiagnostics['isValid']
|
|
@@ -86,12 +87,23 @@ export class LikeC4ModelParser {
|
|
|
86
87
|
|
|
87
88
|
const specifications = parseResult.value.specifications.filter(isValid)
|
|
88
89
|
const element_specs = specifications.flatMap(s => s.elements.filter(isValid))
|
|
89
|
-
for (const { kind,
|
|
90
|
+
for (const { kind, props } of element_specs) {
|
|
90
91
|
try {
|
|
92
|
+
const style = props.find(ast.isElementStyleProperty)
|
|
91
93
|
const kindName = kind.name as c4.ElementKind
|
|
94
|
+
if (kindName in c4Specification.kinds) {
|
|
95
|
+
logger.warn(`Element kind "${kindName}" is already defined`)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
const bodyProps = mapToObj(
|
|
99
|
+
props.filter(ast.isSpecificationElementStringProperty).filter(p => isNonNullish(p.value)) ?? [],
|
|
100
|
+
p => [p.key, removeIndent(p.value)]
|
|
101
|
+
)
|
|
92
102
|
c4Specification.kinds[kindName] = {
|
|
93
|
-
...
|
|
94
|
-
|
|
103
|
+
...bodyProps,
|
|
104
|
+
style: {
|
|
105
|
+
...toElementStyle(style?.props)
|
|
106
|
+
}
|
|
95
107
|
}
|
|
96
108
|
} catch (e) {
|
|
97
109
|
logWarnError(e)
|
|
@@ -102,8 +114,16 @@ export class LikeC4ModelParser {
|
|
|
102
114
|
for (const { kind, props } of relations_specs) {
|
|
103
115
|
try {
|
|
104
116
|
const kindName = kind.name as c4.RelationshipKind
|
|
117
|
+
if (kindName in c4Specification.relationships) {
|
|
118
|
+
logger.warn(`Relationship kind "${kindName}" is already defined`)
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const bodyProps = mapToObj(
|
|
122
|
+
props.filter(ast.isSpecificationRelationshipStringProperty).filter(p => isNonNullish(p.value)) ?? [],
|
|
123
|
+
p => [p.key, p.value]
|
|
124
|
+
)
|
|
105
125
|
c4Specification.relationships[kindName] = {
|
|
106
|
-
...
|
|
126
|
+
...bodyProps,
|
|
107
127
|
...toRelationshipStyleExcludeDefaults(props)
|
|
108
128
|
}
|
|
109
129
|
} catch (e) {
|
|
@@ -138,8 +158,9 @@ export class LikeC4ModelParser {
|
|
|
138
158
|
const id = this.resolveFqn(astNode)
|
|
139
159
|
const kind = astNode.kind.$refText as c4.ElementKind
|
|
140
160
|
const tags = this.convertTags(astNode.body)
|
|
141
|
-
const stylePropsAst = astNode.body?.props.find(ast.
|
|
161
|
+
const stylePropsAst = astNode.body?.props.find(ast.isElementStyleProperty)?.props
|
|
142
162
|
const style = toElementStyle(stylePropsAst)
|
|
163
|
+
const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty))
|
|
143
164
|
const astPath = this.getAstNodePath(astNode)
|
|
144
165
|
|
|
145
166
|
let [title, description, technology] = astNode.props ?? []
|
|
@@ -153,7 +174,7 @@ export class LikeC4ModelParser {
|
|
|
153
174
|
description = removeIndent(bodyProps.description ?? description)
|
|
154
175
|
technology = toSingleLine(bodyProps.technology ?? technology)
|
|
155
176
|
|
|
156
|
-
const links = astNode.body
|
|
177
|
+
const links = this.convertLinks(astNode.body)
|
|
157
178
|
|
|
158
179
|
// Property has higher priority than from style
|
|
159
180
|
const iconProp = astNode.body?.props.find(ast.isIconProperty)
|
|
@@ -169,6 +190,7 @@ export class LikeC4ModelParser {
|
|
|
169
190
|
kind,
|
|
170
191
|
astPath,
|
|
171
192
|
title: title ?? astNode.name,
|
|
193
|
+
...(metadata && { metadata }),
|
|
172
194
|
...(tags && { tags }),
|
|
173
195
|
...(links && isNonEmptyArray(links) && { links }),
|
|
174
196
|
...(isTruthy(technology) && { technology }),
|
|
@@ -182,18 +204,19 @@ export class LikeC4ModelParser {
|
|
|
182
204
|
const target = this.resolveFqn(coupling.target)
|
|
183
205
|
const source = this.resolveFqn(coupling.source)
|
|
184
206
|
const tags = this.convertTags(astNode) ?? this.convertTags(astNode.body)
|
|
185
|
-
const links = astNode.body
|
|
207
|
+
const links = this.convertLinks(astNode.body)
|
|
186
208
|
const kind = astNode.kind?.ref?.name as (c4.RelationshipKind | undefined)
|
|
209
|
+
const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty))
|
|
187
210
|
const astPath = this.getAstNodePath(astNode)
|
|
188
211
|
|
|
189
212
|
const bodyProps = mapToObj(
|
|
190
|
-
astNode.body?.props.filter(ast.isRelationStringProperty) ?? [],
|
|
191
|
-
p => [p.key, p.value
|
|
213
|
+
astNode.body?.props.filter(ast.isRelationStringProperty).filter(p => isNonNullish(p.value)) ?? [],
|
|
214
|
+
p => [p.key, p.value]
|
|
192
215
|
)
|
|
193
216
|
|
|
194
217
|
const title = removeIndent(astNode.title ?? bodyProps.title) ?? ''
|
|
195
218
|
const description = removeIndent(bodyProps.description)
|
|
196
|
-
const technology = toSingleLine(bodyProps.technology)
|
|
219
|
+
const technology = removeIndent(astNode.technology) ?? toSingleLine(bodyProps.technology)
|
|
197
220
|
|
|
198
221
|
const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty)
|
|
199
222
|
const id = stringHash(
|
|
@@ -207,6 +230,7 @@ export class LikeC4ModelParser {
|
|
|
207
230
|
source,
|
|
208
231
|
target,
|
|
209
232
|
title,
|
|
233
|
+
...(metadata && { metadata }),
|
|
210
234
|
...(isTruthy(technology) && { technology }),
|
|
211
235
|
...(isTruthy(description) && { description }),
|
|
212
236
|
...(kind && { kind }),
|
|
@@ -391,7 +415,12 @@ export class LikeC4ModelParser {
|
|
|
391
415
|
}
|
|
392
416
|
return acc
|
|
393
417
|
}
|
|
394
|
-
|
|
418
|
+
if (ast.isNotationProperty(prop)) {
|
|
419
|
+
if (isTruthy(prop.value)) {
|
|
420
|
+
acc.custom[prop.key] = removeIndent(prop.value)
|
|
421
|
+
}
|
|
422
|
+
return acc
|
|
423
|
+
}
|
|
395
424
|
nonexhaustive(prop)
|
|
396
425
|
},
|
|
397
426
|
{
|
|
@@ -475,6 +504,12 @@ export class LikeC4ModelParser {
|
|
|
475
504
|
}
|
|
476
505
|
return acc
|
|
477
506
|
}
|
|
507
|
+
if (ast.isNotationProperty(prop)) {
|
|
508
|
+
if (isTruthy(prop.value)) {
|
|
509
|
+
acc.customRelation[prop.key] = removeIndent(prop.value)
|
|
510
|
+
}
|
|
511
|
+
return acc
|
|
512
|
+
}
|
|
478
513
|
nonexhaustive(prop)
|
|
479
514
|
},
|
|
480
515
|
{
|
|
@@ -516,9 +551,12 @@ export class LikeC4ModelParser {
|
|
|
516
551
|
return this.parseViewRulePredicate(astRule, isValid)
|
|
517
552
|
}
|
|
518
553
|
if (ast.isViewRuleStyle(astRule)) {
|
|
519
|
-
const styleProps = toElementStyle(astRule.props)
|
|
554
|
+
const styleProps = toElementStyle(astRule.props.filter(ast.isStyleProperty))
|
|
555
|
+
const notation = removeIndent(astRule.props.find(ast.isNotationProperty)?.value)
|
|
556
|
+
const targets = this.parseElementExpressionsIterator(astRule.target)
|
|
520
557
|
return {
|
|
521
|
-
targets
|
|
558
|
+
targets,
|
|
559
|
+
...(notation && { notation }),
|
|
522
560
|
style: {
|
|
523
561
|
...styleProps
|
|
524
562
|
}
|
|
@@ -601,6 +639,12 @@ export class LikeC4ModelParser {
|
|
|
601
639
|
step[prop.key] = prop.value
|
|
602
640
|
continue
|
|
603
641
|
}
|
|
642
|
+
if (ast.isNotationProperty(prop)) {
|
|
643
|
+
if (isTruthy(prop.value)) {
|
|
644
|
+
step[prop.key] = prop.value
|
|
645
|
+
}
|
|
646
|
+
continue
|
|
647
|
+
}
|
|
604
648
|
nonexhaustive(prop)
|
|
605
649
|
}
|
|
606
650
|
catch (e) {
|
|
@@ -640,7 +684,7 @@ export class LikeC4ModelParser {
|
|
|
640
684
|
const description = removeIndent(body.props.find(p => p.key === 'description')?.value) ?? null
|
|
641
685
|
|
|
642
686
|
const tags = this.convertTags(body)
|
|
643
|
-
const links =
|
|
687
|
+
const links = this.convertLinks(body)
|
|
644
688
|
|
|
645
689
|
const manualLayout = this.parseViewManualLaout(astNode)
|
|
646
690
|
|
|
@@ -695,7 +739,7 @@ export class LikeC4ModelParser {
|
|
|
695
739
|
const description = removeIndent(props.find(p => p.key === 'description')?.value) ?? null
|
|
696
740
|
|
|
697
741
|
const tags = this.convertTags(body)
|
|
698
|
-
const links =
|
|
742
|
+
const links = this.convertLinks(body)
|
|
699
743
|
|
|
700
744
|
ViewOps.writeId(astNode, id as c4.ViewID)
|
|
701
745
|
|
|
@@ -734,11 +778,13 @@ export class LikeC4ModelParser {
|
|
|
734
778
|
return acc
|
|
735
779
|
}
|
|
736
780
|
if (ast.isViewRuleStyle(n)) {
|
|
737
|
-
const styleProps = toElementStyle(n.props)
|
|
781
|
+
const styleProps = toElementStyle(n.props.filter(ast.isStyleProperty))
|
|
782
|
+
const notation = removeIndent(n.props.find(ast.isNotationProperty)?.value)
|
|
738
783
|
const targets = this.parseElementExpressionsIterator(n.target)
|
|
739
784
|
if (targets.length > 0) {
|
|
740
785
|
acc.push({
|
|
741
786
|
targets,
|
|
787
|
+
...(notation && { notation }),
|
|
742
788
|
style: {
|
|
743
789
|
...styleProps
|
|
744
790
|
}
|
|
@@ -785,6 +831,12 @@ export class LikeC4ModelParser {
|
|
|
785
831
|
return this.services.workspace.AstNodeLocator.getAstNodePath(node)
|
|
786
832
|
}
|
|
787
833
|
|
|
834
|
+
private getMetadata(metadataAstNode: ast.MetadataProperty | undefined): { [key: string]: string } | undefined {
|
|
835
|
+
return metadataAstNode?.props != null
|
|
836
|
+
? mapToObj(metadataAstNode.props, (p) => [p.key, removeIndent(p.value)])
|
|
837
|
+
: undefined
|
|
838
|
+
}
|
|
839
|
+
|
|
788
840
|
private convertTags<E extends { tags?: ast.Tags }>(withTags?: E) {
|
|
789
841
|
let iter = withTags?.tags
|
|
790
842
|
if (!iter) {
|
|
@@ -804,4 +856,22 @@ export class LikeC4ModelParser {
|
|
|
804
856
|
}
|
|
805
857
|
return isNonEmptyArray(tags) ? tags : null
|
|
806
858
|
}
|
|
859
|
+
|
|
860
|
+
private convertLinks(source?: ast.LinkProperty['$container']): ParsedLink[] | undefined {
|
|
861
|
+
if (!source?.props || source.props.length === 0) {
|
|
862
|
+
return undefined
|
|
863
|
+
}
|
|
864
|
+
return pipe(
|
|
865
|
+
source.props,
|
|
866
|
+
filter(ast.isLinkProperty),
|
|
867
|
+
flatMap(p => {
|
|
868
|
+
const url = p.value
|
|
869
|
+
if (isTruthy(url)) {
|
|
870
|
+
const title = isTruthy(p.title) ? toSingleLine(p.title) : undefined
|
|
871
|
+
return title ? { url, title } : { url }
|
|
872
|
+
}
|
|
873
|
+
return []
|
|
874
|
+
})
|
|
875
|
+
)
|
|
876
|
+
}
|
|
807
877
|
}
|
|
@@ -29,13 +29,14 @@ import {
|
|
|
29
29
|
parentFqn,
|
|
30
30
|
whereOperatorAsPredicate
|
|
31
31
|
} from '@likec4/core'
|
|
32
|
-
import { first, flatMap, hasAtLeast, isTruthy, unique } from 'remeda'
|
|
32
|
+
import { first, flatMap, hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
|
|
33
33
|
import { calcViewLayoutHash } from '../../view-utils/view-hash'
|
|
34
34
|
import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
|
|
35
35
|
import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
|
|
36
36
|
import { applyCustomRelationProperties } from '../utils/applyCustomRelationProperties'
|
|
37
37
|
import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
|
|
38
38
|
import { buildComputeNodes } from '../utils/buildComputeNodes'
|
|
39
|
+
import { buildElementNotations } from '../utils/buildElementNotations'
|
|
39
40
|
import { sortNodes } from '../utils/sortNodes'
|
|
40
41
|
import {
|
|
41
42
|
type ElementPredicateFn,
|
|
@@ -161,18 +162,25 @@ export class ComputeCtx {
|
|
|
161
162
|
})
|
|
162
163
|
)
|
|
163
164
|
)
|
|
164
|
-
|
|
165
165
|
const sortedEdges = new Set([
|
|
166
166
|
...nodes.flatMap(n => n.children.length === 0 ? n.outEdges.flatMap(id => edgesMap.get(id) ?? []) : []),
|
|
167
167
|
...edges
|
|
168
168
|
])
|
|
169
169
|
|
|
170
170
|
const autoLayoutRule = this.view.rules.findLast(isViewRuleAutoLayout)
|
|
171
|
+
|
|
172
|
+
const elementNotations = buildElementNotations(nodes)
|
|
173
|
+
|
|
171
174
|
return calcViewLayoutHash({
|
|
172
175
|
...view,
|
|
173
176
|
autoLayout: autoLayoutRule?.autoLayout ?? 'TB',
|
|
174
|
-
nodes,
|
|
175
|
-
edges: applyCustomRelationProperties(rules, nodes, sortedEdges)
|
|
177
|
+
nodes: map(nodes, omit(['notation'])),
|
|
178
|
+
edges: applyCustomRelationProperties(rules, nodes, sortedEdges),
|
|
179
|
+
...(elementNotations.length > 0 && {
|
|
180
|
+
notation: {
|
|
181
|
+
elements: elementNotations
|
|
182
|
+
}
|
|
183
|
+
})
|
|
176
184
|
})
|
|
177
185
|
}
|
|
178
186
|
|
|
@@ -263,7 +271,7 @@ export class ComputeCtx {
|
|
|
263
271
|
|
|
264
272
|
return Object.assign(
|
|
265
273
|
edge,
|
|
266
|
-
|
|
274
|
+
this.getEdgeLabel(relation),
|
|
267
275
|
isTruthy(relation.description) && { description: relation.description },
|
|
268
276
|
isTruthy(relation.technology) && { description: relation.technology },
|
|
269
277
|
isTruthy(relation.kind) && { kind: relation.kind },
|
|
@@ -529,5 +537,19 @@ export class ComputeCtx {
|
|
|
529
537
|
return this
|
|
530
538
|
}
|
|
531
539
|
nonexhaustive(expr)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
protected getEdgeLabel(relation: { title: String | undefined, technology?: String | undefined }): { label: String } | false {
|
|
543
|
+
const labelParts: String[] = []
|
|
544
|
+
|
|
545
|
+
if(isTruthy(relation.title)) {
|
|
546
|
+
labelParts.push(relation.title)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if(isTruthy(relation.technology)) {
|
|
550
|
+
labelParts.push(`[${relation.technology}]`)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return labelParts.length > 0 && { label: labelParts.join('\n') }
|
|
532
554
|
}
|
|
533
555
|
}
|
|
@@ -22,12 +22,13 @@ import {
|
|
|
22
22
|
parentFqn,
|
|
23
23
|
StepEdgeId
|
|
24
24
|
} from '@likec4/core'
|
|
25
|
-
import { hasAtLeast, isTruthy, map, unique } from 'remeda'
|
|
25
|
+
import { hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
|
|
26
26
|
import { calcViewLayoutHash } from '../../view-utils/view-hash'
|
|
27
27
|
import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
|
|
28
28
|
import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
|
|
29
29
|
import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
|
|
30
30
|
import { buildComputeNodes } from '../utils/buildComputeNodes'
|
|
31
|
+
import { buildElementNotations } from '../utils/buildElementNotations'
|
|
31
32
|
import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
|
|
32
33
|
|
|
33
34
|
export namespace DynamicViewComputeCtx {
|
|
@@ -162,11 +163,18 @@ export class DynamicViewComputeCtx {
|
|
|
162
163
|
|
|
163
164
|
const autoLayoutRule = rules.findLast(isViewRuleAutoLayout)
|
|
164
165
|
|
|
166
|
+
const elementNotations = buildElementNotations(nodes)
|
|
167
|
+
|
|
165
168
|
return calcViewLayoutHash({
|
|
166
169
|
...view,
|
|
167
170
|
autoLayout: autoLayoutRule?.autoLayout ?? 'LR',
|
|
168
|
-
nodes,
|
|
169
|
-
edges
|
|
171
|
+
nodes: map(nodes, omit(['notation'])),
|
|
172
|
+
edges,
|
|
173
|
+
...(elementNotations.length > 0 && {
|
|
174
|
+
notation: {
|
|
175
|
+
elements: elementNotations
|
|
176
|
+
}
|
|
177
|
+
})
|
|
170
178
|
})
|
|
171
179
|
}
|
|
172
180
|
|