@likec4/language-server 1.1.1 → 1.2.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.
Files changed (173) hide show
  1. package/contrib/likec4.monarch.ts +4 -4
  2. package/contrib/likec4.tmLanguage.json +1 -1
  3. package/package.json +8 -10
  4. package/src/Rpc.ts +108 -0
  5. package/src/ast.ts +443 -0
  6. package/src/browser/index.ts +30 -0
  7. package/src/elementRef.ts +26 -0
  8. package/src/generated/ast.ts +1632 -0
  9. package/src/generated/grammar.ts +10 -0
  10. package/src/generated/module.ts +32 -0
  11. package/src/index.ts +4 -0
  12. package/src/like-c4.langium +395 -0
  13. package/src/logger.ts +54 -0
  14. package/src/lsp/CodeLensProvider.ts +51 -0
  15. package/src/lsp/DocumentHighlightProvider.ts +12 -0
  16. package/src/lsp/DocumentLinkProvider.test.ts +66 -0
  17. package/src/lsp/DocumentLinkProvider.ts +53 -0
  18. package/src/lsp/DocumentSymbolProvider.ts +201 -0
  19. package/src/lsp/HoverProvider.ts +58 -0
  20. package/{dist/lsp/SemanticTokenProvider.js → src/lsp/SemanticTokenProvider.ts} +58 -43
  21. package/src/lsp/index.ts +6 -0
  22. package/src/model/fqn-computation.ts +47 -0
  23. package/src/model/fqn-index.ts +161 -0
  24. package/src/model/index.ts +5 -0
  25. package/src/model/model-builder.ts +447 -0
  26. package/src/model/model-locator.ts +130 -0
  27. package/src/model/model-parser.ts +580 -0
  28. package/src/model-change/ModelChanges.ts +120 -0
  29. package/src/model-change/changeElementStyle.ts +176 -0
  30. package/src/model-change/changeViewLayout.ts +41 -0
  31. package/src/module.ts +197 -0
  32. package/src/node/index.ts +20 -0
  33. package/src/protocol.ts +87 -0
  34. package/src/references/index.ts +2 -0
  35. package/src/references/scope-computation.ts +142 -0
  36. package/src/references/scope-provider.ts +166 -0
  37. package/src/shared/NodeKindProvider.ts +67 -0
  38. package/src/shared/WorkspaceManager.ts +39 -0
  39. package/src/shared/WorkspaceSymbolProvider.ts +3 -0
  40. package/src/shared/index.ts +3 -0
  41. package/src/test/index.ts +1 -0
  42. package/src/test/testServices.ts +119 -0
  43. package/src/utils/index.ts +1 -0
  44. package/src/utils/printDocs.ts +3 -0
  45. package/src/utils/stringHash.ts +6 -0
  46. package/src/validation/dynamic-view-rule.ts +35 -0
  47. package/src/validation/dynamic-view-step.ts +39 -0
  48. package/src/validation/element.ts +52 -0
  49. package/{dist/validation/index.js → src/validation/index.ts} +25 -17
  50. package/src/validation/property-checks.ts +17 -0
  51. package/src/validation/relation.ts +57 -0
  52. package/src/validation/specification.ts +118 -0
  53. package/src/validation/view-predicates/custom-element-expr.ts +21 -0
  54. package/src/validation/view-predicates/expanded-element.ts +34 -0
  55. package/src/validation/view-predicates/incoming.ts +19 -0
  56. package/src/validation/view-predicates/index.ts +4 -0
  57. package/src/validation/view-predicates/outgoing.ts +19 -0
  58. package/src/validation/view.ts +26 -0
  59. package/src/view-utils/assignNavigateTo.ts +30 -0
  60. package/src/view-utils/index.ts +3 -0
  61. package/src/view-utils/resolve-extended-views.ts +57 -0
  62. package/src/view-utils/resolve-relative-paths.ts +84 -0
  63. package/dist/Rpc.d.ts +0 -10
  64. package/dist/Rpc.js +0 -98
  65. package/dist/ast.d.ts +0 -133
  66. package/dist/ast.js +0 -267
  67. package/dist/browser/index.d.ts +0 -9
  68. package/dist/browser/index.js +0 -16
  69. package/dist/elementRef.d.ts +0 -12
  70. package/dist/elementRef.js +0 -15
  71. package/dist/generated/ast.d.ts +0 -559
  72. package/dist/generated/ast.js +0 -868
  73. package/dist/generated/grammar.d.ts +0 -7
  74. package/dist/generated/grammar.js +0 -3
  75. package/dist/generated/module.d.ts +0 -14
  76. package/dist/generated/module.js +0 -22
  77. package/dist/index.d.ts +0 -5
  78. package/dist/index.js +0 -2
  79. package/dist/logger.d.ts +0 -12
  80. package/dist/logger.js +0 -51
  81. package/dist/lsp/CodeLensProvider.d.ts +0 -10
  82. package/dist/lsp/CodeLensProvider.js +0 -37
  83. package/dist/lsp/DocumentHighlightProvider.d.ts +0 -10
  84. package/dist/lsp/DocumentHighlightProvider.js +0 -10
  85. package/dist/lsp/DocumentLinkProvider.d.ts +0 -11
  86. package/dist/lsp/DocumentLinkProvider.js +0 -41
  87. package/dist/lsp/DocumentLinkProvider.test.d.ts +0 -2
  88. package/dist/lsp/DocumentLinkProvider.test.js +0 -54
  89. package/dist/lsp/DocumentSymbolProvider.d.ts +0 -22
  90. package/dist/lsp/DocumentSymbolProvider.js +0 -184
  91. package/dist/lsp/HoverProvider.d.ts +0 -10
  92. package/dist/lsp/HoverProvider.js +0 -36
  93. package/dist/lsp/SemanticTokenProvider.d.ts +0 -8
  94. package/dist/lsp/index.d.ts +0 -7
  95. package/dist/lsp/index.js +0 -6
  96. package/dist/model/fqn-computation.d.ts +0 -4
  97. package/dist/model/fqn-computation.js +0 -43
  98. package/dist/model/fqn-index.d.ts +0 -26
  99. package/dist/model/fqn-index.js +0 -114
  100. package/dist/model/index.d.ts +0 -6
  101. package/dist/model/index.js +0 -5
  102. package/dist/model/model-builder.d.ts +0 -20
  103. package/dist/model/model-builder.js +0 -352
  104. package/dist/model/model-locator.d.ts +0 -22
  105. package/dist/model/model-locator.js +0 -119
  106. package/dist/model/model-parser.d.ts +0 -27
  107. package/dist/model/model-parser.js +0 -410
  108. package/dist/model-change/ModelChanges.d.ts +0 -15
  109. package/dist/model-change/ModelChanges.js +0 -100
  110. package/dist/model-change/changeElementStyle.d.ts +0 -15
  111. package/dist/model-change/changeElementStyle.js +0 -141
  112. package/dist/model-change/changeViewLayout.d.ts +0 -13
  113. package/dist/model-change/changeViewLayout.js +0 -30
  114. package/dist/module.d.ts +0 -59
  115. package/dist/module.js +0 -121
  116. package/dist/node/index.d.ts +0 -6
  117. package/dist/node/index.js +0 -13
  118. package/dist/protocol.d.ts +0 -58
  119. package/dist/protocol.js +0 -14
  120. package/dist/references/index.d.ts +0 -3
  121. package/dist/references/index.js +0 -2
  122. package/dist/references/scope-computation.d.ts +0 -11
  123. package/dist/references/scope-computation.js +0 -108
  124. package/dist/references/scope-provider.d.ts +0 -18
  125. package/dist/references/scope-provider.js +0 -136
  126. package/dist/shared/NodeKindProvider.d.ts +0 -16
  127. package/dist/shared/NodeKindProvider.js +0 -58
  128. package/dist/shared/WorkspaceManager.d.ts +0 -17
  129. package/dist/shared/WorkspaceManager.js +0 -29
  130. package/dist/shared/WorkspaceSymbolProvider.d.ts +0 -4
  131. package/dist/shared/WorkspaceSymbolProvider.js +0 -3
  132. package/dist/shared/index.d.ts +0 -4
  133. package/dist/shared/index.js +0 -3
  134. package/dist/test/index.d.ts +0 -2
  135. package/dist/test/index.js +0 -1
  136. package/dist/test/testServices.d.ts +0 -23
  137. package/dist/test/testServices.js +0 -102
  138. package/dist/utils/index.d.ts +0 -2
  139. package/dist/utils/index.js +0 -1
  140. package/dist/utils/printDocs.d.ts +0 -3
  141. package/dist/utils/printDocs.js +0 -1
  142. package/dist/utils/stringHash.d.ts +0 -2
  143. package/dist/utils/stringHash.js +0 -5
  144. package/dist/validation/element.d.ts +0 -6
  145. package/dist/validation/element.js +0 -38
  146. package/dist/validation/index.d.ts +0 -3
  147. package/dist/validation/property-checks.d.ts +0 -5
  148. package/dist/validation/property-checks.js +0 -11
  149. package/dist/validation/relation.d.ts +0 -5
  150. package/dist/validation/relation.js +0 -50
  151. package/dist/validation/specification.d.ts +0 -10
  152. package/dist/validation/specification.js +0 -97
  153. package/dist/validation/view-predicates/custom-element-expr.d.ts +0 -5
  154. package/dist/validation/view-predicates/custom-element-expr.js +0 -16
  155. package/dist/validation/view-predicates/expanded-element.d.ts +0 -5
  156. package/dist/validation/view-predicates/expanded-element.js +0 -28
  157. package/dist/validation/view-predicates/incoming.d.ts +0 -5
  158. package/dist/validation/view-predicates/incoming.js +0 -14
  159. package/dist/validation/view-predicates/index.d.ts +0 -5
  160. package/dist/validation/view-predicates/index.js +0 -4
  161. package/dist/validation/view-predicates/outgoing.d.ts +0 -5
  162. package/dist/validation/view-predicates/outgoing.js +0 -14
  163. package/dist/validation/view.d.ts +0 -5
  164. package/dist/validation/view.js +0 -18
  165. package/dist/view-utils/assignNavigateTo.d.ts +0 -3
  166. package/dist/view-utils/assignNavigateTo.js +0 -23
  167. package/dist/view-utils/index.d.ts +0 -4
  168. package/dist/view-utils/index.js +0 -3
  169. package/dist/view-utils/resolve-extended-views.d.ts +0 -7
  170. package/dist/view-utils/resolve-extended-views.js +0 -41
  171. package/dist/view-utils/resolve-relative-paths.d.ts +0 -3
  172. package/dist/view-utils/resolve-relative-paths.js +0 -76
  173. /package/{dist → src}/reset.d.ts +0 -0
