@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.
- package/contrib/likec4.monarch.ts +4 -4
- package/contrib/likec4.tmLanguage.json +1 -1
- 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} +58 -43
- 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/src/validation/dynamic-view-rule.ts +35 -0
- 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} +25 -17
- 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/src/validation/view-predicates/expanded-element.ts +34 -0
- 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 -133
- package/dist/ast.js +0 -267
- 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 -559
- package/dist/generated/ast.js +0 -868
- 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 -37
- 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 -184
- 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 -352
- package/dist/model/model-locator.d.ts +0 -22
- package/dist/model/model-locator.js +0 -119
- package/dist/model/model-parser.d.ts +0 -27
- package/dist/model/model-parser.js +0 -410
- package/dist/model-change/ModelChanges.d.ts +0 -15
- package/dist/model-change/ModelChanges.js +0 -100
- package/dist/model-change/changeElementStyle.d.ts +0 -15
- package/dist/model-change/changeElementStyle.js +0 -141
- package/dist/model-change/changeViewLayout.d.ts +0 -13
- package/dist/model-change/changeViewLayout.js +0 -30
- 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 -108
- 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 -58
- 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/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/expanded-element.js +0 -28
- 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 -18
- package/dist/view-utils/assignNavigateTo.d.ts +0 -3
- package/dist/view-utils/assignNavigateTo.js +0 -23
- 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 -76
- /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
|
+
}
|