@likec4/language-server 1.2.0 → 1.2.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 (173) hide show
  1. package/package.json +8 -10
  2. package/src/Rpc.ts +108 -0
  3. package/src/ast.ts +443 -0
  4. package/src/browser/index.ts +30 -0
  5. package/src/elementRef.ts +26 -0
  6. package/src/generated/ast.ts +1632 -0
  7. package/src/generated/grammar.ts +10 -0
  8. package/src/generated/module.ts +32 -0
  9. package/src/index.ts +4 -0
  10. package/src/like-c4.langium +395 -0
  11. package/src/logger.ts +54 -0
  12. package/src/lsp/CodeLensProvider.ts +51 -0
  13. package/src/lsp/DocumentHighlightProvider.ts +12 -0
  14. package/src/lsp/DocumentLinkProvider.test.ts +66 -0
  15. package/src/lsp/DocumentLinkProvider.ts +53 -0
  16. package/src/lsp/DocumentSymbolProvider.ts +201 -0
  17. package/src/lsp/HoverProvider.ts +58 -0
  18. package/{dist/lsp/SemanticTokenProvider.js → src/lsp/SemanticTokenProvider.ts} +57 -42
  19. package/src/lsp/index.ts +6 -0
  20. package/src/model/fqn-computation.ts +47 -0
  21. package/src/model/fqn-index.ts +161 -0
  22. package/src/model/index.ts +5 -0
  23. package/src/model/model-builder.ts +447 -0
  24. package/src/model/model-locator.ts +130 -0
  25. package/src/model/model-parser.ts +580 -0
  26. package/src/model-change/ModelChanges.ts +120 -0
  27. package/src/model-change/changeElementStyle.ts +176 -0
  28. package/src/model-change/changeViewLayout.ts +41 -0
  29. package/src/module.ts +197 -0
  30. package/src/node/index.ts +20 -0
  31. package/src/protocol.ts +87 -0
  32. package/src/references/index.ts +2 -0
  33. package/src/references/scope-computation.ts +142 -0
  34. package/src/references/scope-provider.ts +166 -0
  35. package/src/shared/NodeKindProvider.ts +67 -0
  36. package/src/shared/WorkspaceManager.ts +39 -0
  37. package/src/shared/WorkspaceSymbolProvider.ts +3 -0
  38. package/src/shared/index.ts +3 -0
  39. package/src/test/index.ts +1 -0
  40. package/src/test/testServices.ts +119 -0
  41. package/src/utils/index.ts +1 -0
  42. package/src/utils/printDocs.ts +3 -0
  43. package/src/utils/stringHash.ts +6 -0
  44. package/{dist/validation/dynamic-view-rule.js → src/validation/dynamic-view-rule.ts} +14 -11
  45. package/src/validation/dynamic-view-step.ts +39 -0
  46. package/src/validation/element.ts +52 -0
  47. package/{dist/validation/index.js → src/validation/index.ts} +22 -18
  48. package/src/validation/property-checks.ts +17 -0
  49. package/src/validation/relation.ts +57 -0
  50. package/src/validation/specification.ts +118 -0
  51. package/src/validation/view-predicates/custom-element-expr.ts +21 -0
  52. package/{dist/validation/view-predicates/expanded-element.js → src/validation/view-predicates/expanded-element.ts} +18 -13
  53. package/src/validation/view-predicates/incoming.ts +19 -0
  54. package/src/validation/view-predicates/index.ts +4 -0
  55. package/src/validation/view-predicates/outgoing.ts +19 -0
  56. package/src/validation/view.ts +26 -0
  57. package/src/view-utils/assignNavigateTo.ts +30 -0
  58. package/src/view-utils/index.ts +3 -0
  59. package/src/view-utils/resolve-extended-views.ts +57 -0
  60. package/src/view-utils/resolve-relative-paths.ts +84 -0
  61. package/dist/Rpc.d.ts +0 -10
  62. package/dist/Rpc.js +0 -98
  63. package/dist/ast.d.ts +0 -149
  64. package/dist/ast.js +0 -271
  65. package/dist/browser/index.d.ts +0 -9
  66. package/dist/browser/index.js +0 -16
  67. package/dist/elementRef.d.ts +0 -12
  68. package/dist/elementRef.js +0 -15
  69. package/dist/generated/ast.d.ts +0 -615
  70. package/dist/generated/ast.js +0 -957
  71. package/dist/generated/grammar.d.ts +0 -7
  72. package/dist/generated/grammar.js +0 -3
  73. package/dist/generated/module.d.ts +0 -14
  74. package/dist/generated/module.js +0 -22
  75. package/dist/index.d.ts +0 -5
  76. package/dist/index.js +0 -2
  77. package/dist/logger.d.ts +0 -12
  78. package/dist/logger.js +0 -51
  79. package/dist/lsp/CodeLensProvider.d.ts +0 -10
  80. package/dist/lsp/CodeLensProvider.js +0 -40
  81. package/dist/lsp/DocumentHighlightProvider.d.ts +0 -10
  82. package/dist/lsp/DocumentHighlightProvider.js +0 -10
  83. package/dist/lsp/DocumentLinkProvider.d.ts +0 -11
  84. package/dist/lsp/DocumentLinkProvider.js +0 -41
  85. package/dist/lsp/DocumentLinkProvider.test.d.ts +0 -2
  86. package/dist/lsp/DocumentLinkProvider.test.js +0 -54
  87. package/dist/lsp/DocumentSymbolProvider.d.ts +0 -22
  88. package/dist/lsp/DocumentSymbolProvider.js +0 -189
  89. package/dist/lsp/HoverProvider.d.ts +0 -10
  90. package/dist/lsp/HoverProvider.js +0 -36
  91. package/dist/lsp/SemanticTokenProvider.d.ts +0 -8
  92. package/dist/lsp/index.d.ts +0 -7
  93. package/dist/lsp/index.js +0 -6
  94. package/dist/model/fqn-computation.d.ts +0 -4
  95. package/dist/model/fqn-computation.js +0 -43
  96. package/dist/model/fqn-index.d.ts +0 -26
  97. package/dist/model/fqn-index.js +0 -114
  98. package/dist/model/index.d.ts +0 -6
  99. package/dist/model/index.js +0 -5
  100. package/dist/model/model-builder.d.ts +0 -20
  101. package/dist/model/model-builder.js +0 -365
  102. package/dist/model/model-locator.d.ts +0 -22
  103. package/dist/model/model-locator.js +0 -115
  104. package/dist/model/model-parser.d.ts +0 -29
  105. package/dist/model/model-parser.js +0 -520
  106. package/dist/model-change/ModelChanges.d.ts +0 -16
  107. package/dist/model-change/ModelChanges.js +0 -106
  108. package/dist/model-change/changeElementStyle.d.ts +0 -18
  109. package/dist/model-change/changeElementStyle.js +0 -141
  110. package/dist/model-change/changeViewLayout.d.ts +0 -13
  111. package/dist/model-change/changeViewLayout.js +0 -29
  112. package/dist/module.d.ts +0 -59
  113. package/dist/module.js +0 -121
  114. package/dist/node/index.d.ts +0 -6
  115. package/dist/node/index.js +0 -13
  116. package/dist/protocol.d.ts +0 -58
  117. package/dist/protocol.js +0 -14
  118. package/dist/references/index.d.ts +0 -3
  119. package/dist/references/index.js +0 -2
  120. package/dist/references/scope-computation.d.ts +0 -11
  121. package/dist/references/scope-computation.js +0 -111
  122. package/dist/references/scope-provider.d.ts +0 -18
  123. package/dist/references/scope-provider.js +0 -136
  124. package/dist/shared/NodeKindProvider.d.ts +0 -16
  125. package/dist/shared/NodeKindProvider.js +0 -60
  126. package/dist/shared/WorkspaceManager.d.ts +0 -17
  127. package/dist/shared/WorkspaceManager.js +0 -29
  128. package/dist/shared/WorkspaceSymbolProvider.d.ts +0 -4
  129. package/dist/shared/WorkspaceSymbolProvider.js +0 -3
  130. package/dist/shared/index.d.ts +0 -4
  131. package/dist/shared/index.js +0 -3
  132. package/dist/test/index.d.ts +0 -2
  133. package/dist/test/index.js +0 -1
  134. package/dist/test/testServices.d.ts +0 -23
  135. package/dist/test/testServices.js +0 -102
  136. package/dist/utils/index.d.ts +0 -2
  137. package/dist/utils/index.js +0 -1
  138. package/dist/utils/printDocs.d.ts +0 -3
  139. package/dist/utils/printDocs.js +0 -1
  140. package/dist/utils/stringHash.d.ts +0 -2
  141. package/dist/utils/stringHash.js +0 -5
  142. package/dist/validation/dynamic-view-rule.d.ts +0 -5
  143. package/dist/validation/dynamic-view-step.d.ts +0 -5
  144. package/dist/validation/dynamic-view-step.js +0 -33
  145. package/dist/validation/element.d.ts +0 -6
  146. package/dist/validation/element.js +0 -38
  147. package/dist/validation/index.d.ts +0 -3
  148. package/dist/validation/property-checks.d.ts +0 -5
  149. package/dist/validation/property-checks.js +0 -11
  150. package/dist/validation/relation.d.ts +0 -5
  151. package/dist/validation/relation.js +0 -50
  152. package/dist/validation/specification.d.ts +0 -10
  153. package/dist/validation/specification.js +0 -97
  154. package/dist/validation/view-predicates/custom-element-expr.d.ts +0 -5
  155. package/dist/validation/view-predicates/custom-element-expr.js +0 -16
  156. package/dist/validation/view-predicates/expanded-element.d.ts +0 -5
  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 -16
  165. package/dist/view-utils/assignNavigateTo.d.ts +0 -3
  166. package/dist/view-utils/assignNavigateTo.js +0 -24
  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 -75
  173. /package/{dist → src}/reset.d.ts +0 -0
