@likec4/language-server 1.20.1 → 1.20.2

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 (53) hide show
  1. package/README.md +19 -0
  2. package/bin/likec4-language-server.mjs +5 -0
  3. package/dist/LikeC4FileSystem.js +9 -9
  4. package/dist/Rpc.d.ts +2 -4
  5. package/dist/Rpc.js +27 -36
  6. package/dist/ast.d.ts +1 -0
  7. package/dist/ast.js +5 -1
  8. package/dist/bundled.mjs +5924 -0
  9. package/dist/formatting/LikeC4Formatter.d.ts +9 -0
  10. package/dist/formatting/LikeC4Formatter.js +131 -14
  11. package/dist/generated/ast.d.ts +13 -2
  12. package/dist/generated/ast.js +18 -1
  13. package/dist/generated/grammar.js +1 -1
  14. package/dist/lsp/CompletionProvider.js +11 -3
  15. package/dist/model/deployments-index.d.ts +2 -1
  16. package/dist/model/deployments-index.js +3 -10
  17. package/dist/model/fqn-index.d.ts +2 -1
  18. package/dist/model/fqn-index.js +24 -17
  19. package/dist/model/model-builder.d.ts +2 -1
  20. package/dist/model/model-builder.js +32 -30
  21. package/dist/model/model-parser.d.ts +1 -1
  22. package/dist/model/model-parser.js +9 -6
  23. package/dist/model/parser/PredicatesParser.js +7 -1
  24. package/dist/utils/disposable.d.ts +8 -0
  25. package/dist/utils/disposable.js +25 -0
  26. package/dist/utils/index.d.ts +1 -0
  27. package/dist/utils/index.js +1 -0
  28. package/dist/validation/_shared.js +4 -1
  29. package/dist/validation/index.d.ts +2 -2
  30. package/dist/validation/index.js +4 -1
  31. package/dist/validation/specification.d.ts +1 -0
  32. package/dist/validation/specification.js +30 -0
  33. package/package.json +33 -27
  34. package/src/LikeC4FileSystem.ts +14 -13
  35. package/src/Rpc.ts +28 -38
  36. package/src/ast.ts +6 -1
  37. package/src/formatting/LikeC4Formatter.ts +198 -17
  38. package/src/generated/ast.ts +35 -2
  39. package/src/generated/grammar.ts +1 -1
  40. package/src/like-c4.langium +14 -3
  41. package/src/lsp/CompletionProvider.ts +27 -18
  42. package/src/model/deployments-index.ts +4 -17
  43. package/src/model/fqn-index.ts +26 -19
  44. package/src/model/model-builder.ts +32 -31
  45. package/src/model/model-parser.ts +14 -11
  46. package/src/model/parser/PredicatesParser.ts +30 -24
  47. package/src/utils/disposable.ts +30 -0
  48. package/src/utils/index.ts +1 -0
  49. package/src/validation/_shared.ts +5 -2
  50. package/src/validation/index.ts +6 -2
  51. package/src/validation/specification.ts +34 -0
  52. package/contrib/likec4.tmLanguage.json +0 -73
  53. package/dist/like-c4.langium +0 -852
@@ -115,7 +115,11 @@ ElementBody: '{'
115
115
  ;
116
116
 
117
117
  ElementProperty:
118
- ElementStringProperty | ElementStyleProperty | LinkProperty | IconProperty | MetadataProperty;
118
+ ElementStringProperty |
119
+ ElementStyleProperty |
120
+ LinkProperty |
121
+ IconProperty |
122
+ MetadataProperty;
119
123
 
120
124
  ElementStringProperty:
121
125
  key=('title' | 'technology' | 'description') ':'? value=String ';'?;
@@ -711,6 +715,9 @@ ColorProperty:
711
715
  OpacityProperty:
712
716
  key='opacity' ':'? value=Percent ';'?;
713
717
 
