@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.
- package/package.json +8 -10
- package/src/Rpc.ts +108 -0
- package/src/ast.ts +443 -0
- package/src/browser/index.ts +30 -0
- package/src/elementRef.ts +26 -0
- package/src/generated/ast.ts +1632 -0
- package/src/generated/grammar.ts +10 -0
- package/src/generated/module.ts +32 -0
- package/src/index.ts +4 -0
- package/src/like-c4.langium +395 -0
- package/src/logger.ts +54 -0
- package/src/lsp/CodeLensProvider.ts +51 -0
- package/src/lsp/DocumentHighlightProvider.ts +12 -0
- package/src/lsp/DocumentLinkProvider.test.ts +66 -0
- package/src/lsp/DocumentLinkProvider.ts +53 -0
- package/src/lsp/DocumentSymbolProvider.ts +201 -0
- package/src/lsp/HoverProvider.ts +58 -0
- package/{dist/lsp/SemanticTokenProvider.js → src/lsp/SemanticTokenProvider.ts} +57 -42
- package/src/lsp/index.ts +6 -0
- package/src/model/fqn-computation.ts +47 -0
- package/src/model/fqn-index.ts +161 -0
- package/src/model/index.ts +5 -0
- package/src/model/model-builder.ts +447 -0
- package/src/model/model-locator.ts +130 -0
- package/src/model/model-parser.ts +580 -0
- package/src/model-change/ModelChanges.ts +120 -0
- package/src/model-change/changeElementStyle.ts +176 -0
- package/src/model-change/changeViewLayout.ts +41 -0
- package/src/module.ts +197 -0
- package/src/node/index.ts +20 -0
- package/src/protocol.ts +87 -0
- package/src/references/index.ts +2 -0
- package/src/references/scope-computation.ts +142 -0
- package/src/references/scope-provider.ts +166 -0
- package/src/shared/NodeKindProvider.ts +67 -0
- package/src/shared/WorkspaceManager.ts +39 -0
- package/src/shared/WorkspaceSymbolProvider.ts +3 -0
- package/src/shared/index.ts +3 -0
- package/src/test/index.ts +1 -0
- package/src/test/testServices.ts +119 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/printDocs.ts +3 -0
- package/src/utils/stringHash.ts +6 -0
- package/{dist/validation/dynamic-view-rule.js → src/validation/dynamic-view-rule.ts} +14 -11
- package/src/validation/dynamic-view-step.ts +39 -0
- package/src/validation/element.ts +52 -0
- package/{dist/validation/index.js → src/validation/index.ts} +22 -18
- package/src/validation/property-checks.ts +17 -0
- package/src/validation/relation.ts +57 -0
- package/src/validation/specification.ts +118 -0
- package/src/validation/view-predicates/custom-element-expr.ts +21 -0
- package/{dist/validation/view-predicates/expanded-element.js → src/validation/view-predicates/expanded-element.ts} +18 -13
- package/src/validation/view-predicates/incoming.ts +19 -0
- package/src/validation/view-predicates/index.ts +4 -0
- package/src/validation/view-predicates/outgoing.ts +19 -0
- package/src/validation/view.ts +26 -0
- package/src/view-utils/assignNavigateTo.ts +30 -0
- package/src/view-utils/index.ts +3 -0
- package/src/view-utils/resolve-extended-views.ts +57 -0
- package/src/view-utils/resolve-relative-paths.ts +84 -0
- package/dist/Rpc.d.ts +0 -10
- package/dist/Rpc.js +0 -98
- package/dist/ast.d.ts +0 -149
- package/dist/ast.js +0 -271
- package/dist/browser/index.d.ts +0 -9
- package/dist/browser/index.js +0 -16
- package/dist/elementRef.d.ts +0 -12
- package/dist/elementRef.js +0 -15
- package/dist/generated/ast.d.ts +0 -615
- package/dist/generated/ast.js +0 -957
- package/dist/generated/grammar.d.ts +0 -7
- package/dist/generated/grammar.js +0 -3
- package/dist/generated/module.d.ts +0 -14
- package/dist/generated/module.js +0 -22
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -2
- package/dist/logger.d.ts +0 -12
- package/dist/logger.js +0 -51
- package/dist/lsp/CodeLensProvider.d.ts +0 -10
- package/dist/lsp/CodeLensProvider.js +0 -40
- package/dist/lsp/DocumentHighlightProvider.d.ts +0 -10
- package/dist/lsp/DocumentHighlightProvider.js +0 -10
- package/dist/lsp/DocumentLinkProvider.d.ts +0 -11
- package/dist/lsp/DocumentLinkProvider.js +0 -41
- package/dist/lsp/DocumentLinkProvider.test.d.ts +0 -2
- package/dist/lsp/DocumentLinkProvider.test.js +0 -54
- package/dist/lsp/DocumentSymbolProvider.d.ts +0 -22
- package/dist/lsp/DocumentSymbolProvider.js +0 -189
- package/dist/lsp/HoverProvider.d.ts +0 -10
- package/dist/lsp/HoverProvider.js +0 -36
- package/dist/lsp/SemanticTokenProvider.d.ts +0 -8
- package/dist/lsp/index.d.ts +0 -7
- package/dist/lsp/index.js +0 -6
- package/dist/model/fqn-computation.d.ts +0 -4
- package/dist/model/fqn-computation.js +0 -43
- package/dist/model/fqn-index.d.ts +0 -26
- package/dist/model/fqn-index.js +0 -114
- package/dist/model/index.d.ts +0 -6
- package/dist/model/index.js +0 -5
- package/dist/model/model-builder.d.ts +0 -20
- package/dist/model/model-builder.js +0 -365
- package/dist/model/model-locator.d.ts +0 -22
- package/dist/model/model-locator.js +0 -115
- package/dist/model/model-parser.d.ts +0 -29
- package/dist/model/model-parser.js +0 -520
- package/dist/model-change/ModelChanges.d.ts +0 -16
- package/dist/model-change/ModelChanges.js +0 -106
- package/dist/model-change/changeElementStyle.d.ts +0 -18
- package/dist/model-change/changeElementStyle.js +0 -141
- package/dist/model-change/changeViewLayout.d.ts +0 -13
- package/dist/model-change/changeViewLayout.js +0 -29
- package/dist/module.d.ts +0 -59
- package/dist/module.js +0 -121
- package/dist/node/index.d.ts +0 -6
- package/dist/node/index.js +0 -13
- package/dist/protocol.d.ts +0 -58
- package/dist/protocol.js +0 -14
- package/dist/references/index.d.ts +0 -3
- package/dist/references/index.js +0 -2
- package/dist/references/scope-computation.d.ts +0 -11
- package/dist/references/scope-computation.js +0 -111
- package/dist/references/scope-provider.d.ts +0 -18
- package/dist/references/scope-provider.js +0 -136
- package/dist/shared/NodeKindProvider.d.ts +0 -16
- package/dist/shared/NodeKindProvider.js +0 -60
- package/dist/shared/WorkspaceManager.d.ts +0 -17
- package/dist/shared/WorkspaceManager.js +0 -29
- package/dist/shared/WorkspaceSymbolProvider.d.ts +0 -4
- package/dist/shared/WorkspaceSymbolProvider.js +0 -3
- package/dist/shared/index.d.ts +0 -4
- package/dist/shared/index.js +0 -3
- package/dist/test/index.d.ts +0 -2
- package/dist/test/index.js +0 -1
- package/dist/test/testServices.d.ts +0 -23
- package/dist/test/testServices.js +0 -102
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.js +0 -1
- package/dist/utils/printDocs.d.ts +0 -3
- package/dist/utils/printDocs.js +0 -1
- package/dist/utils/stringHash.d.ts +0 -2
- package/dist/utils/stringHash.js +0 -5
- package/dist/validation/dynamic-view-rule.d.ts +0 -5
- package/dist/validation/dynamic-view-step.d.ts +0 -5
- package/dist/validation/dynamic-view-step.js +0 -33
- package/dist/validation/element.d.ts +0 -6
- package/dist/validation/element.js +0 -38
- package/dist/validation/index.d.ts +0 -3
- package/dist/validation/property-checks.d.ts +0 -5
- package/dist/validation/property-checks.js +0 -11
- package/dist/validation/relation.d.ts +0 -5
- package/dist/validation/relation.js +0 -50
- package/dist/validation/specification.d.ts +0 -10
- package/dist/validation/specification.js +0 -97
- package/dist/validation/view-predicates/custom-element-expr.d.ts +0 -5
- package/dist/validation/view-predicates/custom-element-expr.js +0 -16
- package/dist/validation/view-predicates/expanded-element.d.ts +0 -5
- package/dist/validation/view-predicates/incoming.d.ts +0 -5
- package/dist/validation/view-predicates/incoming.js +0 -14
- package/dist/validation/view-predicates/index.d.ts +0 -5
- package/dist/validation/view-predicates/index.js +0 -4
- package/dist/validation/view-predicates/outgoing.d.ts +0 -5
- package/dist/validation/view-predicates/outgoing.js +0 -14
- package/dist/validation/view.d.ts +0 -5
- package/dist/validation/view.js +0 -16
- package/dist/view-utils/assignNavigateTo.d.ts +0 -3
- package/dist/view-utils/assignNavigateTo.js +0 -24
- package/dist/view-utils/index.d.ts +0 -4
- package/dist/view-utils/index.js +0 -3
- package/dist/view-utils/resolve-extended-views.d.ts +0 -7
- package/dist/view-utils/resolve-extended-views.js +0 -41
- package/dist/view-utils/resolve-relative-paths.d.ts +0 -3
- package/dist/view-utils/resolve-relative-paths.js +0 -75
- /package/{dist → src}/reset.d.ts +0 -0
package/src/lsp/index.ts
ADDED
|
@@ -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,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
|
+
}
|