@likec4/language-server 1.6.1 → 1.7.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 +23 -19
- package/src/Rpc.ts +1 -1
- package/src/ast.ts +34 -9
- package/src/{browser/index.ts → browser.ts} +4 -1
- package/src/generated/ast.ts +498 -152
- package/src/generated/grammar.ts +2 -2
- package/src/generated/module.ts +1 -1
- package/src/index.ts +1 -1
- package/src/like-c4.langium +116 -44
- package/src/logger.ts +76 -55
- package/src/lsp/DocumentLinkProvider.ts +1 -1
- package/src/lsp/DocumentSymbolProvider.ts +1 -1
- package/src/lsp/HoverProvider.ts +1 -1
- package/src/lsp/SemanticTokenProvider.ts +54 -26
- package/src/model/model-builder.ts +11 -8
- package/src/model/model-locator.ts +12 -25
- package/src/model/model-parser-where.ts +75 -0
- package/src/model/model-parser.ts +168 -68
- package/src/model-change/ModelChanges.ts +2 -3
- package/src/model-change/changeElementStyle.ts +4 -1
- package/src/model-change/changeViewLayout.ts +8 -8
- package/src/model-change/saveManualLayout.ts +4 -6
- package/src/model-graph/LikeC4ModelGraph.ts +50 -48
- package/src/model-graph/compute-view/__test__/fixture.ts +41 -16
- package/src/model-graph/compute-view/compute.ts +135 -69
- package/src/model-graph/compute-view/predicates.ts +232 -136
- package/src/model-graph/dynamic-view/__test__/fixture.ts +5 -1
- package/src/model-graph/dynamic-view/compute.ts +50 -41
- package/src/model-graph/utils/applyCustomElementProperties.ts +31 -29
- package/src/model-graph/utils/applyCustomRelationProperties.ts +52 -15
- package/src/model-graph/utils/elementExpressionToPredicate.ts +8 -3
- package/src/module.ts +4 -18
- package/src/{node/index.ts → node.ts} +1 -1
- package/src/protocol.ts +2 -2
- package/src/shared/NodeKindProvider.ts +4 -2
- package/src/test/setup.ts +13 -0
- package/src/test/testServices.ts +1 -1
- package/src/validation/dynamic-view-rule.ts +12 -12
- package/src/validation/index.ts +6 -6
- package/src/validation/relation.ts +1 -1
- package/src/validation/view-predicates/{custom-element-expr.ts → element-with.ts} +11 -10
- package/src/validation/view-predicates/expanded-element.ts +2 -10
- package/src/validation/view-predicates/incoming.ts +1 -1
- package/src/validation/view-predicates/index.ts +2 -2
- package/src/validation/view-predicates/outgoing.ts +1 -1
- package/src/validation/view-predicates/{custom-relation-expr.ts → relation-with.ts} +2 -2
- package/src/validation/view.ts +8 -9
- package/src/view-utils/manual-layout.ts +65 -72
- package/src/view-utils/resolve-relative-paths.ts +28 -17
- package/src/view-utils/view-hash.ts +33 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { likec4 as c4 } from '@likec4/core'
|
|
2
2
|
import type { LangiumDocuments } from 'langium'
|
|
3
3
|
import { AstUtils, GrammarUtils } from 'langium'
|
|
4
|
-
import type { Location } from 'vscode-languageserver-
|
|
4
|
+
import type { Location } from 'vscode-languageserver-types'
|
|
5
5
|
import type { ParsedAstElement } from '../ast'
|
|
6
6
|
import { ast, isParsedLikeC4LangiumDocument } from '../ast'
|
|
7
7
|
import type { LikeC4Services } from '../module'
|
|
@@ -33,18 +33,15 @@ export class LikeC4ModelLocator {
|
|
|
33
33
|
return doc.c4Elements.find(e => e.id === fqn) ?? null
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
public locateElement(fqn: c4.Fqn,
|
|
36
|
+
public locateElement(fqn: c4.Fqn, _prop?: string): Location | null {
|
|
37
37
|
const entry = this.fqnIndex.byFqn(fqn).head()
|
|
38
|
-
|
|
38
|
+
const docsegment = entry?.nameSegment ?? entry?.selectionSegment
|
|
39
|
+
if (!entry || !docsegment) {
|
|
39
40
|
return null
|
|
40
41
|
}
|
|
41
|
-
// const propertyNode = findNodeForProperty(entry.el.$cstNode, property) ?? entry.el.$cstNode
|
|
42
|
-
// if (!propertyNode) {
|
|
43
|
-
// return null
|
|
44
|
-
// }
|
|
45
42
|
return {
|
|
46
43
|
uri: entry.documentUri.toString(),
|
|
47
|
-
range:
|
|
44
|
+
range: docsegment.range
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
|
|
@@ -61,20 +58,14 @@ export class LikeC4ModelLocator {
|
|
|
61
58
|
if (!ast.isRelation(node)) {
|
|
62
59
|
continue
|
|
63
60
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
uri: doc.uri.toString(),
|
|
69
|
-
range: targetNode.range
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
let targetNode = node.kind ? findNodeForProperty(node.$cstNode, 'kind') : findNodeForKeyword(node.$cstNode, '->')
|
|
61
|
+
|
|
62
|
+
let targetNode = node.title ? findNodeForProperty(node.$cstNode, 'title') : undefined
|
|
63
|
+
targetNode ??= node.kind ? findNodeForProperty(node.$cstNode, 'kind') : undefined
|
|
74
64
|
targetNode ??= findNodeForProperty(node.$cstNode, 'target')
|
|
65
|
+
targetNode ??= node.$cstNode
|
|
75
66
|
|
|
76
67
|
if (!targetNode) {
|
|
77
|
-
|
|
68
|
+
continue
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
return {
|
|
@@ -112,13 +103,9 @@ export class LikeC4ModelLocator {
|
|
|
112
103
|
return null
|
|
113
104
|
}
|
|
114
105
|
const node = res.viewAst
|
|
115
|
-
let targetNode = node.$cstNode
|
|
116
|
-
if (node.name) {
|
|
117
|
-
targetNode = findNodeForProperty(node.$cstNode, 'name') ?? targetNode
|
|
118
|
-
} else if ('viewOf' in node) {
|
|
119
|
-
targetNode = findNodeForProperty(node.$cstNode, 'viewOf') ?? targetNode
|
|
120
|
-
}
|
|
106
|
+
let targetNode = node.name ? findNodeForProperty(node.$cstNode, 'name') : undefined
|
|
121
107
|
targetNode ??= findNodeForKeyword(node.$cstNode, 'view')
|
|
108
|
+
targetNode ??= node.$cstNode
|
|
122
109
|
if (!targetNode) {
|
|
123
110
|
return null
|
|
124
111
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type c4, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
|
|
2
|
+
import { isAndOperator, isOrOperator } from '@likec4/core/types'
|
|
3
|
+
import { ast } from '../ast'
|
|
4
|
+
|
|
5
|
+
const parseEquals = (
|
|
6
|
+
{ operator, not }: ast.WhereKindEqual | ast.WhereTagEqual,
|
|
7
|
+
value: string
|
|
8
|
+
): c4.EqualOperator<string> => {
|
|
9
|
+
if (operator.startsWith('!=')) {
|
|
10
|
+
return {
|
|
11
|
+
neq: value
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (operator.startsWith('=')) {
|
|
15
|
+
return {
|
|
16
|
+
eq: value
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return not ? { neq: value } : { eq: value }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseWhereClause(astNode: ast.WhereExpression): c4.WhereOperator {
|
|
23
|
+
switch (true) {
|
|
24
|
+
case ast.isWhereTagEqual(astNode): {
|
|
25
|
+
const tag = astNode.value?.ref?.name
|
|
26
|
+
invariant(tag, 'Expected tag name')
|
|
27
|
+
return {
|
|
28
|
+
tag: parseEquals(astNode, tag)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
case ast.isWhereKindEqual(astNode): {
|
|
32
|
+
const kind = astNode.value?.ref?.name
|
|
33
|
+
invariant(kind, 'Expected kind name')
|
|
34
|
+
return {
|
|
35
|
+
kind: parseEquals(astNode, kind)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
case ast.isWhereElementNegation(astNode) || ast.isWhereRelationNegation(astNode): {
|
|
39
|
+
return {
|
|
40
|
+
not: parseWhereClause(astNode.value)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
case ast.isWhereBinaryExpression(astNode): {
|
|
44
|
+
const left = parseWhereClause(astNode.left)
|
|
45
|
+
const right = parseWhereClause(astNode.right)
|
|
46
|
+
const operator = astNode.operator.toLowerCase() as Lowercase<ast.WhereBinaryExpression['operator']>
|
|
47
|
+
switch (operator) {
|
|
48
|
+
case 'and': {
|
|
49
|
+
const operands = [
|
|
50
|
+
isAndOperator(left) ? left.and : left,
|
|
51
|
+
isAndOperator(right) ? right.and : right
|
|
52
|
+
].flat()
|
|
53
|
+
invariant(isNonEmptyArray(operands), 'Expected non-empty array')
|
|
54
|
+
return {
|
|
55
|
+
and: operands
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
case 'or': {
|
|
59
|
+
const operands = [
|
|
60
|
+
isOrOperator(left) ? left.or : left,
|
|
61
|
+
isOrOperator(right) ? right.or : right
|
|
62
|
+
].flat()
|
|
63
|
+
invariant(isNonEmptyArray(operands), 'Expected non-empty array')
|
|
64
|
+
return {
|
|
65
|
+
or: operands
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
nonexhaustive(operator)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
nonexhaustive(astNode)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { AstNode, LangiumDocument } from 'langium'
|
|
|
3
3
|
import { AstUtils, CstUtils } from 'langium'
|
|
4
4
|
import { isTruthy, mapToObj } from 'remeda'
|
|
5
5
|
import stripIndent from 'strip-indent'
|
|
6
|
+
import type { Writable } from 'type-fest'
|
|
6
7
|
import type {
|
|
7
8
|
ChecksFromDiagnostics,
|
|
8
9
|
FqnIndexedDocument,
|
|
@@ -29,8 +30,9 @@ import { elementRef, getFqnElementRef } from '../elementRef'
|
|
|
29
30
|
import { logError, logger, logWarnError } from '../logger'
|
|
30
31
|
import type { LikeC4Services } from '../module'
|
|
31
32
|
import { stringHash } from '../utils'
|
|
32
|
-
import { deserializeFromComment } from '../view-utils/manual-layout'
|
|
33
|
+
import { deserializeFromComment, hasManualLayout } from '../view-utils/manual-layout'
|
|
33
34
|
import type { FqnIndex } from './fqn-index'
|
|
35
|
+
import { parseWhereClause } from './model-parser-where'
|
|
34
36
|
|
|
35
37
|
const { getDocument } = AstUtils
|
|
36
38
|
|
|
@@ -142,7 +144,10 @@ export class LikeC4ModelParser {
|
|
|
142
144
|
|
|
143
145
|
let [title, description, technology] = astNode.props ?? []
|
|
144
146
|
|
|
145
|
-
const bodyProps = mapToObj(
|
|
147
|
+
const bodyProps = mapToObj(
|
|
148
|
+
astNode.body?.props.filter(ast.isElementStringProperty) ?? [],
|
|
149
|
+
p => [p.key, p.value || undefined]
|
|
150
|
+
)
|
|
146
151
|
|
|
147
152
|
title = toSingleLine(title ?? bodyProps.title)
|
|
148
153
|
description = removeIndent(bodyProps.description ?? description)
|
|
@@ -180,9 +185,16 @@ export class LikeC4ModelParser {
|
|
|
180
185
|
const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
|
|
181
186
|
const kind = astNode.kind?.ref?.name as c4.RelationshipKind
|
|
182
187
|
const astPath = this.getAstNodePath(astNode)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
|
|
189
|
+
const bodyProps = mapToObj(
|
|
190
|
+
astNode.body?.props.filter(ast.isRelationStringProperty) ?? [],
|
|
191
|
+
p => [p.key, p.value || undefined]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const title = removeIndent(astNode.title ?? bodyProps.title) ?? ''
|
|
195
|
+
const description = removeIndent(bodyProps.description)
|
|
196
|
+
const technology = toSingleLine(bodyProps.technology)
|
|
197
|
+
|
|
186
198
|
const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty)
|
|
187
199
|
const id = stringHash(
|
|
188
200
|
astPath,
|
|
@@ -195,6 +207,8 @@ export class LikeC4ModelParser {
|
|
|
195
207
|
source,
|
|
196
208
|
target,
|
|
197
209
|
title,
|
|
210
|
+
...(isTruthy(technology) && { technology }),
|
|
211
|
+
...(isTruthy(description) && { description }),
|
|
198
212
|
...(kind && { kind }),
|
|
199
213
|
...(tags && { tags }),
|
|
200
214
|
...(isNonEmptyArray(links) && { links }),
|
|
@@ -221,23 +235,33 @@ export class LikeC4ModelParser {
|
|
|
221
235
|
// TODO validate view rules
|
|
222
236
|
private parseViewRulePredicate(astNode: ast.ViewRulePredicate, _isValid: IsValidFn): c4.ViewRulePredicate {
|
|
223
237
|
const exprs = [] as c4.Expression[]
|
|
224
|
-
let
|
|
225
|
-
while (
|
|
238
|
+
let predicate: ast.Predicates | undefined = astNode.predicates
|
|
239
|
+
while (predicate) {
|
|
226
240
|
try {
|
|
227
|
-
if (isTruthy(
|
|
228
|
-
exprs.unshift(this.
|
|
241
|
+
if (isTruthy(predicate.value) && _isValid(predicate.value as any)) {
|
|
242
|
+
exprs.unshift(this.parsePredicate(predicate.value, _isValid))
|
|
229
243
|
}
|
|
230
244
|
} catch (e) {
|
|
231
245
|
logWarnError(e)
|
|
232
246
|
}
|
|
233
|
-
|
|
247
|
+
predicate = predicate.prev
|
|
234
248
|
}
|
|
235
249
|
return ast.isIncludePredicate(astNode) ? { include: exprs } : { exclude: exprs }
|
|
236
250
|
}
|
|
237
251
|
|
|
252
|
+
private parsePredicate(astNode: ast.Predicate, _isValid: IsValidFn): c4.Expression {
|
|
253
|
+
if (ast.isElementPredicate(astNode)) {
|
|
254
|
+
return this.parseElementPredicate(astNode, _isValid)
|
|
255
|
+
}
|
|
256
|
+
if (ast.isRelationPredicate(astNode)) {
|
|
257
|
+
return this.parseRelationPredicate(astNode, _isValid)
|
|
258
|
+
}
|
|
259
|
+
nonexhaustive(astNode)
|
|
260
|
+
}
|
|
261
|
+
|
|
238
262
|
private parseElementExpressionsIterator(astNode: ast.ElementExpressionsIterator): c4.ElementExpression[] {
|
|
239
263
|
const exprs = [] as c4.ElementExpression[]
|
|
240
|
-
let iter: ast.ElementExpressionsIterator
|
|
264
|
+
let iter: ast.ElementExpressionsIterator['prev'] = astNode
|
|
241
265
|
while (iter) {
|
|
242
266
|
try {
|
|
243
267
|
exprs.unshift(this.parseElementExpr(iter.value))
|
|
@@ -249,6 +273,19 @@ export class LikeC4ModelParser {
|
|
|
249
273
|
return exprs
|
|
250
274
|
}
|
|
251
275
|
|
|
276
|
+
private parseElementPredicate(astNode: ast.ElementPredicate, _isValid: IsValidFn): c4.ElementPredicateExpression {
|
|
277
|
+
if (ast.isElementPredicateWith(astNode)) {
|
|
278
|
+
return this.parseElementPredicateWith(astNode, _isValid)
|
|
279
|
+
}
|
|
280
|
+
if (ast.isElementPredicateWhere(astNode)) {
|
|
281
|
+
return this.parseElementPredicateWhere(astNode)
|
|
282
|
+
}
|
|
283
|
+
if (ast.isElementExpression(astNode)) {
|
|
284
|
+
return this.parseElementExpr(astNode)
|
|
285
|
+
}
|
|
286
|
+
nonexhaustive(astNode)
|
|
287
|
+
}
|
|
288
|
+
|
|
252
289
|
private parseElementExpr(astNode: ast.ElementExpression): c4.ElementExpression {
|
|
253
290
|
if (ast.isWildcardExpression(astNode)) {
|
|
254
291
|
return {
|
|
@@ -301,25 +338,12 @@ export class LikeC4ModelParser {
|
|
|
301
338
|
nonexhaustive(astNode)
|
|
302
339
|
}
|
|
303
340
|
|
|
304
|
-
private
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
case ast.isExpandElementExpression(astNode.target):
|
|
311
|
-
targetRef = astNode.target.expand
|
|
312
|
-
break
|
|
313
|
-
case ast.isElementDescedantsExpression(astNode.target):
|
|
314
|
-
targetRef = astNode.target.parent
|
|
315
|
-
break
|
|
316
|
-
default:
|
|
317
|
-
throw new Error('Unsupported target of custom element')
|
|
318
|
-
}
|
|
319
|
-
const elementNode = elementRef(targetRef)
|
|
320
|
-
invariant(elementNode, 'element not found: ' + astNode.$cstNode?.text)
|
|
321
|
-
const element = this.resolveFqn(elementNode)
|
|
322
|
-
const props = astNode.custom.props ?? []
|
|
341
|
+
private parseElementPredicateWith(
|
|
342
|
+
astNode: ast.ElementPredicateWith,
|
|
343
|
+
_isValid: IsValidFn
|
|
344
|
+
): c4.CustomElementExpr {
|
|
345
|
+
const expr = this.parseElementPredicate(astNode.subject, _isValid)
|
|
346
|
+
const props = astNode.custom?.props ?? []
|
|
323
347
|
return props.reduce(
|
|
324
348
|
(acc, prop) => {
|
|
325
349
|
if (ast.isNavigateToProperty(prop)) {
|
|
@@ -362,21 +386,32 @@ export class LikeC4ModelParser {
|
|
|
362
386
|
},
|
|
363
387
|
{
|
|
364
388
|
custom: {
|
|
365
|
-
|
|
389
|
+
expr
|
|
366
390
|
}
|
|
367
391
|
} as c4.CustomElementExpr
|
|
368
392
|
)
|
|
369
393
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
394
|
+
private parseElementPredicateWhere(
|
|
395
|
+
astNode: ast.ElementPredicateWhere
|
|
396
|
+
): c4.ElementWhereExpr {
|
|
397
|
+
const expr = this.parseElementExpr(astNode.subject)
|
|
398
|
+
return {
|
|
399
|
+
where: {
|
|
400
|
+
expr,
|
|
401
|
+
condition: astNode.where ? parseWhereClause(astNode.where) : {
|
|
402
|
+
kind: { neq: '--always-true--' }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
374
405
|
}
|
|
375
|
-
|
|
376
|
-
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private parseRelationPredicate(astNode: ast.RelationPredicate, _isValid: IsValidFn): c4.RelationPredicateExpression {
|
|
409
|
+
if (ast.isRelationPredicateWith(astNode)) {
|
|
410
|
+
const subject = ast.isRelationPredicateWhere(astNode.subject) ? astNode.subject.subject : astNode.subject
|
|
411
|
+
return this.parseRelationPredicateWith(astNode, subject)
|
|
377
412
|
}
|
|
378
|
-
if (ast.
|
|
379
|
-
return this.
|
|
413
|
+
if (ast.isRelationPredicateWhere(astNode)) {
|
|
414
|
+
return this.parseRelationPredicateWhere(astNode)
|
|
380
415
|
}
|
|
381
416
|
if (ast.isRelationExpression(astNode)) {
|
|
382
417
|
return this.parseRelationExpr(astNode)
|
|
@@ -384,15 +419,32 @@ export class LikeC4ModelParser {
|
|
|
384
419
|
nonexhaustive(astNode)
|
|
385
420
|
}
|
|
386
421
|
|
|
387
|
-
private
|
|
388
|
-
|
|
389
|
-
|
|
422
|
+
private parseRelationPredicateWhere(
|
|
423
|
+
astNode: ast.RelationPredicateWhere
|
|
424
|
+
): c4.RelationWhereExpr {
|
|
425
|
+
const expr = this.parseRelationExpr(astNode.subject)
|
|
426
|
+
return {
|
|
427
|
+
where: {
|
|
428
|
+
expr,
|
|
429
|
+
condition: astNode.where ? parseWhereClause(astNode.where) : {
|
|
430
|
+
kind: { neq: '--always-true--' }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private parseRelationPredicateWith(
|
|
437
|
+
astNode: ast.RelationPredicateWith,
|
|
438
|
+
subject: ast.RelationExpression
|
|
439
|
+
): c4.CustomRelationExpr {
|
|
440
|
+
const relation = this.parseRelationExpr(subject)
|
|
441
|
+
const props = astNode.custom?.props ?? []
|
|
390
442
|
return props.reduce(
|
|
391
443
|
(acc, prop) => {
|
|
392
444
|
if (ast.isRelationStringProperty(prop)) {
|
|
393
445
|
const value = removeIndent(prop.value)
|
|
394
446
|
if (isTruthy(value)) {
|
|
395
|
-
acc.customRelation[
|
|
447
|
+
acc.customRelation[prop.key] = value
|
|
396
448
|
}
|
|
397
449
|
return acc
|
|
398
450
|
}
|
|
@@ -467,10 +519,21 @@ export class LikeC4ModelParser {
|
|
|
467
519
|
|
|
468
520
|
private parseViewManualLaout(node: ast.DynamicView | ast.ElementView): c4.ViewManualLayout | undefined {
|
|
469
521
|
const commentNode = CstUtils.findCommentNode(node.$cstNode, ['BLOCK_COMMENT'])
|
|
470
|
-
if (!commentNode) {
|
|
522
|
+
if (!commentNode || !hasManualLayout(commentNode.text)) {
|
|
523
|
+
return undefined
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
return deserializeFromComment(commentNode.text)
|
|
527
|
+
} catch (e) {
|
|
528
|
+
const doc = getDocument(node)
|
|
529
|
+
logger.warn(e)
|
|
530
|
+
logger.warn(
|
|
531
|
+
`Ignoring manual layout of "${node.name ?? 'unnamed'}" at ${doc.uri.fsPath}:${
|
|
532
|
+
1 + (commentNode.range.start.line || 0)
|
|
533
|
+
}`
|
|
534
|
+
)
|
|
471
535
|
return undefined
|
|
472
536
|
}
|
|
473
|
-
return deserializeFromComment(commentNode.text)
|
|
474
537
|
}
|
|
475
538
|
|
|
476
539
|
private parseDynamicStep(node: ast.DynamicViewStep): c4.DynamicViewStep {
|
|
@@ -482,23 +545,55 @@ export class LikeC4ModelParser {
|
|
|
482
545
|
if (!targetEl) {
|
|
483
546
|
throw new Error('Invalid reference to target')
|
|
484
547
|
}
|
|
485
|
-
const title = removeIndent(node.title) ?? null
|
|
486
548
|
let source = this.resolveFqn(sourceEl)
|
|
487
549
|
let target = this.resolveFqn(targetEl)
|
|
550
|
+
const title = removeIndent(
|
|
551
|
+
node.title ?? node.custom?.props.find((p): p is ast.RelationStringProperty => p.key === 'title')?.value
|
|
552
|
+
) ?? ''
|
|
553
|
+
|
|
554
|
+
let step: Writable<c4.DynamicViewStep> = {
|
|
555
|
+
source,
|
|
556
|
+
target,
|
|
557
|
+
title
|
|
558
|
+
}
|
|
488
559
|
if (node.isBackward) {
|
|
489
|
-
|
|
560
|
+
step = {
|
|
490
561
|
source: target,
|
|
491
562
|
target: source,
|
|
492
563
|
title,
|
|
493
564
|
isBackward: true
|
|
494
565
|
}
|
|
495
566
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
567
|
+
if (Array.isArray(node.custom?.props)) {
|
|
568
|
+
for (const prop of node.custom.props) {
|
|
569
|
+
try {
|
|
570
|
+
if (ast.isRelationStringProperty(prop)) {
|
|
571
|
+
const value = removeIndent(prop.value)
|
|
572
|
+
if (isTruthy(value) && prop.key !== 'title') {
|
|
573
|
+
step[prop.key] = value
|
|
574
|
+
}
|
|
575
|
+
continue
|
|
576
|
+
}
|
|
577
|
+
if (ast.isArrowProperty(prop)) {
|
|
578
|
+
step[prop.key] = prop.value
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
if (ast.isColorProperty(prop)) {
|
|
582
|
+
step[prop.key] = prop.value
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
if (ast.isLineProperty(prop)) {
|
|
586
|
+
step[prop.key] = prop.value
|
|
587
|
+
continue
|
|
588
|
+
}
|
|
589
|
+
nonexhaustive(prop)
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
logWarnError(e)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
501
595
|
}
|
|
596
|
+
return step
|
|
502
597
|
}
|
|
503
598
|
|
|
504
599
|
private parseElementView(astNode: ast.ElementView, isValid: IsValidFn): ParsedAstElementView {
|
|
@@ -568,7 +663,7 @@ export class LikeC4ModelParser {
|
|
|
568
663
|
|
|
569
664
|
private parseDynamicElementView(astNode: ast.DynamicView, isValid: IsValidFn): ParsedAstDynamicView {
|
|
570
665
|
const body = astNode.body
|
|
571
|
-
invariant(body, '
|
|
666
|
+
invariant(body, 'DynamicElementView body is not defined')
|
|
572
667
|
// only valid props
|
|
573
668
|
const props = body.props.filter(isValid)
|
|
574
669
|
const astPath = this.getAstNodePath(astNode)
|
|
@@ -604,21 +699,14 @@ export class LikeC4ModelParser {
|
|
|
604
699
|
return acc
|
|
605
700
|
}
|
|
606
701
|
try {
|
|
607
|
-
if (ast.
|
|
608
|
-
const include = [] as
|
|
609
|
-
let iter: ast.
|
|
702
|
+
if (ast.isDynamicViewIncludePredicate(n)) {
|
|
703
|
+
const include = [] as c4.ElementPredicateExpression[]
|
|
704
|
+
let iter: ast.DynamicViewPredicateIterator | undefined = n.predicates
|
|
610
705
|
while (iter) {
|
|
611
706
|
try {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
break
|
|
616
|
-
|
|
617
|
-
case ast.isCustomElementExpression(iter.value):
|
|
618
|
-
isValid(iter.value) && include.unshift(this.parseCustomElementExpr(iter.value))
|
|
619
|
-
break
|
|
620
|
-
default:
|
|
621
|
-
nonexhaustive(iter.value)
|
|
707
|
+
if (isValid(iter.value as any)) {
|
|
708
|
+
const c4expr = this.parseElementPredicate(iter.value, isValid)
|
|
709
|
+
include.unshift(c4expr)
|
|
622
710
|
}
|
|
623
711
|
} catch (e) {
|
|
624
712
|
logWarnError(e)
|
|
@@ -683,10 +771,22 @@ export class LikeC4ModelParser {
|
|
|
683
771
|
}
|
|
684
772
|
|
|
685
773
|
private convertTags<E extends { tags?: ast.Tags }>(withTags?: E) {
|
|
686
|
-
|
|
774
|
+
let iter = withTags?.tags
|
|
775
|
+
if (!iter) {
|
|
687
776
|
return null
|
|
688
777
|
}
|
|
689
|
-
const tags =
|
|
778
|
+
const tags = [] as c4.Tag[]
|
|
779
|
+
while (iter) {
|
|
780
|
+
try {
|
|
781
|
+
const values = iter.values.map(t => t.ref?.name).filter(Boolean) as c4.Tag[]
|
|
782
|
+
if (values.length > 0) {
|
|
783
|
+
tags.unshift(...values)
|
|
784
|
+
}
|
|
785
|
+
} catch (e) {
|
|
786
|
+
// ignore
|
|
787
|
+
}
|
|
788
|
+
iter = iter.prev
|
|
789
|
+
}
|
|
690
790
|
return isNonEmptyArray(tags) ? tags : null
|
|
691
791
|
}
|
|
692
792
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { invariant, nonexhaustive } from '@likec4/core'
|
|
2
|
-
import { Location, Range, TextEdit } from 'vscode-languageserver-
|
|
2
|
+
import { Location, Range, TextEdit } from 'vscode-languageserver-types'
|
|
3
3
|
import { type ParsedLikeC4LangiumDocument } from '../ast'
|
|
4
4
|
import type { LikeC4ModelLocator } from '../model'
|
|
5
5
|
import type { LikeC4Services } from '../module'
|
|
@@ -110,8 +110,7 @@ export class LikeC4ModelChanges {
|
|
|
110
110
|
case 'save-manual-layout':
|
|
111
111
|
const edit = saveManualLayout(this.services, {
|
|
112
112
|
...lookup,
|
|
113
|
-
|
|
114
|
-
edges: change.edges
|
|
113
|
+
layout: change.layout
|
|
115
114
|
})
|
|
116
115
|
return {
|
|
117
116
|
doc: lookup.doc,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Fqn, invariant, isAncestor, type NonEmptyArray, nonNullable, type ViewChanges } from '@likec4/core'
|
|
2
2
|
import { GrammarUtils } from 'langium'
|
|
3
3
|
import { entries, filter, findLast, isTruthy, last } from 'remeda'
|
|
4
|
-
import { type Range, TextEdit } from 'vscode-languageserver-
|
|
4
|
+
import { type Range, TextEdit } from 'vscode-languageserver-types'
|
|
5
5
|
import { ast, type ParsedAstView, type ParsedLikeC4LangiumDocument } from '../ast'
|
|
6
6
|
import type { FqnIndex } from '../model'
|
|
7
7
|
import type { LikeC4Services } from '../module'
|
|
@@ -55,6 +55,9 @@ export function changeElementStyle(services: LikeC4Services, {
|
|
|
55
55
|
modifiedRange: Range
|
|
56
56
|
edits: TextEdit[]
|
|
57
57
|
} {
|
|
58
|
+
// Should never happen
|
|
59
|
+
invariant(viewAst.body, `View ${view.id} has no body`)
|
|
60
|
+
|
|
58
61
|
const viewCstNode = viewAst.$cstNode
|
|
59
62
|
invariant(viewCstNode, 'viewCstNode')
|
|
60
63
|
const insertPos = last(viewAst.body.rules)?.$cstNode?.range.end
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { type AutoLayoutDirection, invariant } from '@likec4/core'
|
|
2
2
|
import { GrammarUtils } from 'langium'
|
|
3
|
-
import {
|
|
4
|
-
import { TextEdit } from 'vscode-languageserver-protocol'
|
|
3
|
+
import { TextEdit } from 'vscode-languageserver-types'
|
|
5
4
|
import { ast, type ParsedAstView, type ParsedLikeC4LangiumDocument, toAstViewLayoutDirection } from '../ast'
|
|
6
5
|
import type { LikeC4Services } from '../module'
|
|
7
6
|
|
|
8
|
-
const { findNodeForProperty } = GrammarUtils
|
|
7
|
+
const { findNodeForProperty, findNodeForKeyword } = GrammarUtils
|
|
9
8
|
|
|
10
9
|
type ChangeViewLayoutArg = {
|
|
11
10
|
view: ParsedAstView
|
|
@@ -15,9 +14,12 @@ type ChangeViewLayoutArg = {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export function changeViewLayout(_services: LikeC4Services, {
|
|
17
|
+
view,
|
|
18
18
|
viewAst,
|
|
19
19
|
layout
|
|
20
20
|
}: ChangeViewLayoutArg): TextEdit {
|
|
21
|
+
// Should never happen
|
|
22
|
+
invariant(viewAst.body, `View ${view.id} has no body`)
|
|
21
23
|
const viewCstNode = viewAst.$cstNode
|
|
22
24
|
invariant(viewCstNode, 'viewCstNode')
|
|
23
25
|
const newlayout = toAstViewLayoutDirection(layout)
|
|
@@ -31,11 +33,9 @@ export function changeViewLayout(_services: LikeC4Services, {
|
|
|
31
33
|
return TextEdit.replace(existingRule.$cstNode.range, `autoLayout ${newlayout}`)
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
const insertPos =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const indent = ' '.repeat(2 + viewCstNode.range.start.character)
|
|
38
|
-
const insert = `\n\n${indent}autoLayout ${newlayout}`
|
|
36
|
+
const insertPos = findNodeForKeyword(viewAst.body.$cstNode, '}')?.range.start
|
|
37
|
+
invariant(insertPos, 'Closing brace not found')
|
|
38
|
+
const insert = `\n autoLayout ${newlayout}\n`
|
|
39
39
|
|
|
40
40
|
return TextEdit.insert(insertPos, insert)
|
|
41
41
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { invariant, type ViewChanges } from '@likec4/core'
|
|
2
2
|
import indentString from 'indent-string'
|
|
3
3
|
import { CstUtils, GrammarUtils } from 'langium'
|
|
4
|
-
import { TextEdit } from 'vscode-languageserver-
|
|
4
|
+
import { TextEdit } from 'vscode-languageserver-types'
|
|
5
5
|
import { ast, type ParsedAstView, type ParsedLikeC4LangiumDocument } from '../ast'
|
|
6
6
|
import type { LikeC4Services } from '../module'
|
|
7
7
|
import { serializeToComment } from '../view-utils/manual-layout'
|
|
@@ -12,18 +12,16 @@ export type ManualLayoutArg = {
|
|
|
12
12
|
view: ParsedAstView
|
|
13
13
|
doc: ParsedLikeC4LangiumDocument
|
|
14
14
|
viewAst: ast.LikeC4View
|
|
15
|
-
|
|
16
|
-
edges: ViewChanges.SaveManualLayout['edges']
|
|
15
|
+
layout: ViewChanges.SaveManualLayout['layout']
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export function saveManualLayout(_services: LikeC4Services, {
|
|
20
19
|
viewAst,
|
|
21
|
-
|
|
22
|
-
edges
|
|
20
|
+
layout
|
|
23
21
|
}: ManualLayoutArg): TextEdit {
|
|
24
22
|
invariant(viewAst.$cstNode, 'invalid view.$cstNode')
|
|
25
23
|
const commentCst = CstUtils.findCommentNode(viewAst.$cstNode, ['BLOCK_COMMENT'])
|
|
26
|
-
let txt = serializeToComment(
|
|
24
|
+
let txt = serializeToComment(layout)
|
|
27
25
|
if (viewAst.$cstNode.range.start.character > 0) {
|
|
28
26
|
txt = indentString(txt, viewAst.$cstNode.range.start.character)
|
|
29
27
|
// const indent = ' '.repeat(viewAst.$cstNode.range.start.character)
|