718
+ MultipleProperty:
719
+ key='multiple' ':'? value=Boolean ';'?;
720
+
714
721
  // Element properties -------------------------------------
715
722
  IconProperty:
716
723
  key='icon' ':'? (libicon=[LibIcon:IconId] | value=('none'|Uri)) ';'?;
@@ -718,7 +725,6 @@ IconProperty:
718
725
  ShapeProperty:
719
726
  key='shape' ':'? value=ElementShape ';'?;
720
727
 
721
-
722
728
  BorderStyleValue returns string:
723
729
  LineOptions | 'none';
724
730
 
@@ -730,7 +736,8 @@ StyleProperty:
730
736
  ShapeProperty |
731
737
  BorderProperty |
732
738
  OpacityProperty |
733
- IconProperty;
739
+ IconProperty |
740
+ MultipleProperty;
734
741
 
735
742
  ElementStyleProperty:
736
743
  key='style' '{'
@@ -748,6 +755,8 @@ ArrowProperty:
748
755
  RelationshipStyleProperty:
749
756
  ColorProperty | LineProperty | ArrowProperty;
750
757
 
758
+ Boolean returns boolean: 'true' | 'false';
759
+
751
760
  LineOptions returns string:
752
761
  'solid' |
753
762
  'dashed' |
@@ -825,6 +834,8 @@ hidden terminal NL: /[\r\n]+/;
825
834
  // -----------------------------------
826
835
  // Terminals
827
836
  //terminal LineStartWithDash: /(?<=([\r?\n][^\S\r\n]*))-/;
837
+ // terminal Boolean returns boolean: 'true' | 'false';
838
+
828
839
 
829
840
  // LibIcons
830
841
  terminal LIB_ICON: /(aws|azure|gcp|tech):[-\w]*/;
@@ -1,23 +1,32 @@
1
- import { AstUtils, type GrammarAST, type MaybePromise } from 'langium'
1
+ import { type GrammarAST, type MaybePromise, AstUtils } from 'langium'
2
2
  import {
3
3
  type CompletionAcceptor,
4
4
  type CompletionContext,
5
5
  type CompletionProviderOptions,
6
- DefaultCompletionProvider
6
+ DefaultCompletionProvider,
7
7
  } from 'langium/lsp'
8
8
  import { anyPass } from 'remeda'
9
9
  import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver-types'
10
10
  import { ast } from '../ast'
11
11
 
12
+ const STYLE_FIELDS = [
13
+ 'color',
14
+ 'shape',
15
+ 'icon',
16
+ 'border',
17
+ 'opacity',
18
+ 'multiple',
19
+ ].join(',')
20
+
12
21
  export class LikeC4CompletionProvider extends DefaultCompletionProvider {
13
22
  override readonly completionOptions = {
14
- triggerCharacters: ['.']
23
+ triggerCharacters: ['.'],
15
24
  } satisfies CompletionProviderOptions
16
25
 
17
26
  protected override completionForKeyword(
18
27
  context: CompletionContext,
19
28
  keyword: GrammarAST.Keyword,
20
- acceptor: CompletionAcceptor
29
+ acceptor: CompletionAcceptor,
21
30
  ): MaybePromise<void> {
22
31
  if (!this.filterKeyword(context, keyword)) {
23
32
  return
@@ -33,8 +42,8 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
33
42
  '\ttitle \'${2:Untitled}\'',
34
43
  '\t',
35
44
  '\tinclude $0',
36
- '}'
37
- ].join('\n')
45
+ '}',
46
+ ].join('\n'),
38
47
  })
39
48
  }
40
49
  if (['title', 'description', 'technology'].includes(keyword.value)) {
@@ -42,7 +51,7 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
42
51
  label: keyword.value,
43
52
  kind: CompletionItemKind.Property,
44
53
  insertTextFormat: InsertTextFormat.Snippet,
45
- insertText: `${keyword.value} '\${0}'`
54
+ insertText: `${keyword.value} '\${0}'`,
46
55
  })
