@likec4/language-server 1.6.0 → 1.7.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 (51) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/package.json +23 -19
  3. package/src/Rpc.ts +1 -1
  4. package/src/ast.ts +34 -9
  5. package/src/{browser/index.ts → browser.ts} +4 -1
  6. package/src/generated/ast.ts +498 -152
  7. package/src/generated/grammar.ts +2 -2
  8. package/src/generated/module.ts +1 -1
  9. package/src/index.ts +1 -1
  10. package/src/like-c4.langium +116 -44
  11. package/src/logger.ts +76 -55
  12. package/src/lsp/DocumentLinkProvider.ts +1 -1
  13. package/src/lsp/DocumentSymbolProvider.ts +1 -1
  14. package/src/lsp/HoverProvider.ts +1 -1
  15. package/src/lsp/SemanticTokenProvider.ts +54 -26
  16. package/src/model/model-builder.ts +11 -8
  17. package/src/model/model-locator.ts +12 -25
  18. package/src/model/model-parser-where.ts +75 -0
  19. package/src/model/model-parser.ts +168 -68
  20. package/src/model-change/ModelChanges.ts +2 -3
  21. package/src/model-change/changeElementStyle.ts +4 -1
  22. package/src/model-change/changeViewLayout.ts +8 -8
  23. package/src/model-change/saveManualLayout.ts +4 -6
  24. package/src/model-graph/LikeC4ModelGraph.ts +50 -48
  25. package/src/model-graph/compute-view/__test__/fixture.ts +41 -16
  26. package/src/model-graph/compute-view/compute.ts +135 -69
  27. package/src/model-graph/compute-view/predicates.ts +232 -136
  28. package/src/model-graph/dynamic-view/__test__/fixture.ts +5 -1
  29. package/src/model-graph/dynamic-view/compute.ts +50 -41
  30. package/src/model-graph/utils/applyCustomElementProperties.ts +31 -29
  31. package/src/model-graph/utils/applyCustomRelationProperties.ts +52 -15
  32. package/src/model-graph/utils/elementExpressionToPredicate.ts +8 -3
  33. package/src/module.ts +4 -18
  34. package/src/{node/index.ts → node.ts} +1 -1
  35. package/src/protocol.ts +2 -2
  36. package/src/shared/NodeKindProvider.ts +4 -2
  37. package/src/test/setup.ts +13 -0
  38. package/src/test/testServices.ts +1 -1
  39. package/src/validation/dynamic-view-rule.ts +12 -12
  40. package/src/validation/index.ts +6 -6
  41. package/src/validation/relation.ts +1 -1
  42. package/src/validation/view-predicates/{custom-element-expr.ts → element-with.ts} +11 -10
  43. package/src/validation/view-predicates/expanded-element.ts +2 -10
  44. package/src/validation/view-predicates/incoming.ts +1 -1
  45. package/src/validation/view-predicates/index.ts +2 -2
  46. package/src/validation/view-predicates/outgoing.ts +1 -1
  47. package/src/validation/view-predicates/{custom-relation-expr.ts → relation-with.ts} +2 -2
  48. package/src/validation/view.ts +8 -9
  49. package/src/view-utils/manual-layout.ts +65 -72
  50. package/src/view-utils/resolve-relative-paths.ts +28 -17
  51. 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-protocol'
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, _property = 'name'): Location | null {
36
+ public locateElement(fqn: c4.Fqn, _prop?: string): Location | null {
37
37
  const entry = this.fqnIndex.byFqn(fqn).head()
38
- if (!entry) {
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: entry.nameSegment?.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
- if (node.title) {
65
- const targetNode = findNodeForProperty(node.$cstNode, 'title')
66
- if (targetNode) {
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
- return null
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(astNode.body?.props.filter(ast.isElementStringProperty) ?? [], p => [p.key, p.value])
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
- const title = removeIndent(
184
- astNode.title ?? astNode.body?.props.find((p): p is ast.RelationStringProperty => p.key === 'title')?.value
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 exprNode: ast.Expressions | undefined = astNode.exprs
225
- while (exprNode) {
238
+ let predicate: ast.Predicates | undefined = astNode.predicates
239
+ while (predicate) {
226
240
  try {
227
- if (isTruthy(exprNode.value)) {
228
- exprs.unshift(this.parseExpression(exprNode.value))
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
- exprNode = exprNode.prev
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 | undefined = astNode
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 parseCustomElementExpr(astNode: ast.CustomElementExpression): c4.CustomElementExpr {
305
- let targetRef
306
- switch (true) {
307
- case ast.isElementRef(astNode.target):
308
- targetRef = astNode.target
309
- break
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
- element
389
+ expr
366
390
  }
367
391
  } as c4.CustomElementExpr
368
392
  )
369
393
  }
370
-
371
- private parseExpression(astNode: ast.Expression): c4.Expression {
372
- if (ast.isCustomRelationExpression(astNode)) {
373
- return this.parseCustomRelationExpr(astNode)
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
- if (ast.isCustomElementExpression(astNode)) {
376
- return this.parseCustomElementExpr(astNode)
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.isElementExpression(astNode)) {
379
- return this.parseElementExpr(astNode)
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 parseCustomRelationExpr(astNode: ast.CustomRelationExpression): c4.CustomRelationExpr {
388
- const relation = this.parseRelationExpr(astNode.relation)
389
- const props = astNode.custom.props ?? []
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['title'] = value
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
- return {
560
+ step = {
490
561
  source: target,
491
562
  target: source,
492
563
  title,
493
564
  isBackward: true
494
565
  }
495
566
  }
496
-
497
- return {
498
- source,
499
- target,
500
- title
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, 'ElementView body is not defined')
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.isDynamicViewRulePredicate(n)) {
608
- const include = [] as (c4.ElementExpression | c4.CustomElementExpr)[]
609
- let iter: ast.DynamicViewRulePredicateIterator | undefined = n.exprs
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
- switch (true) {
613
- case ast.isElementExpression(iter.value):
614
- isValid(iter.value) && include.unshift(this.parseElementExpr(iter.value))
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
- if (!withTags) {
774
+ let iter = withTags?.tags
775
+ if (!iter) {
687
776
  return null
688
777
  }
689
- const tags = withTags.tags?.value.flatMap(({ ref }) => (ref ? (ref.name as c4.Tag) : []))
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-protocol'
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
- nodes: change.nodes,
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-protocol'
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 { last } from 'remeda'
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 = last(viewAst.body.rules)?.$cstNode?.range.end
35
- ?? viewAst.body.$cstNode?.range.end
36
- invariant(insertPos, 'insertPos is not defined')
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-protocol'
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
- nodes: ViewChanges.SaveManualLayout['nodes']
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
- nodes,
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({ nodes, edges })
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)