@@ -0,0 +1,130 @@
1
+ import type { likec4 as c4 } from '@likec4/core'
2
+ import type { LangiumDocuments } from 'langium'
3
+ import { AstUtils, GrammarUtils } from 'langium'
4
+ import type { Location } from 'vscode-languageserver-protocol'
5
+ import type { ParsedAstElement } from '../ast'
6
+ import { ast, isParsedLikeC4LangiumDocument } from '../ast'
7
+ import type { LikeC4Services } from '../module'
8
+ import { type FqnIndex } from './fqn-index'
9
+
10
+ const { findNodeForKeyword, findNodeForProperty } = GrammarUtils
11
+ const { getDocument } = AstUtils
12
+
13
+ export class LikeC4ModelLocator {
14
+ private fqnIndex: FqnIndex
15
+ private langiumDocuments: LangiumDocuments
16
+
17
+ constructor(private services: LikeC4Services) {
18
+ this.fqnIndex = services.likec4.FqnIndex
19
+ this.langiumDocuments = services.shared.workspace.LangiumDocuments
20
+ }
21
+
22
+ private documents() {
23
+ return this.langiumDocuments.all.filter(isParsedLikeC4LangiumDocument)
24
+ }
25
+
26
+ public getParsedElement(astNode: ast.Element): ParsedAstElement | null {
27
+ const fqn = this.fqnIndex.getFqn(astNode)
28
+ if (!fqn) return null
29
+ const doc = getDocument(astNode)
30
+ if (!isParsedLikeC4LangiumDocument(doc)) {
31
+ return null
32
+ }
33
+ return doc.c4Elements.find(e => e.id === fqn) ?? null
34
+ }
35
+
36
+ public locateElement(fqn: c4.Fqn, property = 'name'): Location | null {
37
+ const entry = this.fqnIndex.byFqn(fqn).head()
38
+ if (!entry) {
39
+ return null
40
+ }
41
+ const propertyNode = findNodeForProperty(entry.el.$cstNode, property) ?? entry.el.$cstNode
42
+ if (!propertyNode) {
43
+ return null
44
+ }
45
+ return {
46
+ uri: entry.doc.uri.toString(),
47
+ range: propertyNode.range
48
+ }
49
+ }
50
+
51
+ public locateRelation(relationId: c4.RelationID): Location | null {
52
+ for (const doc of this.documents()) {
53
+ const relation = doc.c4Relations.find(r => r.id === relationId)
54
+ if (!relation) {
55
+ continue
56
+ }
57
+ const node = this.services.workspace.AstNodeLocator.getAstNode(
58
+ doc.parseResult.value,
59
+ relation.astPath
60
+ )
61
+ if (!ast.isRelation(node)) {
62
+ continue
63
+ }
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, '->')
74
+ targetNode ??= findNodeForProperty(node.$cstNode, 'target')
75
+
76
+ if (!targetNode) {
77
+ return null
78
+ }
79
+
80
+ return {
81
+ uri: doc.uri.toString(),
82
+ range: targetNode.range
83
+ }
84
+ }
85
+ return null
86
+ }
87
+
88
+ public locateViewAst(viewId: c4.ViewID) {
89
+ for (const doc of this.documents()) {
90
+ const view = doc.c4Views.find(r => r.id === viewId)
91
+ if (!view) {
92
+ continue
93
+ }
94
+ const viewAst = this.services.workspace.AstNodeLocator.getAstNode(
95
+ doc.parseResult.value,
96
+ view.astPath
97
+ )
98
+ if (ast.isLikeC4View(viewAst)) {
99
+ return {
100
+ doc,
101
+ view,
102
+ viewAst
103
+ }
104
+ }
105
+ }
106
+ return null
107
+ }
108
+
109
+ public locateView(viewId: c4.ViewID): Location | null {
110
+ const res = this.locateViewAst(viewId)
111
+ if (!res) {
112
+ return null
113
+ }
114
+ 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
+ }
121
+ targetNode ??= findNodeForKeyword(node.$cstNode, 'view')
122
+ if (!targetNode) {
123
+ return null
124
+ }
125
+ return {
126
+ uri: res.doc.uri.toString(),
127
+ range: targetNode.range
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,580 @@
1
+ import { type c4, InvalidModelError, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
2
+ import type { AstNode, LangiumDocument } from 'langium'
3
+ import { AstUtils } from 'langium'
4
+ import { isTruthy } from 'remeda'
5
+ import stripIndent from 'strip-indent'
6
+ import type {
7
+ ChecksFromDiagnostics,
8
+ FqnIndexedDocument,
9
+ ParsedAstDynamicView,
10
+ ParsedAstElement,
11
+ ParsedAstElementView,
12
+ ParsedAstRelation,
13
+ ParsedLikeC4LangiumDocument
14
+ } from '../ast'
15
+ import {
16
+ ast,
17
+ checksFromDiagnostics,
18
+ cleanParsedModel,
19
+ isFqnIndexedDocument,
20
+ parseAstOpacityProperty,
21
+ resolveRelationPoints,
22
+ streamModel,
23
+ toAutoLayout,
24
+ toElementStyle,
25
+ toRelationshipStyleExcludeDefaults,
26
+ ViewOps
27
+ } from '../ast'
28
+ import { elementRef, getFqnElementRef } from '../elementRef'
29
+ import { logError, logger, logWarnError } from '../logger'
30
+ import type { LikeC4Services } from '../module'
31
+ import { stringHash } from '../utils'
32
+ import type { FqnIndex } from './fqn-index'
33
+
34
+ const { getDocument } = AstUtils
35
+
36
+ export type ModelParsedListener = () => void
37
+
38
+ function toSingleLine<T extends string | undefined>(str: T): T {
39
+ return (str ? removeIndent(str).split('\n').join(' ') : undefined) as T
40
+ }
41
+
42
+ function removeIndent<T extends string | undefined>(str: T): T {
43
+ return (str ? stripIndent(str).trim() : undefined) as T
44
+ }
45
+
46
+ type IsValidFn = ChecksFromDiagnostics['isValid']
47
+
48
+ export class LikeC4ModelParser {
49
+ private fqnIndex: FqnIndex
50
+ constructor(private services: LikeC4Services) {
51
+ this.fqnIndex = services.likec4.FqnIndex
52
+ logger.debug(`[ModelParser] Created`)
53
+ }
54
+
55
+ parse(doc: LangiumDocument | LangiumDocument[]): ParsedLikeC4LangiumDocument[] {
56
+ const docs = Array.isArray(doc) ? doc : [doc]
57
+ const result = [] as ParsedLikeC4LangiumDocument[]
58
+ for (const doc of docs) {
59
+ if (!isFqnIndexedDocument(doc)) {
60
+ logger.warn(`Not a FqnIndexedDocument: ${doc.uri.toString(true)}`)
61
+ continue
62
+ }
63
+ try {
64
+ result.push(this.parseLikeC4Document(doc))
65
+ } catch (cause) {
66
+ logError(new InvalidModelError(`Error parsing document ${doc.uri.toString()}`, { cause }))
67
+ }
68
+ }
69
+ return result
70
+ }
71
+
72
+ protected parseLikeC4Document(_doc: FqnIndexedDocument) {
73
+ const doc = cleanParsedModel(_doc)
74
+ const { isValid } = checksFromDiagnostics(doc)
75
+ this.parseSpecification(doc, isValid)
76
+ this.parseModel(doc, isValid)
77
+ this.parseViews(doc, isValid)
78
+ return doc
79
+ }
80
+
81
+ private parseSpecification(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
82
+ const { parseResult, c4Specification } = doc
83
+
84
+ const specifications = parseResult.value.specifications.filter(isValid)
85
+ const element_specs = specifications.flatMap(s => s.elements.filter(isValid))
86
+ for (const { kind, style } of element_specs) {
87
+ try {
88
+ const kindName = kind.name as c4.ElementKind
89
+ c4Specification.kinds[kindName] = {
90
+ ...c4Specification.kinds[kindName],
91
+ ...toElementStyle(style?.props)
92
+ }
93
+ } catch (e) {
94
+ logWarnError(e)
95
+ }
96
+ }
97
+
98
+ const relations_specs = specifications.flatMap(s => s.relationships.filter(isValid))
99
+ for (const { kind, props } of relations_specs) {
100
+ try {
101
+ const kindName = kind.name as c4.RelationshipKind
102
+ c4Specification.relationships[kindName] = {
103
+ ...c4Specification.relationships[kindName],
104
+ ...toRelationshipStyleExcludeDefaults(props)
105
+ }
106
+ } catch (e) {
107
+ logWarnError(e)
108
+ }
109
+ }
110
+ }
111
+
112
+ private parseModel(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
113
+ for (const el of streamModel(doc, isValid)) {
114
+ if (ast.isElement(el)) {
115
+ try {
116
+ doc.c4Elements.push(this.parseElement(el))
117
+ } catch (e) {
118
+ logWarnError(e)
119
+ }
120
+ continue
121
+ }
122
+ if (ast.isRelation(el)) {
123
+ try {
124
+ doc.c4Relations.push(this.parseRelation(el))
125
+ } catch (e) {
126
+ logWarnError(e)
127
+ }
128
+ continue
129
+ }
130
+ nonexhaustive(el)
131
+ }
132
+ }
133
+
134
+ private parseElement(astNode: ast.Element): ParsedAstElement {
135
+ const id = this.resolveFqn(astNode)
136
+ const kind = astNode.kind.$refText as c4.ElementKind
137
+ const tags = this.convertTags(astNode.body)
138
+ const stylePropsAst = astNode.body?.props.find(ast.isStyleProperties)?.props
139
+ const style = toElementStyle(stylePropsAst)
140
+ const astPath = this.getAstNodePath(astNode)
141
+
142
+ let [title, description, technology] = astNode.props ?? []
143
+
144
+ const bodyProps = astNode.body?.props.filter(ast.isElementStringProperty) ?? []
145
+
146
+ title = toSingleLine(title ?? bodyProps.find(p => p.key === 'title')?.value)
147
+ description = removeIndent(description ?? bodyProps.find(p => p.key === 'description')?.value)
148
+ technology = toSingleLine(technology ?? bodyProps.find(p => p.key === 'technology')?.value)
149
+
150
+ const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
151
+
152
+ return {
153
+ id,
154
+ kind,
155
+ astPath,
156
+ title: title ?? astNode.name,
157
+ ...(tags && { tags }),
158
+ ...(links && isNonEmptyArray(links) && { links }),
159
+ ...(isTruthy(technology) && { technology }),
160
+ ...(isTruthy(description) && { description }),
161
+ style
162
+ }
163
+ }
164
+
165
+ private parseRelation(astNode: ast.Relation): ParsedAstRelation {
166
+ const coupling = resolveRelationPoints(astNode)
167
+ const target = this.resolveFqn(coupling.target)
168
+ const source = this.resolveFqn(coupling.source)
169
+ const tags = this.convertTags(astNode) ?? this.convertTags(astNode.body)
170
+ const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
171
+ const kind = astNode.kind?.ref?.name as c4.RelationshipKind
172
+ const astPath = this.getAstNodePath(astNode)
173
+ const title = toSingleLine(
174
+ astNode.title ?? astNode.body?.props.find((p): p is ast.RelationStringProperty => p.key === 'title')?.value
175
+ ) ?? ''
176
+ const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty)
177
+ const id = stringHash(
178
+ astPath,
179
+ source,
180
+ target
181
+ ) as c4.RelationID
182
+ return {
183
+ id,
184
+ astPath,
185
+ source,
186
+ target,
187
+ title,
188
+ ...(kind && { kind }),
189
+ ...(tags && { tags }),
190
+ ...(isNonEmptyArray(links) && { links }),
191
+ ...toRelationshipStyleExcludeDefaults(styleProp?.props)
192
+ }
193
+ }
194
+
195
+ private parseViews(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
196
+ const views = doc.parseResult.value.views.flatMap(v => isValid(v) ? v.views : [])
197
+ for (const view of views) {
198
+ try {
199
+ if (!isValid(view)) {
200
+ continue
201
+ }
202
+ doc.c4Views.push(
203
+ ast.isElementView(view) ? this.parseElementView(view, isValid) : this.parseDynamicElementView(view, isValid)
204
+ )
205
+ } catch (e) {
206
+ logWarnError(e)
207
+ }
208
+ }
209
+ }
210
+
211
+ private parseElementExpr(astNode: ast.ElementExpr): c4.ElementExpression {
212
+ if (ast.isWildcardExpr(astNode)) {
213
+ return {
214
+ wildcard: true
215
+ }
216
+ }
217
+ if (ast.isElementKindExpr(astNode)) {
218
+ // invariant(astNode.kind.ref, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
219
+ return {
220
+ elementKind: astNode.kind.$refText as c4.ElementKind,
221
+ isEqual: astNode.isEqual
222
+ }
223
+ }
224
+ if (ast.isElementTagExpr(astNode)) {
225
+ let elementTag = astNode.tag.$refText
226
+ if (elementTag.startsWith('#')) {
227
+ elementTag = elementTag.slice(1)
228
+ }
229
+ // invariant(astNode.tag.ref, 'ElementTagExpr tag is not resolved: ' + astNode.$cstNode?.text)
230
+ return {
231
+ elementTag: elementTag as c4.Tag,
232
+ isEqual: astNode.isEqual
233
+ }
234
+ }
235
+ if (ast.isExpandElementExpr(astNode)) {
236
+ const elementNode = elementRef(astNode.parent)
237
+ invariant(elementNode, 'Element not found ' + astNode.parent.$cstNode?.text)
238
+ const expanded = this.resolveFqn(elementNode)
239
+ return {
240
+ expanded
241
+ }
242
+ }
243
+ if (ast.isDescedantsExpr(astNode)) {
244
+ const elementNode = elementRef(astNode.parent)
245
+ invariant(elementNode, 'Element not found ' + astNode.parent.$cstNode?.text)
246
+ const element = this.resolveFqn(elementNode)
247
+ return {
248
+ element,
249
+ isDescedants: true
250
+ }
251
+ }
252
+ if (ast.isElementRef(astNode)) {
253
+ const elementNode = elementRef(astNode)
254
+ invariant(elementNode, 'Element not found ' + astNode.$cstNode?.text)
255
+ const element = this.resolveFqn(elementNode)
256
+ return {
257
+ element
258
+ }
259
+ }
260
+ nonexhaustive(astNode)
261
+ }
262
+
263
+ private parseCustomElementExpr(astNode: ast.CustomElementExpr): c4.CustomElementExpr {
264
+ let targetRef
265
+ if (ast.isElementRef(astNode.target)) {
266
+ targetRef = astNode.target
267
+ } else if (ast.isExpandElementExpr(astNode.target)) {
268
+ targetRef = astNode.target.parent
269
+ } else {
270
+ invariant(false, 'ElementRef expected as target of custom element')
271
+ }
272
+ // invariant(ast.isElementRef(astNode.target), 'ElementRef expected as target of custom element')
273
+ const elementNode = elementRef(targetRef)
274
+ invariant(elementNode, 'element not found: ' + astNode.$cstNode?.text)
275
+ const element = this.resolveFqn(elementNode)
276
+ const props = astNode.body?.props ?? []
277
+ return props.reduce(
278
+ (acc, prop) => {
279
+ if (ast.isNavigateToProperty(prop)) {
280
+ const viewId = prop.value.view.$refText
281
+ if (isTruthy(viewId)) {
282
+ acc.custom.navigateTo = viewId as c4.ViewID
283
+ }
284
+ return acc
285
+ }
286
+ if (ast.isElementStringProperty(prop)) {
287
+ const value = prop.key === 'description' ? removeIndent(prop.value) : toSingleLine(prop.value)
288
+ acc.custom[prop.key] = value.trim()
289
+ return acc
290
+ }
291
+ if (ast.isIconProperty(prop)) {
292
+ acc.custom[prop.key] = prop.value as c4.IconUrl
293
+ return acc
294
+ }
295
+ if (ast.isColorProperty(prop)) {
296
+ acc.custom[prop.key] = prop.value
297
+ return acc
298
+ }
299
+ if (ast.isShapeProperty(prop)) {
300
+ acc.custom[prop.key] = prop.value
301
+ return acc
302
+ }
303
+ if (ast.isBorderProperty(prop)) {
304
+ acc.custom[prop.key] = prop.value
305
+ return acc
306
+ }
307
+ if (ast.isOpacityProperty(prop)) {
308
+ acc.custom[prop.key] = parseAstOpacityProperty(prop)
309
+ return acc
310
+ }
311
+
312
+ nonexhaustive(prop)
313
+ },
314
+ {
315
+ custom: {
316
+ element
317
+ }
318
+ } as c4.CustomElementExpr
319
+ )
320
+ }
321
+
322
+ private parsePredicateExpr(astNode: ast.ViewRulePredicateExpr): c4.Expression {
323
+ if (ast.isRelationExpr(astNode)) {
324
+ return {
325
+ source: this.parseElementExpr(astNode.source),
326
+ target: this.parseElementExpr(astNode.target),
327
+ isBidirectional: astNode.isBidirectional
328
+ }
329
+ }
330
+ if (ast.isInOutExpr(astNode)) {
331
+ return {
332
+ inout: this.parseElementExpr(astNode.inout.to)
333
+ }
334
+ }
335
+ if (ast.isOutgoingExpr(astNode)) {
336
+ return {
337
+ outgoing: this.parseElementExpr(astNode.from)
338
+ }
339
+ }
340
+ if (ast.isIncomingExpr(astNode)) {
341
+ return {
342
+ incoming: this.parseElementExpr(astNode.to)
343
+ }
344
+ }
345
+ if (ast.isCustomElementExpr(astNode)) {
346
+ return this.parseCustomElementExpr(astNode)
347
+ }
348
+ if (ast.isElementExpr(astNode)) {
349
+ return this.parseElementExpr(astNode)
350
+ }
351
+ nonexhaustive(astNode)
352
+ }
353
+
354
+ private parseViewRule(astRule: ast.ViewRule, isValid: IsValidFn): c4.ViewRule {
355
+ if (ast.isIncludePredicate(astRule) || ast.isExcludePredicate(astRule)) {
356
+ const exprs = astRule.expressions.flatMap(n => {
357
+ try {
358
+ return isValid(n) ? this.parsePredicateExpr(n) : []
359
+ } catch (e) {
360
+ logWarnError(e)
361
+ return []
362
+ }
363
+ })
364
+ return ast.isIncludePredicate(astRule) ? { include: exprs } : { exclude: exprs }
365
+ }
366
+ if (ast.isViewRuleStyle(astRule)) {
367
+ const styleProps = toElementStyle(astRule.styleprops)
368
+ return {
369
+ targets: astRule.targets.map(n => this.parseElementExpr(n)),
370
+ style: {
371
+ ...styleProps
372
+ }
373
+ }
374
+ }
375
+ if (ast.isViewRuleAutoLayout(astRule)) {
376
+ return {
377
+ autoLayout: toAutoLayout(astRule.direction)
378
+ }
379
+ }
380
+ nonexhaustive(astRule)
381
+ }
382
+
383
+ private parseDynamicStep(node: ast.DynamicViewStep): c4.DynamicViewStep {
384
+ const sourceEl = elementRef(node.source)
385
+ if (!sourceEl) {
386
+ throw new Error('Invalid reference to source')
387
+ }
388
+ const targetEl = elementRef(node.target)
389
+ if (!targetEl) {
390
+ throw new Error('Invalid reference to target')
391
+ }
392
+ let source = this.resolveFqn(sourceEl)
393
+ let target = this.resolveFqn(targetEl)
394
+ if (node.isBackward) {
395
+ ;[source, target] = [target, source]
396
+ }
397
+
398
+ const title = toSingleLine(node.title) ?? null
399
+ return {
400
+ source,
401
+ target,
402
+ title,
403
+ isBackward: node.isBackward
404
+ }
405
+ }
406
+
407
+ private parseElementView(astNode: ast.ElementView, isValid: IsValidFn): ParsedAstElementView {
408
+ const body = astNode.body
409
+ invariant(body, 'ElementView body is not defined')
410
+ const astPath = this.getAstNodePath(astNode)
411
+
412
+ let viewOf = null as c4.Fqn | null
413
+ if ('viewOf' in astNode) {
414
+ const viewOfEl = elementRef(astNode.viewOf)
415
+ const _viewOf = viewOfEl && this.resolveFqn(viewOfEl)
416
+ if (!_viewOf) {
417
+ logger.warn('viewOf is not resolved: ' + astNode.$cstNode?.text)
418
+ } else {
419
+ viewOf = _viewOf
420
+ }
421
+ }
422
+
423
+ let id = astNode.name
424
+ if (!id) {
425
+ id = 'view_' + stringHash(
426
+ getDocument(astNode).uri.toString(),
427
+ astPath,
428
+ viewOf ?? ''
429
+ ) as c4.ViewID
430
+ }
431
+
432
+ const title = toSingleLine(body.props.find(p => p.key === 'title')?.value) ?? null
433
+ const description = removeIndent(body.props.find(p => p.key === 'description')?.value) ?? null
434
+
435
+ const tags = this.convertTags(body)
436
+ const links = body.props.filter(ast.isLinkProperty).map(p => p.value)
437
+
438
+ const view: ParsedAstElementView = {
439
+ __: 'element',
440
+ id: id as c4.ViewID,
441
+ astPath,
442
+ ...(viewOf && { viewOf }),
443
+ title,
444
+ description,
445
+ tags,
446
+ links: isNonEmptyArray(links) ? links : null,
447
+ rules: body.rules.flatMap(n => {
448
+ try {
449
+ return isValid(n) ? this.parseViewRule(n, isValid) : []
450
+ } catch (e) {
451
+ logWarnError(e)
452
+ return []
453
+ }
454
+ })
455
+ }
456
+ ViewOps.writeId(astNode, view.id)
457
+
458
+ if ('extends' in astNode) {
459
+ const extendsView = astNode.extends.view.ref
460
+ invariant(extendsView?.name, 'view extends is not resolved: ' + astNode.$cstNode?.text)
461
+ return Object.assign(view, {
462
+ extends: extendsView.name as c4.ViewID
463
+ })
464
+ }
465
+
466
+ return view
467
+ }
468
+
469
+ private parseDynamicElementView(astNode: ast.DynamicView, isValid: IsValidFn): ParsedAstDynamicView {
470
+ const body = astNode.body
471
+ invariant(body, 'ElementView body is not defined')
472
+ // only valid props
473
+ const props = body.props.filter(isValid)
474
+ const astPath = this.getAstNodePath(astNode)
475
+
476
+ let id = astNode.name
477
+ if (!id) {
478
+ id = 'dynamic_' + stringHash(
479
+ getDocument(astNode).uri.toString(),
480
+ astPath
481
+ ) as c4.ViewID
482
+ }
483
+
484
+ const title = toSingleLine(props.find(p => p.key === 'title')?.value) ?? null
485
+ const description = removeIndent(props.find(p => p.key === 'description')?.value) ?? null
486
+
487
+ const tags = this.convertTags(body)
488
+ const links = props.filter(ast.isLinkProperty).map(p => p.value)
489
+
490
+ ViewOps.writeId(astNode, id as c4.ViewID)
491
+
492
+ return {
493
+ __: 'dynamic',
494
+ id: id as c4.ViewID,
495
+ astPath,
496
+ title,
497
+ description,
498
+ tags,
499
+ links: isNonEmptyArray(links) ? links : null,
500
+ rules: body.rules.reduce((acc, n) => {
501
+ if (!isValid(n)) {
502
+ return acc
503
+ }
504
+ try {
505
+ if (ast.isDynamicViewRulePredicate(n)) {
506
+ const include = [] as (c4.ElementExpression | c4.CustomElementExpr)[]
507
+ for (const expr of n.expressions) {
508
+ if (ast.isElementExpr(expr)) {
509
+ include.push(this.parseElementExpr(expr))
510
+ continue
511
+ }
512
+ if (ast.isCustomElementExpr(expr)) {
513
+ include.push(this.parseCustomElementExpr(expr))
514
+ continue
515
+ }
516
+ }
517
+ if (include.length > 0) {
518
+ acc.push({ include })
519
+ }
520
+ return acc
521
+ }
522
+ if (ast.isViewRuleStyle(n)) {
523
+ const styleProps = toElementStyle(n.styleprops)
524
+ const targets = n.targets.map(n => this.parseElementExpr(n))
525
+ if (targets.length > 0) {
526
+ acc.push({
527
+ targets,
528
+ style: {
529
+ ...styleProps
530
+ }
531
+ })
532
+ }
533
+ return acc
534
+ }
535
+ if (ast.isViewRuleAutoLayout(n)) {
536
+ acc.push({
537
+ autoLayout: toAutoLayout(n.direction)
538
+ })
539
+ return acc
540
+ }
541
+ nonexhaustive(n)
542
+ } catch (e) {
543
+ logWarnError(e)
544
+ return acc
545
+ }
546
+ }, [] as Array<c4.DynamicViewRule>),
547
+ steps: body.steps.reduce((acc, n) => {
548
+ try {
549
+ if (isValid(n)) {
550
+ acc.push(this.parseDynamicStep(n))
551
+ }
552
+ } catch (e) {
553
+ logWarnError(e)
554
+ }
555
+ return acc
556
+ }, [] as c4.DynamicViewStep[])
557
+ }
558
+ }
559
+
560
+ protected resolveFqn(node: ast.Element | ast.ExtendElement) {
561
+ if (ast.isExtendElement(node)) {
562
+ return getFqnElementRef(node.element)
563
+ }
564
+ const fqn = this.fqnIndex.getFqn(node)
565
+ invariant(fqn, `Not indexed element: ${this.getAstNodePath(node)}`)
566
+ return fqn
567
+ }
568
+
569
+ private getAstNodePath(node: AstNode) {
570
+ return this.services.workspace.AstNodeLocator.getAstNodePath(node)
571
+ }
572
+
573
+ private convertTags<E extends { tags?: ast.Tags }>(withTags?: E) {
574
+ if (!withTags) {
575
+ return null
576
+ }
577
+ const tags = withTags.tags?.value.flatMap(({ ref }) => (ref ? (ref.name as c4.Tag) : []))
578
+ return isNonEmptyArray(tags) ? tags : null
579
+ }
580
+ }