@likec4/language-server 1.7.3 → 1.8.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/contrib/likec4.tmLanguage.json +1 -1
- package/package.json +9 -9
- package/src/ast.ts +30 -10
- package/src/generated/ast.ts +181 -41
- package/src/generated/grammar.ts +1 -1
- package/src/like-c4.langium +50 -8
- 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/validation/dynamic-view-rule.ts +0 -6
- package/src/validation/property-checks.ts +1 -1
- package/src/view-utils/view-hash.ts +12 -18
package/src/like-c4.langium
CHANGED
|
@@ -42,18 +42,30 @@ 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:
|
|
@@ -92,7 +104,7 @@ ElementBody: '{'
|
|
|
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 ';'?;
|
|
@@ -133,6 +145,7 @@ fragment RelationFragment:
|
|
|
133
145
|
('->' | '-[' kind=[RelationshipKind] ']->' | kind=[RelationshipKind:DotId] )
|
|
134
146
|
target=ElementRef
|
|
135
147
|
title=String?
|
|
148
|
+
technology=String?
|
|
136
149
|
tags=Tags?
|
|
137
150
|
body=RelationBody?
|
|
138
151
|
;
|
|
@@ -144,7 +157,7 @@ RelationBody: '{'
|
|
|
144
157
|
;
|
|
145
158
|
|
|
146
159
|
RelationProperty:
|
|
147
|
-
RelationStringProperty | RelationStyleProperty | LinkProperty;
|
|
160
|
+
RelationStringProperty | RelationStyleProperty | LinkProperty | MetadataProperty;
|
|
148
161
|
|
|
149
162
|
RelationStringProperty:
|
|
150
163
|
key=('title' | 'technology' | 'description') ':'? value=String ';'?;
|
|
@@ -155,6 +168,18 @@ RelationStyleProperty:
|
|
|
155
168
|
'}'
|
|
156
169
|
;
|
|
157
170
|
|
|
171
|
+
MetadataProperty:
|
|
172
|
+
'metadata' MetadataBody
|
|
173
|
+
;
|
|
174
|
+
|
|
175
|
+
MetadataBody: '{'
|
|
176
|
+
props+=(MetadataAttribute)*
|
|
177
|
+
'}'
|
|
178
|
+
;
|
|
179
|
+
|
|
180
|
+
MetadataAttribute:
|
|
181
|
+
key=IdTerminal value=String
|
|
182
|
+
;
|
|
158
183
|
|
|
159
184
|
// Views -------------------------------------
|
|
160
185
|
|
|
@@ -200,7 +225,15 @@ DynamicViewBody: '{'
|
|
|
200
225
|
;
|
|
201
226
|
|
|
202
227
|
|
|
203
|
-
type StringProperty =
|
|
228
|
+
type StringProperty =
|
|
229
|
+
ElementStringProperty |
|
|
230
|
+
ViewStringProperty |
|
|
231
|
+
RelationStringProperty |
|
|
232
|
+
MetadataAttribute |
|
|
233
|
+
SpecificationElementStringProperty |
|
|
234
|
+
SpecificationRelationshipStringProperty |
|
|
235
|
+
NotationProperty
|
|
236
|
+
;
|
|
204
237
|
|
|
205
238
|
ViewProperty:
|
|
206
239
|
ViewStringProperty | LinkProperty
|
|
@@ -376,16 +409,24 @@ DynamicViewPredicateIterator:
|
|
|
376
409
|
|
|
377
410
|
ViewRuleStyle:
|
|
378
411
|
'style' target=ElementExpressionsIterator '{'
|
|
379
|
-
props+=
|
|
412
|
+
props+=(
|
|
413
|
+
StyleProperty |
|
|
414
|
+
NotationProperty
|
|
415
|
+
)*
|
|
380
416
|
'}';
|
|
381
417
|
|
|
382
418
|
ViewRuleAutoLayout:
|
|
383
419
|
'autoLayout' direction=ViewLayoutDirection;
|
|
384
420
|
|
|
421
|
+
NotationProperty:
|
|
422
|
+
key='notation' ':'? value=String ';'?
|
|
423
|
+
;
|
|
424
|
+
|
|
385
425
|
CustomElementProperties: '{'
|
|
386
426
|
props+=(
|
|
387
427
|
NavigateToProperty |
|
|
388
428
|
ElementStringProperty |
|
|
429
|
+
NotationProperty |
|
|
389
430
|
StyleProperty
|
|
390
431
|
)*
|
|
391
432
|
'}'
|
|
@@ -394,6 +435,7 @@ CustomElementProperties: '{'
|
|
|
394
435
|
CustomRelationProperties: '{'
|
|
395
436
|
props+=(
|
|
396
437
|
RelationStringProperty |
|
|
438
|
+
NotationProperty |
|
|
397
439
|
RelationshipStyleProperty
|
|
398
440
|
)*
|
|
399
441
|
'}'
|
|
@@ -405,7 +447,7 @@ NavigateToProperty:
|
|
|
405
447
|
// Common properties -------------------------------------
|
|
406
448
|
|
|
407
449
|
LinkProperty:
|
|
408
|
-
key='link' ':'? value=Uri ';'?;
|
|
450
|
+
key='link' ':'? value=Uri title=String? ';'?;
|
|
409
451
|
ColorProperty:
|
|
410
452
|
key='color' ':'? value=ThemeColor ';'?;
|
|
411
453
|
|
|
@@ -433,7 +475,7 @@ StyleProperty:
|
|
|
433
475
|
OpacityProperty |
|
|
434
476
|
IconProperty;
|
|
435
477
|
|
|
436
|
-
|
|
478
|
+
ElementStyleProperty:
|
|
437
479
|
key='style' '{'
|
|
438
480
|
props+=StyleProperty*
|
|
439
481
|
'}';
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComputedNode, ViewRule } from '@likec4/core'
|
|
2
|
-
import { Expr
|
|
3
|
-
import { isEmpty,
|
|
2
|
+
import { Expr } from '@likec4/core'
|
|
3
|
+
import { isEmpty, isNullish, omitBy } from 'remeda'
|
|
4
4
|
import { elementExprToPredicate } from './elementExpressionToPredicate'
|
|
5
5
|
|
|
6
6
|
export function applyCustomElementProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
|
|
@@ -15,12 +15,13 @@ export function applyCustomElementProperties(_rules: ViewRule[], _nodes: Compute
|
|
|
15
15
|
} of rules
|
|
16
16
|
) {
|
|
17
17
|
const { border, opacity, ...rest } = omitBy(props, isNullish)
|
|
18
|
+
const notEmpty = !isEmpty(rest)
|
|
18
19
|
const satisfies = elementExprToPredicate(expr)
|
|
19
20
|
nodes.forEach((node, i) => {
|
|
20
21
|
if (!satisfies(node)) {
|
|
21
22
|
return
|
|
22
23
|
}
|
|
23
|
-
if (
|
|
24
|
+
if (notEmpty) {
|
|
24
25
|
node = {
|
|
25
26
|
...node,
|
|
26
27
|
isCustomized: true,
|
|
@@ -25,6 +25,9 @@ export function applyViewRuleStyles(_rules: ViewRule[], nodes: ComputedNode[]) {
|
|
|
25
25
|
if (isDefined(rule.style.icon)) {
|
|
26
26
|
n.icon = rule.style.icon
|
|
27
27
|
}
|
|
28
|
+
if (isDefined(rule.notation)) {
|
|
29
|
+
n.notation = rule.notation
|
|
30
|
+
}
|
|
28
31
|
let styleOverride: ComputedNode['style'] | undefined
|
|
29
32
|
if (isDefined(rule.style.border)) {
|
|
30
33
|
styleOverride = { border: rule.style.border }
|