47
56
  }
48
57
  if (['views', 'specification', 'model', 'deployment', 'with'].includes(keyword.value)) {
@@ -51,7 +60,7 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
51
60
  detail: `Insert ${keyword.value} block`,
52
61
  kind: CompletionItemKind.Module,
53
62
  insertTextFormat: InsertTextFormat.Snippet,
54
- insertText: `${keyword.value} {\n\t$0\n}`
63
+ insertText: `${keyword.value} {\n\t$0\n}`,
55
64
  })
56
65
  }
57
66
  if (keyword.value === 'group') {
@@ -63,8 +72,8 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
63
72
  insertText: [
64
73
  'group \'${1:Title}\' {',
65
74
  '\t$0',
66
- '}'
67
- ].join('\n')
75
+ '}',
76
+ ].join('\n'),
68
77
  })
69
78
  }
70
79
  if (keyword.value === 'dynamic' && AstUtils.hasContainerOfType(context.node, ast.isModelViews)) {
@@ -78,8 +87,8 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
78
87
  '\ttitle \'${2:Untitled}\'',
79
88
  '\t',
80
89
  '\t$0',
81
- '}'
82
- ].join('\n')
90
+ '}',
91
+ ].join('\n'),
83
92
  })
84
93
  }
85
94
  if (keyword.value === 'style' && context.node) {
@@ -89,7 +98,7 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
89
98
  detail: `Insert ${keyword.value} block`,
90
99
  kind: CompletionItemKind.Module,
91
100
  insertTextFormat: InsertTextFormat.Snippet,
92
- insertText: `${keyword.value} \${1:name} \${2:*} {\n\t\${3|color,shape,border,opacity,icon|} $0\n}`
101
+ insertText: `${keyword.value} \${1:name} \${2:*} {\n\t\${3|${STYLE_FIELDS}|} $0\n}`,
93
102
  })
94
103
  }
95
104
  if (AstUtils.hasContainerOfType(context.node, anyPass([ast.isModelViews, ast.isGlobalStyleGroup]))) {
@@ -98,7 +107,7 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
98
107
  detail: `Insert ${keyword.value} block`,
99
108
  kind: CompletionItemKind.Module,
100
109
  insertTextFormat: InsertTextFormat.Snippet,
101
- insertText: `${keyword.value} \${1:*} {\n\t\${2|color,shape,border,opacity,icon|} $0\n}`
110
+ insertText: `${keyword.value} \${1:*} {\n\t\${2|${STYLE_FIELDS}|} $0\n}`,
102
111
  })
103
112
  }
104
113
  return acceptor(context, {
@@ -106,7 +115,7 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
106
115
  detail: `Insert ${keyword.value} block`,
107
116
  kind: CompletionItemKind.Module,
108
117
  insertTextFormat: InsertTextFormat.Snippet,
109
- insertText: `${keyword.value} {\n\t\${1|color,shape,border,opacity,icon|} $0\n}`
118
+ insertText: `${keyword.value} {\n\t\${1|${STYLE_FIELDS}|} $0\n}`,
110
119
  })
111
120
  }
112
121
  if (keyword.value === 'extend') {
@@ -115,7 +124,7 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
115
124
  detail: `Extend another view`,
116
125
  kind: CompletionItemKind.Class,
117
126
  insertTextFormat: InsertTextFormat.Snippet,
118
- insertText: 'extend ${1:element} {\n\t$0\n}'
127
+ insertText: 'extend ${1:element} {\n\t$0\n}',
119
128
  })
120
129
  }
121
130
 
@@ -124,14 +133,14 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
124
133
  label: keyword.value,
125
134
  kind: CompletionItemKind.Class,
126
135
  insertTextFormat: InsertTextFormat.Snippet,
