@likec4/language-server 1.6.0 → 1.7.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/contrib/likec4.tmLanguage.json +1 -1
- package/package.json +23 -19
- package/src/Rpc.ts +1 -1
- package/src/ast.ts +34 -9
- package/src/{browser/index.ts → browser.ts} +4 -1
- package/src/generated/ast.ts +498 -152
- package/src/generated/grammar.ts +2 -2
- package/src/generated/module.ts +1 -1
- package/src/index.ts +1 -1
- package/src/like-c4.langium +116 -44
- package/src/logger.ts +76 -55
- package/src/lsp/DocumentLinkProvider.ts +1 -1
- package/src/lsp/DocumentSymbolProvider.ts +1 -1
- package/src/lsp/HoverProvider.ts +1 -1
- package/src/lsp/SemanticTokenProvider.ts +54 -26
- package/src/model/model-builder.ts +11 -8
- package/src/model/model-locator.ts +12 -25
- package/src/model/model-parser-where.ts +75 -0
- package/src/model/model-parser.ts +168 -68
- package/src/model-change/ModelChanges.ts +2 -3
- package/src/model-change/changeElementStyle.ts +4 -1
- package/src/model-change/changeViewLayout.ts +8 -8
- package/src/model-change/saveManualLayout.ts +4 -6
- package/src/model-graph/LikeC4ModelGraph.ts +50 -48
- package/src/model-graph/compute-view/__test__/fixture.ts +41 -16
- package/src/model-graph/compute-view/compute.ts +135 -69
- package/src/model-graph/compute-view/predicates.ts +232 -136
- package/src/model-graph/dynamic-view/__test__/fixture.ts +5 -1
- package/src/model-graph/dynamic-view/compute.ts +50 -41
- package/src/model-graph/utils/applyCustomElementProperties.ts +31 -29
- package/src/model-graph/utils/applyCustomRelationProperties.ts +52 -15
- package/src/model-graph/utils/elementExpressionToPredicate.ts +8 -3
- package/src/module.ts +4 -18
- package/src/{node/index.ts → node.ts} +1 -1
- package/src/protocol.ts +2 -2
- package/src/shared/NodeKindProvider.ts +4 -2
- package/src/test/setup.ts +13 -0
- package/src/test/testServices.ts +1 -1
- package/src/validation/dynamic-view-rule.ts +12 -12
- package/src/validation/index.ts +6 -6
- package/src/validation/relation.ts +1 -1
- package/src/validation/view-predicates/{custom-element-expr.ts → element-with.ts} +11 -10
- package/src/validation/view-predicates/expanded-element.ts +2 -10
- package/src/validation/view-predicates/incoming.ts +1 -1
- package/src/validation/view-predicates/index.ts +2 -2
- package/src/validation/view-predicates/outgoing.ts +1 -1
- package/src/validation/view-predicates/{custom-relation-expr.ts → relation-with.ts} +2 -2
- package/src/validation/view.ts +8 -9
- package/src/view-utils/manual-layout.ts +65 -72
- package/src/view-utils/resolve-relative-paths.ts +28 -17
- package/src/view-utils/view-hash.ts +33 -0
|
@@ -2,9 +2,9 @@ import { AstUtils, type ValidationCheck } from 'langium'
|
|
|
2
2
|
import { ast } from '../../ast'
|
|
3
3
|
import type { LikeC4Services } from '../../module'
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const relationPredicateWithChecks = (
|
|
6
6
|
_services: LikeC4Services
|
|
7
|
-
): ValidationCheck<ast.
|
|
7
|
+
): ValidationCheck<ast.RelationPredicateWith> => {
|
|
8
8
|
return (el, accept) => {
|
|
9
9
|
const container = AstUtils.getContainerOfType(el, ast.isViewRulePredicate)
|
|
10
10
|
if (ast.isExcludePredicate(container)) {
|
package/src/validation/view.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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'
|
|
5
4
|
|
|
6
5
|
export const viewChecks = (services: LikeC4Services): ValidationCheck<ast.LikeC4View> => {
|
|
7
6
|
const index = services.shared.workspace.IndexManager
|
|
8
7
|
return (el, accept) => {
|
|
9
|
-
const commentNode = CstUtils.findCommentNode(el.$cstNode, ['BLOCK_COMMENT'])
|
|
10
|
-
if (commentNode && hasManualLayout(commentNode.text) && !deserializeFromComment(commentNode.text)) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
8
|
+
// const commentNode = CstUtils.findCommentNode(el.$cstNode, ['BLOCK_COMMENT'])
|
|
9
|
+
// if (commentNode && hasManualLayout(commentNode.text) && !deserializeFromComment(commentNode.text)) {
|
|
10
|
+
// accept('warning', `Malformed @likec4-generated (ignored)`, {
|
|
11
|
+
// node: el,
|
|
12
|
+
// range: commentNode.range
|
|
13
|
+
// })
|
|
14
|
+
// }
|
|
16
15
|
if (!el.name) {
|
|
17
16
|
return
|
|
18
17
|
}
|
|
@@ -1,64 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
1
|
+
import type { ViewManualLayout } from '@likec4/core/types'
|
|
2
|
+
import { decode, encode } from '@msgpack/msgpack'
|
|
3
|
+
import { fromBase64, toBase64 } from '@smithy/util-base64'
|
|
4
|
+
import { mapValues } from 'remeda'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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(layout.nodes).map(([id, { x, y, width, height }]) => [id as Fqn, x, y, width, height]),
|
|
33
|
-
entries(layout.edges).map((
|
|
34
|
-
[id, { controlPoints }]
|
|
35
|
-
) => [id as EdgeId, controlPoints.flatMap(({ x, y }) => [x, y])])
|
|
36
|
-
]
|
|
6
|
+
function pack({
|
|
7
|
+
nodes,
|
|
8
|
+
edges,
|
|
9
|
+
...rest
|
|
10
|
+
}: ViewManualLayout) {
|
|
11
|
+
return {
|
|
12
|
+
...rest,
|
|
13
|
+
nodes: mapValues(nodes, ({ x, y, width, height, isCompound, ...n }) => ({
|
|
14
|
+
...n,
|
|
15
|
+
b: [x, y, width, height] as const,
|
|
16
|
+
c: isCompound
|
|
17
|
+
})),
|
|
18
|
+
edges: mapValues(edges, ({ points, controlPoints, labelBBox, dotpos, ...e }) => ({
|
|
19
|
+
...!!controlPoints && { cp: controlPoints },
|
|
20
|
+
...!!labelBBox && { l: labelBBox },
|
|
21
|
+
...!!dotpos && { dp: dotpos },
|
|
22
|
+
...e,
|
|
23
|
+
p: points
|
|
24
|
+
}))
|
|
37
25
|
}
|
|
26
|
+
}
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
function unpack({
|
|
29
|
+
nodes,
|
|
30
|
+
edges,
|
|
31
|
+
...rest
|
|
32
|
+
}: ReturnType<typeof pack>): ViewManualLayout {
|
|
33
|
+
return {
|
|
34
|
+
...rest,
|
|
35
|
+
nodes: mapValues(nodes, ({ b, c, ...n }) => ({
|
|
36
|
+
x: b[0],
|
|
37
|
+
y: b[1],
|
|
38
|
+
width: b[2],
|
|
39
|
+
height: b[3],
|
|
40
|
+
isCompound: c,
|
|
41
|
+
...n
|
|
42
|
+
})),
|
|
43
|
+
edges: mapValues(edges, ({ p, cp, l, dp, ...e }) => ({
|
|
44
|
+
...!!cp && { controlPoints: cp },
|
|
45
|
+
...!!l && { labelBBox: l },
|
|
46
|
+
...!!dp && { dotpos: dp },
|
|
47
|
+
...e,
|
|
48
|
+
points: p
|
|
49
|
+
}))
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export type CompactViewManualLayout = [
|
|
53
|
-
1, // version
|
|
54
|
-
nodes: Array<CompactViewManualLayout.Node>,
|
|
55
|
-
edges: Array<CompactViewManualLayout.Edge>
|
|
56
|
-
]
|
|
57
|
-
|
|
58
53
|
export function serializeToComment(layout: ViewManualLayout) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const lines =
|
|
54
|
+
const bytes = encode(pack(layout))
|
|
55
|
+
const base64 = toBase64(bytes)
|
|
56
|
+
const lines = [] as string[]
|
|
57
|
+
let offset = 0
|
|
58
|
+
const MAX_LINE_LENGTH = 200
|
|
59
|
+
while (offset < base64.length) {
|
|
60
|
+
lines.push(' * ' + base64.slice(offset, Math.min(offset + MAX_LINE_LENGTH, base64.length)))
|
|
61
|
+
offset += MAX_LINE_LENGTH
|
|
62
|
+
}
|
|
62
63
|
lines.unshift(
|
|
63
64
|
'/**',
|
|
64
65
|
' * @likec4-generated(v1)'
|
|
@@ -72,24 +73,16 @@ export function hasManualLayout(comment: string) {
|
|
|
72
73
|
return comment.includes('@likec4-generated')
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
export function deserializeFromComment(comment: string): ViewManualLayout
|
|
76
|
+
export function deserializeFromComment(comment: string): ViewManualLayout {
|
|
76
77
|
if (!hasManualLayout(comment)) {
|
|
77
|
-
|
|
78
|
-
return undefined
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
const b64 = comment
|
|
82
|
-
.trim()
|
|
83
|
-
.split('\n')
|
|
84
|
-
.filter(l => !l.includes('**') && !l.includes('@likec4-') && !l.includes('*/'))
|
|
85
|
-
.map(l => l.replaceAll('*', '').trim())
|
|
86
|
-
.join('')
|
|
87
|
-
const decodedb64 = atob(b64)
|
|
88
|
-
const compacted = JSON5.parse(decodedb64)
|
|
89
|
-
invariant(CompactViewManualLayout.isCompactLayout(compacted), 'Invalid compacted layout')
|
|
90
|
-
return CompactViewManualLayout.unpack(compacted)
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.error(e)
|
|
93
|
-
return undefined
|
|
78
|
+
throw new Error(`Not a likec4-generated comment: ${comment}`)
|
|
94
79
|
}
|
|
80
|
+
const b64 = comment
|
|
81
|
+
.trim()
|
|
82
|
+
.split('\n')
|
|
83
|
+
.filter(l => !l.includes('**') && !l.includes('@likec4-') && !l.includes('*/'))
|
|
84
|
+
.map(l => l.replaceAll('*', '').trim())
|
|
85
|
+
.join('')
|
|
86
|
+
const decodedb64 = fromBase64(b64)
|
|
87
|
+
return unpack(decode(decodedb64) as any) as ViewManualLayout
|
|
95
88
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { LikeC4View } from '@likec4/core'
|
|
2
2
|
import { invariant } from '@likec4/core'
|
|
3
|
-
import { hasAtLeast, unique, zip } from 'remeda'
|
|
3
|
+
import { filter, hasAtLeast, isTruthy, map, pipe, unique, zip } from 'remeda'
|
|
4
4
|
|
|
5
5
|
function commonAncestorPath(views: LikeC4View[], sep = '/') {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const uniqURIs = pipe(
|
|
7
|
+
views,
|
|
8
|
+
map(v => v.docUri),
|
|
9
|
+
filter(isTruthy),
|
|
10
|
+
unique()
|
|
11
|
+
)
|
|
8
12
|
if (uniqURIs.length === 0) return ''
|
|
9
|
-
if (uniqURIs.length === 1) {
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
if (hasAtLeast(uniqURIs, 1) && uniqURIs.length === 1) {
|
|
14
|
+
const parts = new URL(uniqURIs[0]).pathname.split(sep)
|
|
15
|
+
if (parts.length <= 1) return sep
|
|
16
|
+
parts.pop() // remove filename
|
|
17
|
+
return parts.join(sep) + sep
|
|
12
18
|
}
|
|
13
19
|
invariant(hasAtLeast(uniqURIs, 2), 'Expected at least 2 unique URIs')
|
|
14
20
|
const [baseUri, ...tail] = uniqURIs
|
|
@@ -31,7 +37,8 @@ function commonAncestorPath(views: LikeC4View[], sep = '/') {
|
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export function resolveRelativePaths(views: LikeC4View[]): LikeC4View[] {
|
|
34
|
-
const
|
|
40
|
+
const sep = '/'
|
|
41
|
+
const commonPrefix = commonAncestorPath(views, sep)
|
|
35
42
|
return (
|
|
36
43
|
views
|
|
37
44
|
// For each view, compute the relative path to the common prefix
|
|
@@ -43,12 +50,19 @@ export function resolveRelativePaths(views: LikeC4View[]): LikeC4View[] {
|
|
|
43
50
|
parts: []
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
let path = new URL(view.docUri).pathname
|
|
54
|
+
if (commonPrefix.length > 0) {
|
|
55
|
+
invariant(
|
|
56
|
+
path.startsWith(commonPrefix),
|
|
57
|
+
`Expect path "${path}" to start with common prefix: "${commonPrefix}"`
|
|
58
|
+
)
|
|
59
|
+
path = path.slice(commonPrefix.length)
|
|
60
|
+
} else {
|
|
61
|
+
path = path.includes(sep) ? path.slice(path.lastIndexOf(sep) + 1) : path
|
|
62
|
+
}
|
|
49
63
|
return {
|
|
50
64
|
...view,
|
|
51
|
-
parts
|
|
65
|
+
parts: path.split(sep)
|
|
52
66
|
}
|
|
53
67
|
})
|
|
54
68
|
// Sort views by path segments
|
|
@@ -72,13 +86,10 @@ export function resolveRelativePaths(views: LikeC4View[]): LikeC4View[] {
|
|
|
72
86
|
})
|
|
73
87
|
// Build relativePath from path segments
|
|
74
88
|
.map(({ parts, ...view }) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
relativePath: parts.join('/')
|
|
79
|
-
}
|
|
89
|
+
return {
|
|
90
|
+
...view,
|
|
91
|
+
relativePath: parts.join(sep)
|
|
80
92
|
}
|
|
81
|
-
return view
|
|
82
93
|
})
|
|
83
94
|
)
|
|
84
95
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ComputedView } from '@likec4/core/types'
|
|
2
|
+
import objectHash from 'object-hash'
|
|
3
|
+
import { isString, pick } from 'remeda'
|
|
4
|
+
import type { SetOptional } from 'type-fest'
|
|
5
|
+
|
|
6
|
+
export function calcViewLayoutHash<V extends ComputedView>(view: SetOptional<V, 'hash'>): V {
|
|
7
|
+
const tohash = {
|
|
8
|
+
id: view.id,
|
|
9
|
+
__: view.__ ?? 'element',
|
|
10
|
+
autoLayout: view.autoLayout,
|
|
11
|
+
nodes: view.nodes
|
|
12
|
+
.map(pick(['id', 'title', 'description', 'technology', 'shape', 'icon', 'children']))
|
|
13
|
+
.toSorted((a, b) => a.id.localeCompare(b.id)),
|
|
14
|
+
edges: view.edges
|
|
15
|
+
.map(pick(['id', 'source', 'target', 'label', 'description', 'technology', 'dir', 'head', 'tail', 'line']))
|
|
16
|
+
.toSorted((a, b) => a.id.localeCompare(b.id))
|
|
17
|
+
}
|
|
18
|
+
view.hash = objectHash(tohash, {
|
|
19
|
+
ignoreUnknown: true,
|
|
20
|
+
respectType: false,
|
|
21
|
+
replacer(value) {
|
|
22
|
+
if (!isString(value)) {
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
value = value.trim()
|
|
26
|
+
if (value.match('^(aws|tech|gcp|https|http)')) {
|
|
27
|
+
value = 'U' // hide urls
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
return view as V
|
|
33
|
+
}
|