@@ -0,0 +1,6 @@
1
+ export * from './CodeLensProvider'
2
+ export * from './DocumentHighlightProvider'
3
+ export * from './DocumentLinkProvider'
4
+ export * from './DocumentSymbolProvider'
5
+ export * from './HoverProvider'
6
+ export * from './SemanticTokenProvider'
@@ -0,0 +1,47 @@
1
+ import { AsFqn, type c4, nonexhaustive } from '@likec4/core'
2
+ import { MultiMap } from 'langium'
3
+ import { isEmpty, isNullish as isNil } from 'remeda'
4
+ import { ast, ElementOps, type LikeC4LangiumDocument } from '../ast'
5
+ import { getFqnElementRef } from '../elementRef'
6
+ import type { LikeC4Services } from '../module'
7
+
8
+ type TraversePair = [el: ast.Element | ast.ExtendElement | ast.Relation, parent: c4.Fqn | null]
9
+
10
+ export function computeDocumentFqn(document: LikeC4LangiumDocument, services: LikeC4Services) {
11
+ const c4fqns = (document.c4fqns = new MultiMap())
12
+ const elements = document.parseResult.value.models.flatMap(m => m.elements)
13
+ if (elements.length === 0) {
14
+ return
15
+ }
16
+ const locator = services.workspace.AstNodeLocator
17
+ const traverseStack: TraversePair[] = elements.map(el => [el, null])
18
+ let pair
19
+ while ((pair = traverseStack.shift())) {
20
+ const [el, parent] = pair
21
+ if (ast.isRelation(el)) {
22
+ continue
23
+ }
24
+ if (ast.isExtendElement(el)) {
25
+ if (!isNil(el.body) && !isEmpty(el.body.elements)) {
26
+ const fqn = getFqnElementRef(el.element)
27
+ el.body.elements.forEach(child => traverseStack.push([child, fqn]))
28
+ }
29
+ continue
30
+ }
31
+ if (ast.isElement(el)) {
32
+ const fqn = AsFqn(el.name, parent)
33
+ const path = locator.getAstNodePath(el)
34
+ c4fqns.add(fqn, {
35
+ el: new WeakRef(el),
36
+ path,
37
+ name: el.name
38
+ })
39
+ ElementOps.writeId(el, fqn)
40
+ if (!isNil(el.body) && !isEmpty(el.body.elements)) {
41
+ el.body.elements.forEach(child => traverseStack.push([child, fqn]))
42
+ }
43
+ continue
44
+ }
45
+ nonexhaustive(el)
46
+ }
47
+ }
@@ -0,0 +1,161 @@
1
+ import type { Fqn } from '@likec4/core'
2
+ import { nameFromFqn, parentFqn } from '@likec4/core'
3
+ import type { LangiumDocuments, Stream } from 'langium'
4
+ import { DocumentState, DONE_RESULT, MultiMap, stream, StreamImpl } from 'langium'
5
+ import type { ast, FqnIndexedDocument } from '../ast'
6
+ import { ElementOps, isFqnIndexedDocument, isLikeC4LangiumDocument } from '../ast'
7
+ import { logError, logger } from '../logger'
8
+ import type { LikeC4Services } from '../module'
9
+ import { printDocs } from '../utils/printDocs'
10
+ import { computeDocumentFqn } from './fqn-computation'
11
+
12
+ export interface FqnIndexEntry {
13
+ fqn: Fqn
14
+ name: string
15
+ el: ast.Element
16
+ doc: FqnIndexedDocument
17
+ path: string
18
+ }
19
+
20
+ const True = () => true
21
+
22
+ export class FqnIndex {
23
+ protected langiumDocuments: LangiumDocuments
24
+
25
+ constructor(private services: LikeC4Services) {
26
+ this.langiumDocuments = services.shared.workspace.LangiumDocuments
27
+
28
+ services.shared.workspace.DocumentBuilder.onBuildPhase(
29
+ DocumentState.IndexedContent,
30
+ async (docs, _cancelToken) => {
31
+ logger.debug(`[FqnIndex] onIndexedContent ${docs.length}:\n` + printDocs(docs))
32
+ for (const doc of docs) {
33
+ if (isLikeC4LangiumDocument(doc)) {
34
+ delete doc.c4fqns
35
+ delete doc.c4Elements
36
+ delete doc.c4Specification
37
+ delete doc.c4Relations
38
+ delete doc.c4Views
39
+ try {
40
+ computeDocumentFqn(doc, services)
41
+ } catch (e) {
42
+ logError(e)
43
+ }
44
+ }
45
+ }
46
+ return Promise.resolve()
47
+ }
48
+ )
49
+ logger.debug(`[FqnIndex] Created`)
50
+ }
51
+
52
+ get documents() {
53
+ return this.langiumDocuments.all.filter(isFqnIndexedDocument)
54
+ }
55
+
56
+ private entries(filterByFqn: (fqn: Fqn) => boolean = True): Stream<FqnIndexEntry> {
57
+ return this.documents.flatMap(doc =>
58
+ doc.c4fqns.entries().flatMap(([fqn, entry]): FqnIndexEntry | FqnIndexEntry[] => {
59
+ if (filterByFqn(fqn)) {
60
+ const el = entry.el.deref()
61
+ if (el) {
62
+ return { ...entry, fqn, el, doc }
63
+ }
64
+ }
65
+ return []
66
+ })
67
+ )
68
+ }
69
+
70
+ public getFqn(el: ast.Element): Fqn | null {
71
+ return ElementOps.readId(el) ?? null
72
+ // let fqn = ElementOps.readId(el) ?? null
73
+ // if (fqn) {
74
+ // const doc = getDocument(el)
75
+ // if (isFqnIndexedDocument(doc) && doc.c4fqns.has(fqn)) {
76
+ // return fqn
77
+ // }
78
+ // const path = this.services.workspace.AstNodeLocator.getAstNodePath(el)
79
+ // logError(`Clean cached FQN ${fqn} at ${path}`)
80
+ // ElementOps.writeId(el, null)
81
+ // fqn = null
82
+ // }
83
+ // return fqn
84
+ }
85
+
86
+ public byFqn(fqn: Fqn): Stream<FqnIndexEntry> {
87
+ return this.documents.flatMap(doc => {
88
+ return doc.c4fqns.get(fqn).flatMap(entry => {
89
+ const el = entry.el.deref()
90
+ if (el) {
91
+ return { fqn, el, doc, path: entry.path, name: entry.name }
92
+ }
93
+ return []
94
+ })
95
+ })
96
+ }
97
+
98
+ public directChildrenOf(parent: Fqn): Stream<FqnIndexEntry> {
99
+ return stream([parent]).flatMap(_parent => {
100
+ const children = this.entries(fqn => parentFqn(fqn) === _parent)
101
+ .map((entry): [string, FqnIndexEntry] => [entry.name, entry])
102
+ .toArray()
103
+ if (children.length === 0) {
104
+ return []
105
+ }
106
+ return new MultiMap(children)
107
+ .entriesGroupedByKey()
108
+ .flatMap(([_name, descrs]) => (descrs.length === 1 ? descrs : []))
109
+ .iterator()
110
+ })
111
+ }
112
+
113
+ /**
114
+ * Returns descedant elements with unique names in the scope
115
+ */
116
+ public uniqueDescedants(parent: Fqn): Stream<FqnIndexEntry> {
117
+ return new StreamImpl(
118
+ () => {
119
+ const prefix = `${parent}.`
120
+
121
+ const childrenNames = new Set<string>()
122
+ const descedants = [] as FqnIndexEntry[]
123
+
124
+ const nested = new MultiMap<string, FqnIndexEntry>()
125
+
126
+ this.entries(f => f.startsWith(prefix)).forEach(e => {
127
+ const name = nameFromFqn(e.fqn)
128
+ const entry = { ...e, name }
129
+ // To keep direct children always
130
+ if (parentFqn(e.fqn) === parent) {
131
+ childrenNames.add(name)
132
+ nested.add(name, entry)
133
+ } else {
134
+ descedants.push(entry)
135
+ }
136
+ })
137
+
138
+ if (nested.size + descedants.length === 0) {
139
+ return null
140
+ }
141
+
142
+ for (const descedant of descedants) {
143
+ if (!childrenNames.has(descedant.name)) {
144
+ nested.add(descedant.name, descedant)
145
+ }
146
+ }
147
+
148
+ return nested
149
+ .entriesGroupedByKey()
150
+ .flatMap(([_name, descrs]) => (descrs.length === 1 ? descrs : []))
151
+ .iterator()
152
+ },
153
+ iterator => {
154
+ if (iterator) {
155
+ return iterator.next()
156
+ }
157
+ return DONE_RESULT as IteratorResult<FqnIndexEntry>
158
+ }
159
+ )
160
+ }
161
+ }
@@ -0,0 +1,5 @@
1
+ export * from './fqn-computation'
2
+ export * from './fqn-index'
3
+ export * from './model-builder'
4
+ export * from './model-locator'
5
+ export * from './model-parser'
@@ -0,0 +1,447 @@
1
+ import {
2
+ type c4,
3
+ compareByFqnHierarchically,
4
+ isElementView,
5
+ isStrictElementView,
6
+ parentFqn,
7
+ type StrictElementView,
8
+ type ViewID
9
+ } from '@likec4/core'
10
+ import { computeDynamicView, computeView, LikeC4ModelGraph } from '@likec4/graph'
11
+ import { deepEqual as eq } from 'fast-equals'
12
+ import type { URI, WorkspaceCache } from 'langium'
13
+ import { DocumentState, interruptAndCheck, type LangiumDocument, type LangiumDocuments } from 'langium'
14
+ import {
15
+ filter,
16
+ find,
17
+ flatMap,
18
+ forEach,
19
+ isNullish,
20
+ isTruthy,
21
+ map,
22
+ mapToObj,
23
+ pipe,
24
+ prop,
25
+ reduce,
26
+ sort,
27
+ values
28
+ } from 'remeda'
29
+ import { type CancellationToken, Disposable } from 'vscode-languageserver'
30
+ import type {
31
+ ParsedAstElement,
32
+ ParsedAstRelation,
33
+ ParsedAstSpecification,
34
+ ParsedAstView,
35
+ ParsedLikeC4LangiumDocument
36
+ } from '../ast'
37
+ import { isParsedLikeC4LangiumDocument } from '../ast'
38
+ import { logError, logger, logWarnError } from '../logger'
39
+ import type { LikeC4Services } from '../module'
40
+ import { printDocs } from '../utils/printDocs'
41
+ import { assignNavigateTo, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
42
+
43
+ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]) {
44
+ const c4Specification: ParsedAstSpecification = {
45
+ kinds: {},
46
+ relationships: {}
47
+ }
48
+ forEach(map(docs, prop('c4Specification')), spec => {
49
+ Object.assign(c4Specification.kinds, spec.kinds)
50
+ Object.assign(c4Specification.relationships, spec.relationships)
51
+ })
52
+ const resolveLinks = (doc: LangiumDocument, links: c4.NonEmptyArray<string>) => {
53
+ return links.map(l => services.lsp.DocumentLinkProvider.resolveLink(doc, l)) as c4.NonEmptyArray<string>
54
+ }
55
+
56
+ const toModelElement = (doc: LangiumDocument) => {
57
+ return ({
58
+ tags,
59
+ links,
60
+ style: {
61
+ color,
62
+ shape,
63
+ icon,
64
+ opacity,
65
+ border
66
+ },
67
+ id,
68
+ kind,
69
+ title,
70
+ description,
71
+ technology
72
+ }: ParsedAstElement): c4.Element | null => {
73
+ try {
74
+ const __kind = c4Specification.kinds[kind]
75
+ if (!__kind) {
76
+ logger.warn(`No kind '${kind}' found for ${id}`)
77
+ return null
78
+ }
79
+ color ??= __kind.color
80
+ shape ??= __kind.shape
81
+ icon ??= __kind.icon
82
+ opacity ??= __kind.opacity
83
+ border ??= __kind.border
84
+ return {
85
+ ...(color && { color }),
86
+ ...(shape && { shape }),
87
+ ...(icon && { icon }),
88
+ style: {
89
+ ...(border && { border }),
90
+ ...(opacity && { opacity })
91
+ },
92
+ links: links ? resolveLinks(doc, links) : null,
93
+ tags: tags ?? null,
94
+ technology: technology ?? null,
95
+ description: description ?? null,
96
+ title,
97
+ kind,
98
+ id
99
+ }
100
+ } catch (e) {
101
+ logWarnError(e)
102
+ }
103
+ return null
104
+ }
105
+ }
106
+
107
+ const elements = pipe(
108
+ docs,
109
+ flatMap(d => d.c4Elements.map(toModelElement(d))),
110
+ filter(isTruthy),
111
+ sort(compareByFqnHierarchically),
112
+ reduce(
113
+ (acc, el) => {
114
+ const parent = parentFqn(el.id)
115
+ if (parent && isNullish(acc[parent])) {
116
+ logWarnError(`No parent found for ${el.id}`)
117
+ return acc
118
+ }
119
+ if (el.id in acc) {
120
+ // should not happen, as validated
121
+ logWarnError(`Duplicate element id: ${el.id}`)
122
+ return acc
123
+ }
124
+ acc[el.id] = el
125
+ return acc
126
+ },
127
+ {} as c4.LikeC4Model['elements']
128
+ )
129
+ )
130
+
131
+ const toModelRelation = (doc: LangiumDocument) => {
132
+ return ({
133
+ astPath,
134
+ source,
135
+ target,
136
+ kind,
137
+ links,
138
+ id,
139
+ ...model
140
+ }: ParsedAstRelation): c4.Relation | null => {
141
+ if (isNullish(elements[source]) || isNullish(elements[target])) {
142
+ return null
143
+ }
144
+
145
+ if (!!kind && kind in c4Specification.relationships) {
146
+ return {
147
+ ...(links && { links: resolveLinks(doc, links) }),
148
+ ...c4Specification.relationships[kind],
149
+ ...model,
150
+ source,
151
+ target,
152
+ kind,
153
+ id
154
+ }
155
+ }
156
+ return {
157
+ ...(links && { links: resolveLinks(doc, links) }),
158
+ ...model,
159
+ source,
160
+ target,
161
+ id
162
+ }
163
+ }
164
+ }
165
+
166
+ const relations = pipe(
167
+ flatMap(docs, d => map(d.c4Relations, toModelRelation(d))),
168
+ filter(isTruthy),
169
+ mapToObj(r => [r.id, r])
170
+ )
171
+
172
+ // const toElementView = (view: ParsedAstElementView, doc: LangiumDocument): c4.ElementView => {
173
+ // let { astPath, rules, title, description, tags, links, id, __, ...model } = view
174
+
175
+ // if ('viewOf' in view) {
176
+ // title ??= elements[view.viewOf]?.title ?? null
177
+ // }
178
+ // if (!title && view.id === 'index') {
179
+ // title = 'Landscape view'
180
+ // }
181
+ // return {
182
+ // __,
183
+ // id,
184
+ // ...model,
185
+ // title,
186
+ // description,
187
+ // tags,
188
+ // links: links ? resolveLinks(doc, links) : null,
189
+ // docUri: '',
190
+ // rules
191
+ // }
192
+ // }
193
+
194
+ // const toDynamicView = (view: ParsedAstDynamicView, doc: LangiumDocument): c4.DynamicView => {
195
+ // let { rules, steps, title, description, tags, links, id, __ } = view
196
+ // return {
197
+ // __,
198
+ // id,
199
+ // title,
200
+ // description,
201
+ // tags,
202
+ // links: links ? resolveLinks(doc, links) : null,
203
+ // docUri: '',
204
+ // rules,
205
+ // steps
206
+ // }
207
+ // }
208
+
209
+ const toC4View = (doc: LangiumDocument) => {
210
+ const docUri = doc.uri.toString()
211
+ return (parsedAstView: ParsedAstView): c4.View => {
212
+ let {
213
+ id,
214
+ title,
215
+ description,
216
+ tags,
217
+ links,
218
+
219
+ // ignore this property
220
+ astPath: _ignore,
221
+
222
+ // model should include discriminant __
223
+ ...model
224
+ } = parsedAstView
225
+
226
+ if (parsedAstView.__ === 'element' && isNullish(title) && 'viewOf' in parsedAstView) {
227
+ title ??= elements[parsedAstView.viewOf]?.title ?? null
228
+ }
229
+
230
+ if (isNullish(title) && id === 'index') {
231
+ title = 'Landscape view'
232
+ }
233
+
234
+ return {
235
+ id,
236
+ title,
237
+ description,
238
+ tags,
239
+ links: links ? resolveLinks(doc, links) : null,
240
+ docUri,
241
+ ...model
242
+ }
243
+ }
244
+ }
245
+
246
+ const views = pipe(
247
+ docs,
248
+ flatMap(d => map(d.c4Views, toC4View(d))),
249
+ resolveRelativePaths,
250
+ mapToObj(v => [v.id, v]),
251
+ resolveRulesExtendedViews
252
+ )
253
+ // add index view if not present
254
+ if (!('index' in views)) {
255
+ views['index' as ViewID] = {
256
+ __: 'element',
257
+ id: 'index' as ViewID,
258
+ title: 'Landscape',
259
+ description: null,
260
+ tags: null,
261
+ links: null,
262
+ rules: [
263
+ {
264
+ include: [
265
+ {
266
+ wildcard: true
267
+ }
268
+ ]
269
+ }
270
+ ]
271
+ }
272
+ }
273
+
274
+ return {
275
+ elements,
276
+ relations,
277
+ views
278
+ }
279
+ }
280
+
281
+ const RAW_MODEL_CACHE = 'LikeC4RawModel'
282
+ const MODEL_CACHE = 'LikeC4Model'
283
+
284
+ type ModelParsedListener = (docs: URI[]) => void
285
+
286
+ export class LikeC4ModelBuilder {
287
+ private langiumDocuments: LangiumDocuments
288
+ private listeners: ModelParsedListener[] = []
289
+
290
+ constructor(private services: LikeC4Services) {
291
+ this.langiumDocuments = services.shared.workspace.LangiumDocuments
292
+ const parser = services.likec4.ModelParser
293
+
294
+ services.shared.workspace.DocumentBuilder.onUpdate((_changed, deleted) => {
295
+ if (deleted.length > 0) {
296
+ this.notifyListeners(deleted)
297
+ }
298
+ })
299
+
300
+ services.shared.workspace.DocumentBuilder.onBuildPhase(
301
+ DocumentState.Validated,
302
+ async (docs, _cancelToken) => {
303
+ let parsed = [] as URI[]
304
+ logger.debug(`[ModelBuilder] onValidated (${docs.length} docs)\n${printDocs(docs)}`)
305
+ for (const doc of parser.parse(docs)) {
306
+ parsed.push(doc.uri)
307
+ }
308
+ if (parsed.length > 0) {
309
+ this.notifyListeners(parsed)
310
+ }
311
+ return await Promise.resolve()
312
+ }
313
+ )
314
+ logger.debug(`[ModelBuilder] Created`)
315
+ }
316
+
317
+ public async buildModel(cancelToken?: CancellationToken): Promise<c4.LikeC4Model | null> {
318
+ return await this.services.shared.workspace.WorkspaceLock.read(async () => {
319
+ if (cancelToken) {
320
+ await interruptAndCheck(cancelToken)
321
+ }
322
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4Model | null>
323
+ return cache.get(RAW_MODEL_CACHE, () => {
324
+ const docs = this.documents()
325
+ if (docs.length === 0) {
326
+ logger.debug('[ModelBuilder] No documents to build model from')
327
+ return null
328
+ }
329
+ logger.debug(`[ModelBuilder] buildModel from ${docs.length} docs:\n${printDocs(docs)}`)
330
+ return buildModel(this.services, docs)
331
+ })
332
+ })
333
+ }
334
+
335
+ private previousViews: Record<ViewID, c4.ComputedView> = {}
336
+
337
+ public async buildComputedModel(cancelToken?: CancellationToken): Promise<c4.LikeC4ComputedModel | null> {
338
+ const model = await this.buildModel(cancelToken)
339
+ if (!model) {
340
+ return null
341
+ }
342
+ return await this.services.shared.workspace.WorkspaceLock.read(async () => {
343
+ if (cancelToken) {
344
+ await interruptAndCheck(cancelToken)
345
+ }
346
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4ComputedModel | null>
347
+ const viewsCache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
348
+ return cache.get(MODEL_CACHE, () => {
349
+ const index = new LikeC4ModelGraph(model)
350
+
351
+ const allViews = [] as c4.ComputedView[]
352
+ for (const view of values(model.views)) {
353
+ const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
354
+ if (!result.isSuccess) {
355
+ logWarnError(result.error)
356
+ continue
357
+ }
358
+ allViews.push(result.view)
359
+ }
360
+ assignNavigateTo(allViews)
361
+ const views = mapToObj(allViews, v => {
362
+ const previous = this.previousViews[v.id]
363
+ const view = previous && eq(v, previous) ? previous : v
364
+ viewsCache.set(computedViewKey(v.id), view)
365
+ return [v.id, view] as const
366
+ })
367
+ this.previousViews = { ...views }
368
+ return {
369
+ elements: model.elements,
370
+ relations: model.relations,
371
+ views
372
+ }
373
+ })
374
+ })
375
+ }
376
+
377
+ public async computeView(viewId: ViewID, cancelToken?: CancellationToken): Promise<c4.ComputedView | null> {
378
+ const model = await this.buildModel(cancelToken)
379
+ const view = model?.views[viewId]
380
+ if (!view) {
381
+ logger.warn(`[ModelBuilder] Cannot find view ${viewId}`)
382
+ return null
383
+ }
384
+ return await this.services.shared.workspace.WorkspaceLock.read(async () => {
385
+ if (cancelToken) {
386
+ await interruptAndCheck(cancelToken)
387
+ }
388
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
389
+ return cache.get(computedViewKey(viewId), () => {
390
+ const index = new LikeC4ModelGraph(model)
391
+ const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
392
+ if (!result.isSuccess) {
393
+ logError(result.error)
394
+ return null
395
+ }
396
+
397
+ const allElementViews = values(model.views).filter(
398
+ (v): v is StrictElementView => isStrictElementView(v) && v.id !== viewId
399
+ )
400
+
401
+ let computedView = result.view
402
+ computedView.nodes.forEach(node => {
403
+ if (!node.navigateTo) {
404
+ // find first element view that is not the current one
405
+ const navigateTo = find(allElementViews, v => v.viewOf === node.id)
406
+ if (navigateTo) {
407
+ node.navigateTo = navigateTo.id
408
+ }
409
+ }
410
+ })
411
+
412
+ const previous = this.previousViews[viewId]
413
+ computedView = previous && eq(computedView, previous) ? previous : computedView
414
+ this.previousViews[viewId] = computedView
415
+
416
+ return computedView
417
+ })
418
+ })
419
+ }
420
+
421
+ public onModelParsed(callback: ModelParsedListener): Disposable {
422
+ this.listeners.push(callback)
423
+ return Disposable.create(() => {
424
+ const index = this.listeners.indexOf(callback)
425
+ if (index >= 0) {
426
+ this.listeners.splice(index, 1)
427
+ }
428
+ })
429
+ }
430
+
431
+ private documents() {
432
+ return this.langiumDocuments.all.filter(isParsedLikeC4LangiumDocument).toArray()
433
+ }
434
+
435
+ private notifyListeners(docs: URI[]) {
436
+ for (const listener of this.listeners) {
437
+ try {
438
+ listener(docs)
439
+ } catch (e) {
440
+ logError(e)
441
+ }
442
+ }
443
+ }
444
+ }
445
+ function computedViewKey(viewId: string): string {
446
+ return `computed-view-${viewId}`
447
+ }