@likec4/language-server 1.2.2 → 1.3.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.
Files changed (36) hide show
  1. package/package.json +19 -8
  2. package/src/ast.ts +2 -0
  3. package/src/generated/ast.ts +157 -123
  4. package/src/generated/grammar.ts +2 -2
  5. package/src/generated/module.ts +1 -1
  6. package/src/like-c4.langium +53 -34
  7. package/src/logger.ts +21 -7
  8. package/src/lsp/CompletionProvider.ts +7 -0
  9. package/src/lsp/SemanticTokenProvider.ts +78 -17
  10. package/src/lsp/index.ts +1 -0
  11. package/src/model/model-builder.ts +3 -39
  12. package/src/model/model-parser.ts +19 -4
  13. package/src/model-change/ModelChanges.ts +58 -53
  14. package/src/model-change/changeElementStyle.ts +5 -6
  15. package/src/model-change/saveManualLayout.ts +43 -0
  16. package/src/model-graph/LikeC4ModelGraph.ts +304 -0
  17. package/src/model-graph/compute-view/__test__/fixture.ts +438 -0
  18. package/src/model-graph/compute-view/compute.ts +430 -0
  19. package/src/model-graph/compute-view/index.ts +33 -0
  20. package/src/model-graph/compute-view/predicates.ts +404 -0
  21. package/src/model-graph/dynamic-view/__test__/fixture.ts +56 -0
  22. package/src/model-graph/dynamic-view/compute.ts +198 -0
  23. package/src/model-graph/dynamic-view/index.ts +29 -0
  24. package/src/model-graph/index.ts +3 -0
  25. package/src/model-graph/utils/applyElementCustomProperties.ts +49 -0
  26. package/src/model-graph/utils/applyViewRuleStyles.ts +68 -0
  27. package/src/model-graph/utils/buildComputeNodes.ts +61 -0
  28. package/src/model-graph/utils/sortNodes.ts +105 -0
  29. package/src/module.ts +3 -0
  30. package/src/protocol.ts +3 -18
  31. package/src/references/scope-computation.ts +29 -11
  32. package/src/references/scope-provider.ts +22 -16
  33. package/src/validation/view.ts +9 -4
  34. package/src/view-utils/manual-layout.ts +93 -0
  35. package/contrib/likec4.monarch.ts +0 -41
  36. 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
- ThemeColor,
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
- | ChangeView.ChangeElementStyle
65
+ | ViewChangeOp
81
66
 
82
67
  export interface ChangeViewRequestParams {
83
68
  viewId: ViewID
84
- changes: NonEmptyArray<ChangeView>
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
- if (ast.isSpecificationTag(spec)) {
34
- if (spec.tag && isTruthy(spec.tag.name)) {
35
- docExports.push(
36
- this.descriptions.createDescription(spec.tag, '#' + spec.tag.name, document)
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
- continue
40
- }
41
- if (spec.kind && isTruthy(spec.kind.name)) {
42
- docExports.push(
43
- this.descriptions.createDescription(spec.kind, spec.kind.name, document)
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 { logError } from '../logger'
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 container = context.container
108
- if (ast.isFqnElementRef(container) && context.property === 'el') {
109
- const parent = container.parent
110
- if (!parent) {
111
- return this.getGlobalScope(referenceType)
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
- return new StreamScope(this.directChildrenOf(getFqnElementRef(parent)))
114
- }
115
- if (ast.isElementRef(container) && context.property === 'el') {
116
- const parent = container.parent
117
- if (parent) {
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
- logError(e)
124
- return this.getGlobalScope(referenceType)
129
+ logger.warn(e)
130
+ return EMPTY_SCOPE
125
131
  }
126
132
  }
127
133
 
@@ -1,13 +1,18 @@
1
- import type { ValidationCheck } from 'langium'
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
- // if (el.extends) {
9
- // // TODO: circular dependency check
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
- };