@likec4/language-server 1.2.2 → 1.4.0
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 +19 -8
- package/src/ast.ts +2 -0
- package/src/generated/ast.ts +157 -123
- package/src/generated/grammar.ts +2 -2
- package/src/generated/module.ts +1 -1
- package/src/like-c4.langium +53 -34
- package/src/logger.ts +21 -7
- package/src/lsp/CompletionProvider.ts +7 -0
- package/src/lsp/SemanticTokenProvider.ts +78 -17
- package/src/lsp/index.ts +1 -0
- package/src/model/model-builder.ts +3 -39
- package/src/model/model-parser.ts +19 -4
- package/src/model-change/ModelChanges.ts +58 -53
- package/src/model-change/changeElementStyle.ts +5 -6
- package/src/model-change/saveManualLayout.ts +43 -0
- package/src/model-graph/LikeC4ModelGraph.ts +304 -0
- package/src/model-graph/compute-view/__test__/fixture.ts +438 -0
- package/src/model-graph/compute-view/compute.ts +430 -0
- package/src/model-graph/compute-view/index.ts +33 -0
- package/src/model-graph/compute-view/predicates.ts +404 -0
- package/src/model-graph/dynamic-view/__test__/fixture.ts +56 -0
- package/src/model-graph/dynamic-view/compute.ts +198 -0
- package/src/model-graph/dynamic-view/index.ts +29 -0
- package/src/model-graph/index.ts +3 -0
- package/src/model-graph/utils/applyElementCustomProperties.ts +49 -0
- package/src/model-graph/utils/applyViewRuleStyles.ts +68 -0
- package/src/model-graph/utils/buildComputeNodes.ts +61 -0
- package/src/model-graph/utils/sortNodes.ts +105 -0
- package/src/module.ts +3 -0
- package/src/protocol.ts +3 -18
- package/src/references/scope-computation.ts +29 -11
- package/src/references/scope-provider.ts +22 -16
- package/src/validation/view.ts +9 -4
- package/src/view-utils/manual-layout.ts +93 -0
- package/contrib/likec4.monarch.ts +0 -41
- package/src/lsp/DocumentLinkProvider.test.ts +0 -66
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ComputedNode, ViewRule } from '@likec4/core'
|
|
2
|
+
import { Expr, nonNullable } from '@likec4/core'
|
|
3
|
+
import { isEmpty, isNullish as isNil, omitBy } from 'remeda'
|
|
4
|
+
|
|
5
|
+
export function applyElementCustomProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
|
|
6
|
+
const rules = _rules.flatMap(r => ('include' in r ? r.include.filter(Expr.isCustomElement) : []))
|
|
7
|
+
if (rules.length === 0) {
|
|
8
|
+
return _nodes
|
|
9
|
+
}
|
|
10
|
+
const nodes = [..._nodes]
|
|
11
|
+
for (
|
|
12
|
+
const {
|
|
13
|
+
custom: { element, ...props }
|
|
14
|
+
} of rules
|
|
15
|
+
) {
|
|
16
|
+
const nodeIdx = nodes.findIndex(n => n.id === element)
|
|
17
|
+
if (nodeIdx === -1) {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
let node = nonNullable(nodes[nodeIdx])
|
|
21
|
+
const { border, opacity, ...rest } = omitBy(props, isNil)
|
|
22
|
+
if (!isEmpty(rest)) {
|
|
23
|
+
node = {
|
|
24
|
+
...node,
|
|
25
|
+
...rest
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let styleOverride: ComputedNode['style'] | undefined
|
|
30
|
+
if (border !== undefined) {
|
|
31
|
+
styleOverride = { border }
|
|
32
|
+
}
|
|
33
|
+
if (opacity !== undefined) {
|
|
34
|
+
styleOverride = { ...styleOverride, opacity }
|
|
35
|
+
}
|
|
36
|
+
if (styleOverride) {
|
|
37
|
+
node = {
|
|
38
|
+
...node,
|
|
39
|
+
style: {
|
|
40
|
+
...node.style,
|
|
41
|
+
...styleOverride
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
nodes[nodeIdx] = node
|
|
47
|
+
}
|
|
48
|
+
return nodes
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ComputedNode, ViewRule } from '@likec4/core'
|
|
2
|
+
import { Expr, isViewRuleStyle, nonexhaustive, parentFqn } from '@likec4/core'
|
|
3
|
+
import { anyPass, filter, type Predicate } from 'rambdax'
|
|
4
|
+
import { isDefined, isNullish } from 'remeda'
|
|
5
|
+
|
|
6
|
+
export function applyViewRuleStyles(_rules: ViewRule[], nodes: ComputedNode[]) {
|
|
7
|
+
const rules = _rules.filter(isViewRuleStyle)
|
|
8
|
+
if (rules.length === 0) {
|
|
9
|
+
return nodes
|
|
10
|
+
}
|
|
11
|
+
for (const rule of rules) {
|
|
12
|
+
const predicates = [] as Predicate<ComputedNode>[]
|
|
13
|
+
for (const target of rule.targets) {
|
|
14
|
+
if (Expr.isWildcard(target)) {
|
|
15
|
+
predicates.push(() => true)
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
if (Expr.isElementKindExpr(target)) {
|
|
19
|
+
predicates.push(
|
|
20
|
+
target.isEqual ? n => n.kind === target.elementKind : n => n.kind !== target.elementKind
|
|
21
|
+
)
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
if (Expr.isElementTagExpr(target)) {
|
|
25
|
+
predicates.push(
|
|
26
|
+
target.isEqual
|
|
27
|
+
? ({ tags }) => !!tags && tags.includes(target.elementTag)
|
|
28
|
+
: ({ tags }) => isNullish(tags) || !tags.includes(target.elementTag)
|
|
29
|
+
)
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (Expr.isExpandedElementExpr(target)) {
|
|
33
|
+
predicates.push(n => n.id === target.expanded || parentFqn(n.id) === target.expanded)
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (Expr.isElementRef(target)) {
|
|
37
|
+
const { element, isDescedants } = target
|
|
38
|
+
predicates.push(
|
|
39
|
+
isDescedants ? n => n.id.startsWith(element + '.') : n => (n.id as string) === element
|
|
40
|
+
)
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
nonexhaustive(target)
|
|
44
|
+
}
|
|
45
|
+
filter(anyPass(predicates), nodes).forEach(n => {
|
|
46
|
+
n.shape = rule.style.shape ?? n.shape
|
|
47
|
+
n.color = rule.style.color ?? n.color
|
|
48
|
+
if (isDefined.strict(rule.style.icon)) {
|
|
49
|
+
n.icon = rule.style.icon
|
|
50
|
+
}
|
|
51
|
+
let styleOverride: ComputedNode['style'] | undefined
|
|
52
|
+
if (isDefined.strict(rule.style.border)) {
|
|
53
|
+
styleOverride = { ...styleOverride, border: rule.style.border }
|
|
54
|
+
}
|
|
55
|
+
if (isDefined.strict(rule.style.opacity)) {
|
|
56
|
+
styleOverride = { ...styleOverride, opacity: rule.style.opacity }
|
|
57
|
+
}
|
|
58
|
+
if (styleOverride) {
|
|
59
|
+
n.style = {
|
|
60
|
+
...n.style,
|
|
61
|
+
...styleOverride
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return nodes
|
|
68
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ComputedNode, Element, Fqn } from '@likec4/core'
|
|
2
|
+
import { compareByFqnHierarchically, DefaultElementShape, DefaultThemeColor, parentFqn } from '@likec4/core'
|
|
3
|
+
|
|
4
|
+
function updateDepthOfAncestors(node: ComputedNode, nodes: ReadonlyMap<Fqn, ComputedNode>) {
|
|
5
|
+
let parentNd
|
|
6
|
+
while (!!node.parent && (parentNd = nodes.get(node.parent))) {
|
|
7
|
+
const depth = parentNd.depth ?? 1
|
|
8
|
+
parentNd.depth = Math.max(depth, (node.depth ?? 0) + 1)
|
|
9
|
+
if (parentNd.depth === depth) {
|
|
10
|
+
// stop if we didn't change depth
|
|
11
|
+
break
|
|
12
|
+
}
|
|
13
|
+
node = parentNd
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildComputeNodes(elements: Iterable<Element>) {
|
|
18
|
+
return (
|
|
19
|
+
Array.from(elements)
|
|
20
|
+
// Sort from Top to Bottom
|
|
21
|
+
// So we can ensure that parent nodes are created before child nodes
|
|
22
|
+
.sort(compareByFqnHierarchically)
|
|
23
|
+
.reduce((map, { id, color, shape, style, ...el }) => {
|
|
24
|
+
let parent = parentFqn(id)
|
|
25
|
+
let level = 0
|
|
26
|
+
// Find the first ancestor that is already in the map
|
|
27
|
+
while (parent) {
|
|
28
|
+
const parentNd = map.get(parent)
|
|
29
|
+
if (parentNd) {
|
|
30
|
+
// if parent has no children and we are about to add first one
|
|
31
|
+
// we need to set its depth to 1
|
|
32
|
+
if (parentNd.children.length == 0) {
|
|
33
|
+
parentNd.depth = 1
|
|
34
|
+
// go up the tree and update depth of all parents
|
|
35
|
+
updateDepthOfAncestors(parentNd, map)
|
|
36
|
+
}
|
|
37
|
+
parentNd.children.push(id)
|
|
38
|
+
level = parentNd.level + 1
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
parent = parentFqn(parent)
|
|
42
|
+
}
|
|
43
|
+
const node: ComputedNode = {
|
|
44
|
+
...el,
|
|
45
|
+
id,
|
|
46
|
+
parent,
|
|
47
|
+
level,
|
|
48
|
+
color: color ?? DefaultThemeColor,
|
|
49
|
+
shape: shape ?? DefaultElementShape,
|
|
50
|
+
children: [],
|
|
51
|
+
inEdges: [],
|
|
52
|
+
outEdges: [],
|
|
53
|
+
style: {
|
|
54
|
+
...style
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
map.set(id, node)
|
|
58
|
+
return map
|
|
59
|
+
}, new Map<Fqn, ComputedNode>()) as ReadonlyMap<Fqn, ComputedNode>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import pkg from '@dagrejs/graphlib'
|
|
2
|
+
import {
|
|
3
|
+
compareByFqnHierarchically,
|
|
4
|
+
compareRelations,
|
|
5
|
+
type ComputedEdge,
|
|
6
|
+
type ComputedNode,
|
|
7
|
+
type EdgeId,
|
|
8
|
+
type Fqn,
|
|
9
|
+
invariant,
|
|
10
|
+
nonNullable
|
|
11
|
+
} from '@likec4/core'
|
|
12
|
+
import { difference, filter, map, pipe, sort, take } from 'remeda'
|
|
13
|
+
|
|
14
|
+
// '@dagrejs/graphlib' is a CommonJS module
|
|
15
|
+
// Here is a workaround to import it
|
|
16
|
+
const { Graph, alg } = pkg
|
|
17
|
+
|
|
18
|
+
// side effect
|
|
19
|
+
function sortChildren(nodes: readonly ComputedNode[]) {
|
|
20
|
+
nodes.forEach(parent => {
|
|
21
|
+
if (parent.children.length > 0) {
|
|
22
|
+
parent.children = nodes.flatMap(n => (n.parent === parent.id ? n.id : []))
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sortNodes({
|
|
28
|
+
nodes,
|
|
29
|
+
edges
|
|
30
|
+
}: {
|
|
31
|
+
nodes: ComputedNode[]
|
|
32
|
+
edges: ComputedEdge[]
|
|
33
|
+
}): ComputedNode[] {
|
|
34
|
+
if (edges.length === 0) {
|
|
35
|
+
return nodes
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const g = new Graph({
|
|
39
|
+
compound: false,
|
|
40
|
+
directed: true,
|
|
41
|
+
multigraph: false
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const getNode = (id: Fqn) =>
|
|
45
|
+
nonNullable(
|
|
46
|
+
nodes.find(n => n.id === id),
|
|
47
|
+
'Edge not found'
|
|
48
|
+
)
|
|
49
|
+
const getEdge = (id: EdgeId) =>
|
|
50
|
+
nonNullable(
|
|
51
|
+
edges.find(edge => edge.id === id),
|
|
52
|
+
'Edge not found'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for (const e of [...edges].sort(compareRelations)) {
|
|
56
|
+
g.setEdge(e.source, e.target)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const n of nodes) {
|
|
60
|
+
g.setNode(n.id)
|
|
61
|
+
if (n.children.length > 0) {
|
|
62
|
+
// n.children.forEach(c => {
|
|
63
|
+
// g.setEdge(n.id, c, undefined, `${n.id}:${c}`)
|
|
64
|
+
// })
|
|
65
|
+
n.inEdges.forEach(e => {
|
|
66
|
+
const edge = getEdge(e)
|
|
67
|
+
// if this edge from leaf to the child of this node
|
|
68
|
+
if (edge.target !== n.id && getNode(edge.source).children.length === 0) {
|
|
69
|
+
// const id = `${edge.source}:${n.id}`
|
|
70
|
+
g.setEdge(edge.source, n.id)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
// n.outEdges.forEach(e => {
|
|
74
|
+
// const edge = getEdge(e)
|
|
75
|
+
// if (edge.source !== n.id) {
|
|
76
|
+
// const id = `${n.id}:${edge.target}`
|
|
77
|
+
// g.setEdge(n.id, edge.target, undefined, id)
|
|
78
|
+
// }
|
|
79
|
+
// })
|
|
80
|
+
}
|
|
81
|
+
if (n.parent) {
|
|
82
|
+
g.setEdge(n.parent, n.id)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let sources = g.sources()
|
|
87
|
+
if (sources.length === 0) {
|
|
88
|
+
sources = pipe(
|
|
89
|
+
nodes,
|
|
90
|
+
sort(compareByFqnHierarchically),
|
|
91
|
+
filter(n => n.inEdges.length === 0 || n.parent === null),
|
|
92
|
+
map(n => n.id)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
const orderedIds = alg.postorder(g, sources).reverse() as Fqn[]
|
|
96
|
+
const sorted = orderedIds.map(getNode)
|
|
97
|
+
if (sorted.length < nodes.length) {
|
|
98
|
+
const unsorted = difference(nodes, sorted)
|
|
99
|
+
sorted.push(...unsorted)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
invariant(sorted.length === nodes.length, 'Not all nodes were processed by graphlib')
|
|
103
|
+
sortChildren(sorted)
|
|
104
|
+
return sorted
|
|
105
|
+
}
|
package/src/module.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { LikeC4GeneratedModule, LikeC4GeneratedSharedModule } from './generated/
|
|
|
13
13
|
import { logger } from './logger'
|
|
14
14
|
import {
|
|
15
15
|
LikeC4CodeLensProvider,
|
|
16
|
+
LikeC4CompletionProvider,
|
|
16
17
|
LikeC4DocumentHighlightProvider,
|
|
17
18
|
LikeC4DocumentLinkProvider,
|
|
18
19
|
LikeC4DocumentSymbolProvider,
|
|
@@ -69,6 +70,7 @@ export interface LikeC4AddedServices {
|
|
|
69
70
|
ModelChanges: LikeC4ModelChanges
|
|
70
71
|
}
|
|
71
72
|
lsp: {
|
|
73
|
+
CompletionProvider: LikeC4CompletionProvider
|
|
72
74
|
DocumentHighlightProvider: LikeC4DocumentHighlightProvider
|
|
73
75
|
DocumentSymbolProvider: LikeC4DocumentSymbolProvider
|
|
74
76
|
SemanticTokenProvider: LikeC4SemanticTokenProvider
|
|
@@ -100,6 +102,7 @@ export const LikeC4Module: Module<LikeC4Services, PartialLangiumServices & LikeC
|
|
|
100
102
|
ModelLocator: bind(LikeC4ModelLocator)
|
|
101
103
|
},
|
|
102
104
|
lsp: {
|
|
105
|
+
CompletionProvider: bind(LikeC4CompletionProvider),
|
|
103
106
|
DocumentHighlightProvider: bind(LikeC4DocumentHighlightProvider),
|
|
104
107
|
DocumentSymbolProvider: bind(LikeC4DocumentSymbolProvider),
|
|
105
108
|
SemanticTokenProvider: bind(LikeC4SemanticTokenProvider),
|
package/src/protocol.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AutoLayoutDirection,
|
|
3
|
-
BorderStyle,
|
|
4
3
|
ComputedView,
|
|
5
|
-
ElementShape,
|
|
6
4
|
Fqn,
|
|
7
5
|
LikeC4ComputedModel,
|
|
8
6
|
LikeC4Model,
|
|
9
|
-
NonEmptyArray,
|
|
10
7
|
RelationID,
|
|
11
|
-
|
|
8
|
+
ViewChangeOp,
|
|
12
9
|
ViewID
|
|
13
10
|
} from '@likec4/core'
|
|
14
11
|
import type { DocumentUri, Location } from 'vscode-languageserver-protocol'
|
|
@@ -57,31 +54,19 @@ export type LocateRequest = typeof locate
|
|
|
57
54
|
// #endregion
|
|
58
55
|
|
|
59
56
|
export namespace ChangeView {
|
|
60
|
-
|
|
61
57
|
export interface ChangeAutoLayout {
|
|
62
58
|
op: 'change-autolayout'
|
|
63
59
|
layout: AutoLayoutDirection
|
|
64
60
|
}
|
|
65
|
-
|
|
66
|
-
export interface ChangeElementStyle {
|
|
67
|
-
op: 'change-element-style'
|
|
68
|
-
style: {
|
|
69
|
-
border?: BorderStyle
|
|
70
|
-
opacity?: number
|
|
71
|
-
shape?: ElementShape
|
|
72
|
-
color?: ThemeColor
|
|
73
|
-
}
|
|
74
|
-
targets: NonEmptyArray<Fqn>
|
|
75
|
-
}
|
|
76
61
|
}
|
|
77
62
|
|
|
78
63
|
export type ChangeView =
|
|
79
64
|
| ChangeView.ChangeAutoLayout
|
|
80
|
-
|
|
|
65
|
+
| ViewChangeOp
|
|
81
66
|
|
|
82
67
|
export interface ChangeViewRequestParams {
|
|
83
68
|
viewId: ViewID
|
|
84
|
-
|
|
69
|
+
change: ChangeView
|
|
85
70
|
}
|
|
86
71
|
export const changeView = new RequestType<ChangeViewRequestParams, Location | null, void>('likec4/change-view')
|
|
87
72
|
export type ChangeViewRequest = typeof changeView
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nonexhaustive } from '@likec4/core'
|
|
1
2
|
import {
|
|
2
3
|
type AstNode,
|
|
3
4
|
type AstNodeDescription,
|
|
@@ -30,18 +31,35 @@ export class LikeC4ScopeComputation extends DefaultScopeComputation {
|
|
|
30
31
|
])
|
|
31
32
|
) {
|
|
32
33
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
switch (true) {
|
|
35
|
+
case ast.isSpecificationElementKind(spec): {
|
|
36
|
+
if (isTruthy(spec.kind.name)) {
|
|
37
|
+
docExports.push(
|
|
38
|
+
this.descriptions.createDescription(spec.kind, spec.kind.name, document)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
continue
|
|
38
42
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
case ast.isSpecificationTag(spec): {
|
|
44
|
+
if (isTruthy(spec.tag.name)) {
|
|
45
|
+
docExports.push(
|
|
46
|
+
this.descriptions.createDescription(spec.tag, '#' + spec.tag.name, document)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
case ast.isSpecificationRelationshipKind(spec): {
|
|
52
|
+
if (isTruthy(spec.kind.name)) {
|
|
53
|
+
docExports.push(
|
|
54
|
+
this.descriptions.createDescription(spec.kind, spec.kind.name, document),
|
|
55
|
+
this.descriptions.createDescription(spec.kind, '.' + spec.kind.name, document)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
// Thow error if not exhaustive
|
|
61
|
+
default:
|
|
62
|
+
nonexhaustive(spec)
|
|
45
63
|
}
|
|
46
64
|
} catch (e) {
|
|
47
65
|
logError(e)
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CstUtils,
|
|
7
7
|
DefaultScopeProvider,
|
|
8
8
|
DONE_RESULT,
|
|
9
|
+
EMPTY_SCOPE,
|
|
9
10
|
EMPTY_STREAM,
|
|
10
11
|
GrammarUtils,
|
|
11
12
|
type ReferenceInfo,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
} from 'langium'
|
|
18
19
|
import { ast } from '../ast'
|
|
19
20
|
import { elementRef, getFqnElementRef } from '../elementRef'
|
|
20
|
-
import {
|
|
21
|
+
import { logger } from '../logger'
|
|
21
22
|
import type { FqnIndex, FqnIndexEntry } from '../model/fqn-index'
|
|
22
23
|
import type { LikeC4Services } from '../module'
|
|
23
24
|
|
|
@@ -102,26 +103,31 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
override getScope(context: ReferenceInfo): Scope {
|
|
105
|
-
const referenceType = this.reflection.getReferenceType(context)
|
|
106
106
|
try {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
|
|
107
|
+
const referenceType = this.reflection.getReferenceType(context)
|
|
108
|
+
try {
|
|
109
|
+
const container = context.container
|
|
110
|
+
if (ast.isFqnElementRef(container) && context.property === 'el') {
|
|
111
|
+
const parent = container.parent
|
|
112
|
+
if (!parent) {
|
|
113
|
+
return this.getGlobalScope(referenceType)
|
|
114
|
+
}
|
|
115
|
+
return new StreamScope(this.directChildrenOf(getFqnElementRef(parent)))
|
|
112
116
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return new StreamScope(this.scopeElementRef(parent))
|
|
117
|
+
if (ast.isElementRef(container) && context.property === 'el') {
|
|
118
|
+
const parent = container.parent
|
|
119
|
+
if (parent) {
|
|
120
|
+
return new StreamScope(this.scopeElementRef(parent))
|
|
121
|
+
}
|
|
119
122
|
}
|
|
123
|
+
return this.computeScope(context)
|
|
124
|
+
} catch (e) {
|
|
125
|
+
logger.error(e)
|
|
126
|
+
return this.getGlobalScope(referenceType)
|
|
120
127
|
}
|
|
121
|
-
return this.computeScope(context)
|
|
122
128
|
} catch (e) {
|
|
123
|
-
|
|
124
|
-
return
|
|
129
|
+
logger.warn(e)
|
|
130
|
+
return EMPTY_SCOPE
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
|
package/src/validation/view.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { CstUtils, type ValidationCheck } from 'langium'
|
|
2
2
|
import { ast } from '../ast'
|
|
3
3
|
import type { LikeC4Services } from '../module'
|
|
4
|
+
import { deserializeFromComment, hasManualLayout } from '../view-utils/manual-layout'
|
|
4
5
|
|
|
5
6
|
export const viewChecks = (services: LikeC4Services): ValidationCheck<ast.LikeC4View> => {
|
|
6
7
|
const index = services.shared.workspace.IndexManager
|
|
7
8
|
return (el, accept) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const commentNode = CstUtils.findCommentNode(el.$cstNode, ['BLOCK_COMMENT'])
|
|
10
|
+
if (commentNode && hasManualLayout(commentNode.text) && !deserializeFromComment(commentNode.text)) {
|
|
11
|
+
accept('warning', `Malformed @likec4-generated (ignored)`, {
|
|
12
|
+
node: el,
|
|
13
|
+
range: commentNode.range
|
|
14
|
+
})
|
|
15
|
+
}
|
|
11
16
|
if (!el.name) {
|
|
12
17
|
return
|
|
13
18
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { invariant } from '@likec4/core'
|
|
2
|
+
import type { EdgeId, Fqn, ViewManualLayout } from '@likec4/core/types'
|
|
3
|
+
import JSON5 from 'json5'
|
|
4
|
+
import { chunk, entries, hasAtLeast, mapToObj } from 'remeda'
|
|
5
|
+
|
|
6
|
+
export namespace CompactViewManualLayout {
|
|
7
|
+
export type Node = [
|
|
8
|
+
id: Fqn,
|
|
9
|
+
x: number,
|
|
10
|
+
y: number,
|
|
11
|
+
width: number,
|
|
12
|
+
height: number
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export type Edge = [
|
|
16
|
+
id: EdgeId,
|
|
17
|
+
// flatten array of points, [x1, y1, x2, y2, ...]
|
|
18
|
+
controlPoints: number[]
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
// TODO replace with Zod/Valibot
|
|
22
|
+
export function isCompactLayout(layout: any): layout is CompactViewManualLayout {
|
|
23
|
+
return Array.isArray(layout) && hasAtLeast(layout, 3)
|
|
24
|
+
&& layout[0] === 1
|
|
25
|
+
&& Array.isArray(layout[1])
|
|
26
|
+
&& Array.isArray(layout[2])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pack(layout: ViewManualLayout): CompactViewManualLayout {
|
|
30
|
+
return [
|
|
31
|
+
1,
|
|
32
|
+
entries.strict(layout.nodes).map(([id, { x, y, width, height }]) => [id, x, y, width, height]),
|
|
33
|
+
entries.strict(layout.edges).map(([id, { controlPoints }]) => [id, controlPoints.flatMap(({ x, y }) => [x, y])])
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function unpack([_v, nodes, edges]: CompactViewManualLayout): ViewManualLayout {
|
|
38
|
+
return {
|
|
39
|
+
nodes: mapToObj(nodes, ([id, x, y, width, height]) => [id, { x, y, width, height }]),
|
|
40
|
+
// edges: Object.fromEntries(edges.map(([id, controlPoints]) => [id, { controlPoints: mapWithFeedback(controlPoints, (x, y) => ({ x, y }), { x: 0, y: 0 }) }])
|
|
41
|
+
edges: mapToObj(edges, ([id, controlPoints]) => {
|
|
42
|
+
return [id, {
|
|
43
|
+
controlPoints: chunk(controlPoints, 2).map(([x, y = 0]) => ({ x, y }))
|
|
44
|
+
}]
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type CompactViewManualLayout = [
|
|
51
|
+
1, // version
|
|
52
|
+
nodes: Array<CompactViewManualLayout.Node>,
|
|
53
|
+
edges: Array<CompactViewManualLayout.Edge>
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
export function serializeToComment(layout: ViewManualLayout) {
|
|
57
|
+
const compacted = CompactViewManualLayout.pack(layout)
|
|
58
|
+
const encoded = btoa(JSON5.stringify(compacted))
|
|
59
|
+
const lines = chunk(Array.from(encoded), 100).map(l => ' * ' + l.join(''))
|
|
60
|
+
lines.unshift(
|
|
61
|
+
'/**',
|
|
62
|
+
' * @likec4-generated(v1)'
|
|
63
|
+
)
|
|
64
|
+
lines.push(' */')
|
|
65
|
+
|
|
66
|
+
return lines.join('\n')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function hasManualLayout(comment: string) {
|
|
70
|
+
return comment.includes('@likec4-generated')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function deserializeFromComment(comment: string): ViewManualLayout | undefined {
|
|
74
|
+
if (!hasManualLayout(comment)) {
|
|
75
|
+
console.error(`Not a likec4-generated comment: ${comment}`)
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const b64 = comment
|
|
80
|
+
.trim()
|
|
81
|
+
.split('\n')
|
|
82
|
+
.filter(l => !l.includes('**') && !l.includes('@likec4-') && !l.includes('*/'))
|
|
83
|
+
.map(l => l.replaceAll('*', '').trim())
|
|
84
|
+
.join('')
|
|
85
|
+
const decodedb64 = atob(b64)
|
|
86
|
+
const compacted = JSON5.parse(decodedb64)
|
|
87
|
+
invariant(CompactViewManualLayout.isCompactLayout(compacted), 'Invalid compacted layout')
|
|
88
|
+
return CompactViewManualLayout.unpack(compacted)
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error(e)
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Monarch syntax highlighting for the likec4 language.
|
|
2
|
-
export default {
|
|
3
|
-
keywords: [
|
|
4
|
-
'BottomTop','LeftRight','RightLeft','TopBottom','amber','autoLayout','blue','border','browser','color','crow','cylinder','dashed','description','diamond','dotted','dynamic','element','exclude','extend','extends','gray','green','head','icon','include','indigo','it','kind','line','link','mobile','model','muted','navigateTo','none','normal','odiamond','of','onormal','opacity','open','person','primary','queue','rectangle','red','relationship','secondary','shape','sky','slate','solid','specification','storage','style','tag','tail','technology','this','title','vee','view','views','with'
|
|
5
|
-
],
|
|
6
|
-
operators: [
|
|
7
|
-
'*','->','<-','<->'
|
|
8
|
-
],
|
|
9
|
-
symbols: /\*|->|-\[|<-|<->|\]->|\{|\}/,
|
|
10
|
-
|
|
11
|
-
tokenizer: {
|
|
12
|
-
initial: [
|
|
13
|
-
{ regex: /\w+:\/\/\S+/, action: {"token":"URI_WITH_SCHEMA"} },
|
|
14
|
-
{ regex: /\.{0,2}\/[^\/]\S+/, action: {"token":"URI_RELATIVE"} },
|
|
15
|
-
{ regex: /\b\._/, action: {"token":"DotUnderscore"} },
|
|
16
|
-
{ regex: /\b\.\*/, action: {"token":"DotWildcard"} },
|
|
17
|
-
{ regex: /\#\b/, action: {"token":"TagHash"} },
|
|
18
|
-
{ regex: /\b\./, action: {"token":"Dot"} },
|
|
19
|
-
{ regex: /\!\={1,2}/, action: {"token":"NotEqual"} },
|
|
20
|
-
{ regex: /\={1,2}/, action: {"token":"Eq"} },
|
|
21
|
-
{ regex: /:/, action: {"token":"Colon"} },
|
|
22
|
-
{ regex: /;/, action: {"token":"SemiColon"} },
|
|
23
|
-
{ regex: /,/, action: {"token":"Comma"} },
|
|
24
|
-
{ regex: /\b\d+%/, action: {"token":"Percent"} },
|
|
25
|
-
{ regex: /"[^"]*"|'[^']*'/, action: {"token":"string"} },
|
|
26
|
-
{ regex: /\b[_]*[a-zA-Z][_-\w]*/, action: { cases: { '@keywords': {"token":"keyword"}, '@default': {"token":"IdTerminal"} }} },
|
|
27
|
-
{ include: '@whitespace' },
|
|
28
|
-
{ regex: /@symbols/, action: { cases: { '@operators': {"token":"operator"}, '@default': {"token":""} }} },
|
|
29
|
-
],
|
|
30
|
-
whitespace: [
|
|
31
|
-
{ regex: /\/\*/, action: {"token":"comment","next":"@comment"} },
|
|
32
|
-
{ regex: /\/\/[^\n\r]*/, action: {"token":"comment"} },
|
|
33
|
-
{ regex: /\s+/, action: {"token":"white"} },
|
|
34
|
-
],
|
|
35
|
-
comment: [
|
|
36
|
-
{ regex: /[^/\*]+/, action: {"token":"comment"} },
|
|
37
|
-
{ regex: /\*\//, action: {"token":"comment","next":"@pop"} },
|
|
38
|
-
{ regex: /[/\*]/, action: {"token":"comment"} },
|
|
39
|
-
],
|
|
40
|
-
}
|
|
41
|
-
};
|