127
- insertText: 'autoLayout ${1|TopBottom,BottomTop,LeftRight,RightLeft|}$0'
136
+ insertText: 'autoLayout ${1|TopBottom,BottomTop,LeftRight,RightLeft|}$0',
128
137
  })
129
138
  }
130
139
  acceptor(context, {
131
140
  label: keyword.value,
132
141
  kind: this.getKeywordCompletionItemKind(keyword),
133
142
  detail: 'Keyword',
134
- sortText: '1'
143
+ sortText: '1',
135
144
  })
136
145
  }
137
146
  }
@@ -1,5 +1,5 @@
1
1
  import type { Fqn } from '@likec4/core'
2
- import type { LangiumDocument, LangiumDocuments, Stream } from 'langium'
2
+ import type { DocumentCache, LangiumDocuments, Stream } from 'langium'
3
3
  import { AstUtils, DocumentState, MultiMap } from 'langium'
4
4
  import { forEachObj, groupBy, isTruthy, pipe, prop } from 'remeda'
5
5
  import {
@@ -13,28 +13,15 @@ import { logWarnError } from '../logger'
13
13
  import type { LikeC4Services } from '../module'
14
14
  import type { LikeC4NameProvider } from '../references'
15
15
 
16
- const DeploymentsIndexKey = Symbol.for('DeploymentsIndex')
17
-
18
- type IndexedDocument = LangiumDocument & {
19
- [DeploymentsIndexKey]?: DocumentDeploymentsIndex
20
- }
21
-
22
16
  export class DeploymentsIndex {
23
17
  protected Names: LikeC4NameProvider
24
18
  protected langiumDocuments: LangiumDocuments
19
+ protected documentCache: DocumentCache<string, DocumentDeploymentsIndex>
25
20
 
26
21
  constructor(private services: LikeC4Services) {
27
22
  this.Names = services.references.NameProvider
28
23
  this.langiumDocuments = services.shared.workspace.LangiumDocuments
29
-
30
- services.shared.workspace.DocumentBuilder.onBuildPhase(
31
- DocumentState.IndexedContent,
32
- (docs, _cancelToken) => {
33
- for (const doc of docs) {
34
- delete (doc as IndexedDocument)[DeploymentsIndexKey]
35
- }
36
- },
37
- )
24
+ this.documentCache = services.DocumentCache
38
25
  }
39
26
 
40
27
  private documents() {
@@ -47,7 +34,7 @@ export class DeploymentsIndex {
47
34
  if (document.state < DocumentState.IndexedContent) {
48
35
  logWarnError(`Document ${document.uri.path} is not indexed`)
49
36
  }
50
- return (document as IndexedDocument)[DeploymentsIndexKey] ??= this.createDocumentIndex(document)
37
+ return this.documentCache.get(document.uri, 'DeploymentsIndex', () => this.createDocumentIndex(document))
51
38
  }
52
39
  /**
53
40
  * Nested elements (nodes/artifacts) of the node
@@ -6,6 +6,7 @@ import type { ast, DocFqnIndexAstNodeDescription, FqnIndexedDocument } from '../
6
6
  import { ElementOps, isFqnIndexedDocument, isLikeC4LangiumDocument } from '../ast'
7
7
  import { logger, logWarnError } from '../logger'
8
8
  import type { LikeC4Services } from '../module'
9
+ import { ADisposable } from '../utils'
9
10
  import { computeDocumentFqn } from './fqn-computation'
10
11
 
11
12
  export interface FqnIndexEntry {
@@ -16,31 +17,37 @@ export interface FqnIndexEntry {
16
17
  path: string
17
18
  }
18
19
 
19
- export class FqnIndex {
20
+ export class FqnIndex extends ADisposable {
20
21
  protected langiumDocuments: LangiumDocuments
21
22
 
22
23
  constructor(private services: LikeC4Services) {
24
+ super()
23
25
  this.langiumDocuments = services.shared.workspace.LangiumDocuments
24
26
 
25
- services.shared.workspace.DocumentBuilder.onBuildPhase(
26
- DocumentState.IndexedContent,
27
- async (docs, _cancelToken) => {
28
- for (const doc of docs) {
29
- if (isLikeC4LangiumDocument(doc)) {
30
- delete doc.c4fqnIndex
31
- delete doc.c4Elements
32
- delete doc.c4Specification
33
- delete doc.c4Relations
34
- delete doc.c4Views
35
- try {
36
- computeDocumentFqn(doc, services)
37
- } catch (e) {
38
- logWarnError(e)
27
+ this.onDispose(
28
+ services.shared.workspace.DocumentBuilder.onBuildPhase(
29
+ DocumentState.IndexedContent,
30
+ async (docs, _cancelToken) => {
31
+ for (const doc of docs) {
32
+ if (isLikeC4LangiumDocument(doc)) {
33
+ delete doc.c4fqnIndex
34
+ delete doc.c4Elements
35
+ delete doc.c4Specification
36
+ delete doc.c4Relations
37
+ delete doc.c4Deployments
38
+ delete doc.c4DeploymentRelations
39
+ delete doc.c4Globals
40
+ delete doc.c4Views
41
+ try {
42
+ computeDocumentFqn(doc, services)
43
+ } catch (e) {
44
+ logWarnError(e)
45
+ }
39
46
  }
40
47
  }
41
- }
42
- return await Promise.resolve()
43
- }
48
+ return await Promise.resolve()
49
+ },
50
+ ),
44
51
  )
45
52
  logger.debug(`[FqnIndex] Created`)
46
53
  }
@@ -125,7 +132,7 @@ export class FqnIndex {
125
132
  return iterator.next()
126
133
  }
127
134
  return DONE_RESULT as IteratorResult<AstNodeDescription>
128
- }
135
+ },
129
136
  )
130
137
  }
131
138
  }
@@ -19,6 +19,7 @@ import {
19
19
  flatMap,
20
20
  groupBy,
21
21
  indexBy,
22
+ isBoolean,
22
23
  isDefined,
23
24
  isEmpty,
24
25
  isNonNullish,
@@ -28,7 +29,7 @@ import {
28
29
  map,
29
30
  mapToObj,
30
31
  mapValues,
31
- pick,
32
+ omit,
32
33
  pipe,
33
34
  prop,
34
35
  reduce,
@@ -48,6 +49,7 @@ import type {
48
49
  import { isParsedLikeC4LangiumDocument } from '../ast'
49
50
  import { logger, logWarnError } from '../logger'
50
51
  import type { LikeC4Services } from '../module'
52
+ import { ADisposable } from '../utils'
51
53
  import { assignNavigateTo, resolveRelativePaths } from '../view-utils'
52
54
 
53
55
  function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4Model {
@@ -115,6 +117,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
115
117
  icon,
116
118
  opacity,
117
119
  border,
120
+ multiple,
118
121
  },
119
122
  id,
120
123
  kind,
@@ -136,6 +139,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
136
139
  opacity ??= __kind.style.opacity
137
140
  border ??= __kind.style.border
138
141
  technology ??= __kind.technology
142
+ multiple ??= __kind.style.multiple
139
143
  return {
140
144
  ...(color && { color }),
141
145
  ...(shape && { shape }),
@@ -144,6 +148,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
144
148
  ...(__kind.notation && { notation: __kind.notation }),
145
149
  style: {
146
150
  ...(border && { border }),
151
+ ...(isBoolean(multiple) && { multiple }),
147
152
  ...(isNumber(opacity) && { opacity }),
148
153
  },
149
154
  links,
@@ -449,37 +454,41 @@ const CACHE_KEY_COMPUTED_MODEL = 'ComputedLikeC4Model'
449
454
 
450
455
  type ModelParsedListener = (docs: URI[]) => void
451
456
 
452
- export class LikeC4ModelBuilder {
457
+ export class LikeC4ModelBuilder extends ADisposable {
453
458
  private langiumDocuments: LangiumDocuments
454
459
  private listeners: ModelParsedListener[] = []
455
460
 
456
461
  constructor(private services: LikeC4Services) {
462
+ super()
457
463
  this.langiumDocuments = services.shared.workspace.LangiumDocuments
458
464
  const parser = services.likec4.ModelParser
459
465
 
460
- services.shared.workspace.DocumentBuilder.onUpdate((_changed, deleted) => {
461
- if (deleted.length > 0) {
462
- this.notifyListeners(deleted)
463
- }
464
- })
465
-
466
- services.shared.workspace.DocumentBuilder.onBuildPhase(
467
- DocumentState.Validated,
468
- async (docs, _cancelToken) => {
469
- let parsed = [] as URI[]
470
- try {
466
+ this.onDispose(
467
+ services.shared.workspace.DocumentBuilder.onUpdate((_changed, deleted) => {
468
+ if (deleted.length > 0) {
469
+ this.notifyListeners(deleted)
470
+ }
471
+ }),
472
+ )
473
+ this.onDispose(
474
+ services.shared.workspace.DocumentBuilder.onBuildPhase(
475
+ DocumentState.Validated,
476
+ async (docs, _cancelToken) => {
477
+ let parsed = [] as URI[]
471
478
  logger.debug(`[ModelBuilder] onValidated (${docs.length} docs)`)
472
479
  for (const doc of docs) {
473
- parsed.push(parser.parse(doc).uri)
480
+ try {
481
+ parsed.push(parser.parse(doc).uri)
482
+ } catch (e) {
483
+ logWarnError(e)
484
+ }
474
485
  }
475
- } catch (e) {
476
- logWarnError(e)
477
- }
478
- if (parsed.length > 0) {
479
- this.notifyListeners(parsed)
480
- }
481
- return await Promise.resolve()
482
- },
486
+ await interruptAndCheck(_cancelToken)
487
+ if (parsed.length > 0) {
488
+ this.notifyListeners(parsed)
489
+ }
490
+ },
491
+ ),
483
492
  )
484
493
  logger.debug(`[ModelBuilder] Created`)
485
494
  }
@@ -546,15 +555,7 @@ export class LikeC4ModelBuilder {
546
555
  })
547
556
  this.previousViews = { ...views }
548
557
  return {
549
- ...structuredClone(
550
- pick(model, [
551
- 'specification',
552
- 'elements',
553
- 'relations',
554
- 'globals',
555
- 'deployments',
556
- ]),
557
- ),
558
+ ...omit(model, ['views']),
558
559
  views,
559
560
  }
560
561
  })
@@ -1,6 +1,6 @@
1
1
  import { invariant } from '@likec4/core'
2
- import type { LangiumDocument } from 'langium'
3
- import DefaultWeakMap from 'mnemonist/default-weak-map'
2
+ import { type LangiumDocument, DocumentCache, DocumentState } from 'langium'
3
+ import { DefaultWeakMap } from 'mnemonist'
4
4
  import { pipe } from 'remeda'
5
5
  import type { LikeC4DocumentProps, ParsedLikeC4LangiumDocument } from '../ast'
6
6
  import { isFqnIndexedDocument } from '../ast'
@@ -26,18 +26,17 @@ const DocumentParserFromMixins = pipe(
26
26
  PredicatesParser,
27
27
  SpecificationParser,
28
28
  ViewsParser,
29
- GlobalsParser
29
+ GlobalsParser,
30
30
  )
31
31
 
32
32
  export class DocumentParser extends DocumentParserFromMixins {
33
33
  }
34
34
 
35
35
  export class LikeC4ModelParser {
36
- private cachedParsers = new DefaultWeakMap<LangiumDocument, DocumentParser>((doc: LangiumDocument) =>
37
- new DocumentParser(this.services, doc as ParsedLikeC4LangiumDocument)
38
- )
36
+ private cachedParsers: DocumentCache<string, DocumentParser>
39
37
 
40
38
  constructor(private services: LikeC4Services) {
39
+ this.cachedParsers = new DocumentCache(services.shared, DocumentState.Validated)
41
40
  }
42
41
 
43
42
  parse(doc: LangiumDocument): ParsedLikeC4LangiumDocument {
@@ -49,7 +48,7 @@ export class LikeC4ModelParser {
49
48
  elements: {},
50
49
  relationships: {},
51
50
  colors: {},
52
- deployments: {}
51
+ deployments: {},
53
52
  },
54
53
  c4Elements: [],
55
54
  c4Relations: [],
@@ -58,12 +57,12 @@ export class LikeC4ModelParser {
58
57
  c4Globals: {
59
58
  predicates: {},
60
59
  dynamicPredicates: {},
61
- styles: {}
60
+ styles: {},
62
61
  },
63
- c4Views: []
62
+ c4Views: [],
64
63
  }
65
64
  doc = Object.assign(doc, props)
66
- const parser = this.cachedParsers.get(doc)
65
+ const parser = this.forDocument(doc)
67
66
  parser.parseSpecification()
68
67
  parser.parseModel()
69
68
  parser.parseGlobals()
@@ -77,6 +76,10 @@ export class LikeC4ModelParser {
77
76
 
78
77
  forDocument(doc: LangiumDocument): DocumentParser {
79
78
  invariant(isFqnIndexedDocument(doc), `Not a FqnIndexedDocument: ${doc.uri.toString(true)}`)
80
- return this.cachedParsers.get(doc)
79
+ return this.cachedParsers.get(
80
+ doc.uri,
81
+ 'DocumentParser',
82
+ () => new DocumentParser(this.services, doc as ParsedLikeC4LangiumDocument),
83
+ )
81
84
  }
82
85
  }
@@ -1,6 +1,6 @@
1
1
  import type * as c4 from '@likec4/core'
2
2
  import { invariant, nonexhaustive } from '@likec4/core'
3
- import { isDefined, isTruthy } from 'remeda'
3
+ import { isBoolean, isDefined, isTruthy } from 'remeda'
4
4
  import { ast, parseAstOpacityProperty, toColor } from '../../ast'
5
5
  import { logWarnError } from '../../logger'
6
6
  import { elementRef } from '../../utils/elementRef'
@@ -53,14 +53,14 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
53
53
  parseElementExpression(astNode: ast.ElementExpression): c4.ElementExpression {
54
54
  if (ast.isWildcardExpression(astNode)) {
55
55
  return {
56
- wildcard: true
56
+ wildcard: true,
57
57
  }
58
58
  }
59
59
  if (ast.isElementKindExpression(astNode)) {
60
60
  invariant(astNode.kind?.ref, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
61
61
  return {
62
62
  elementKind: astNode.kind.ref.name as c4.ElementKind,
63
- isEqual: astNode.isEqual
63
+ isEqual: astNode.isEqual,
64
64
  }
65
65
  }
66
66
  if (ast.isElementTagExpression(astNode)) {
@@ -71,7 +71,7 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
71
71
  }
72
72
  return {
73
73
  elementTag: elementTag as c4.Tag,
74
- isEqual: astNode.isEqual
74
+ isEqual: astNode.isEqual,
75
75
  }
76
76
  }
77
77
  if (ast.isExpandElementExpression(astNode)) {
@@ -79,7 +79,7 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
79
79
  invariant(elementNode, 'Element not found ' + astNode.expand.$cstNode?.text)
80
80
  const expanded = this.resolveFqn(elementNode)
81
81
  return {
82
- expanded
82
+ expanded,
83
83
  }
84
84
  }
85
85
  if (ast.isElementDescedantsExpression(astNode)) {
@@ -89,7 +89,7 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
89
89
  return {
90
90
  element,
91
91
  isChildren: astNode.suffix === '.*',
92
- isDescendants: astNode.suffix === '.**'
92
+ isDescendants: astNode.suffix === '.**',
93
93
  }
94
94
  }
95
95
  if (ast.isElementRef(astNode)) {
@@ -97,7 +97,7 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
97
97
  invariant(elementNode, 'Element not found ' + astNode.$cstNode?.text)
98
98
  const element = this.resolveFqn(elementNode)
99
99
  return {
100
- element
100
+ element,
101
101
  }
102
102
  }
103
103
  nonexhaustive(astNode)
@@ -109,9 +109,9 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
109
109
  where: {
110
110
  expr,
111
111
  condition: astNode.where ? parseWhereClause(astNode.where) : {
112
- kind: { neq: '--always-true--' }
113
- }
114
- }
112
+ kind: { neq: '--always-true--' },
113
+ },
114
+ },
115
115
  }
116
116
  }
117
117
 
@@ -174,13 +174,19 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
174
174
  }
175
175
  return acc
176
176
  }
177
+ if (ast.isMultipleProperty(prop)) {
178
+ if (isBoolean(prop.value)) {
179
+ acc.custom[prop.key] = prop.value
180
+ }
181
+ return acc
182
+ }
177
183
  nonexhaustive(prop)
178
184
  },
179
185
  {
180
186
  custom: {
181
- expr
182
- }
183
- } as c4.CustomElementExpr
187
+ expr,
188
+ },
189
+ } as c4.CustomElementExpr,
184
190
  )
185
191
  }
186
192
 
@@ -207,15 +213,15 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
207
213
  where: {
208
214
  expr,
209
215
  condition: astNode.where ? parseWhereClause(astNode.where) : {
210
- kind: { neq: '--always-true--' }
211
- }
212
- }
216
+ kind: { neq: '--always-true--' },
217
+ },
218
+ },
213
219
  }
214
220
  }
215
221
 
216
222
  parseRelationPredicateWith(
217
223
  astNode: ast.RelationPredicateWith,
218
- relation: c4.RelationExpression | c4.RelationWhereExpr
224
+ relation: c4.RelationExpression | c4.RelationWhereExpr,
219
225
  ): c4.CustomRelationExpr {
220
226
  const props = astNode.custom?.props ?? []
221
227
  return props.reduce(
@@ -256,9 +262,9 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
256
262
  },
257
263
  {
258
264
  customRelation: {
259
- relation
260
- }
261
- } as c4.CustomRelationExpr
265
+ relation,
266
+ },
267
+ } as c4.CustomRelationExpr,
262
268
  )
263
269
  }
264
270
 
@@ -267,22 +273,22 @@ export function PredicatesParser<TBase extends Base>(B: TBase) {
267
273
  return {
268
274
  source: this.parseElementExpression(astNode.source.from),
269
275
  target: this.parseElementExpression(astNode.target),
270
- isBidirectional: astNode.source.isBidirectional
276
+ isBidirectional: astNode.source.isBidirectional,
271
277
  }
272
278
  }
273
279
  if (ast.isInOutRelationExpression(astNode)) {
274
280
  return {
275
- inout: this.parseElementExpression(astNode.inout.to)
281
+ inout: this.parseElementExpression(astNode.inout.to),
276
282
  }
277
283
  }
278
284
  if (ast.isOutgoingRelationExpression(astNode)) {
279
285
  return {
280
- outgoing: this.parseElementExpression(astNode.from)
286
+ outgoing: this.parseElementExpression(astNode.from),
281
287
  }
282
288
  }
283
289
  if (ast.isIncomingRelationExpression(astNode)) {
284
290
  return {
285
- incoming: this.parseElementExpression(astNode.to)
291
+ incoming: this.parseElementExpression(astNode.to),
286
292
  }
287
293
  }
288
294
  nonexhaustive(astNode)