@likec4/language-server 1.7.4 → 1.8.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.
@@ -1,6 +1,6 @@
1
1
  import type { ComputedNode, ViewRule } from '@likec4/core'
2
- import { Expr, nonNullable } from '@likec4/core'
3
- import { isEmpty, isNonNullish, isNullish, omitBy, pickBy } from 'remeda'
2
+ import { Expr } from '@likec4/core'
3
+ import { isEmpty, isNullish, omitBy } from 'remeda'
4
4
  import { elementExprToPredicate } from './elementExpressionToPredicate'
5
5
 
6
6
  export function applyCustomElementProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
@@ -15,12 +15,13 @@ export function applyCustomElementProperties(_rules: ViewRule[], _nodes: Compute
15
15
  } of rules
16
16
  ) {
17
17
  const { border, opacity, ...rest } = omitBy(props, isNullish)
18
+ const notEmpty = !isEmpty(rest)
18
19
  const satisfies = elementExprToPredicate(expr)
19
20
  nodes.forEach((node, i) => {
20
21
  if (!satisfies(node)) {
21
22
  return
22
23
  }
23
- if (!isEmpty(rest)) {
24
+ if (notEmpty) {
24
25
  node = {
25
26
  ...node,
26
27
  isCustomized: true,
@@ -25,6 +25,9 @@ export function applyViewRuleStyles(_rules: ViewRule[], nodes: ComputedNode[]) {
25
25
  if (isDefined(rule.style.icon)) {
26
26
  n.icon = rule.style.icon
27
27
  }
28
+ if (isDefined(rule.notation)) {
29
+ n.notation = rule.notation
30
+ }
28
31
  let styleOverride: ComputedNode['style'] | undefined
29
32
  if (isDefined(rule.style.border)) {
30
33
  styleOverride = { border: rule.style.border }
@@ -0,0 +1,63 @@
1
+ import type { ComputedNode, ElementNotation } from '@likec4/core'
2
+ import { entries, flatMap, groupBy, map, mapValues, pipe, piped, prop, sortBy, unique } from 'remeda'
3
+
4
+ /**
5
+ * Build element notations from computed nodes:
6
+ * 1. Group by notation
7
+ * 2. Group by shape
8
+ * 3. Group by color
9
+ * 4. For each group get unique kinds
10
+ * 5. Unwind the groups
11
+ */
12
+ export function buildElementNotations(nodes: ComputedNode[]): ElementNotation[] {
13
+ return pipe(
14
+ nodes,
15
+ groupBy(prop('notation')),
16
+ mapValues(
17
+ piped(
18
+ groupBy(prop('shape')),
19
+ mapValues(
20
+ piped(
21
+ groupBy(prop('color')),
22
+ mapValues(
23
+ piped(
24
+ map(prop('kind')),
25
+ unique()
26
+ )
27
+ ),
28
+ entries(),
29
+ map(([color, kinds]) => ({
30
+ kinds,
31
+ color
32
+ }))
33
+ )
34
+ ),
35
+ entries(),
36
+ flatMap(([shape, colors]) =>
37
+ colors.map(({ color, kinds }) => ({
38
+ shape,
39
+ color,
40
+ kinds
41
+ }))
42
+ )
43
+ )
44
+ ),
45
+ entries(),
46
+ flatMap(([title, shapes]) =>
47
+ shapes.map(({ shape, color, kinds }) => ({
48
+ title,
49
+ shape,
50
+ color,
51
+ kinds
52
+ }))
53
+ ),
54
+ sortBy(
55
+ prop('title'),
56
+ prop('shape'),
57
+ [
58
+ n => n.kinds.length,
59
+ 'desc'
60
+ ]
61
+ )
62
+ )
63
+ }
@@ -82,15 +82,15 @@ export class LikeC4ScopeComputation extends DefaultScopeComputation {
82
82
  docExports: AstNodeDescription[],
83
83
  document: LikeC4LangiumDocument
84
84
  ) {
85
- if (isNullish(likec4lib) || likec4lib.length === 0) {
85
+ if (isNullish(likec4lib)) {
86
86
  return
87
87
  }
88
- for (const iconAst of likec4lib.flatMap(l => l.icons)) {
89
- try {
88
+ try {
89
+ for (const iconAst of likec4lib.flatMap(l => l.icons)) {
90
90
  docExports.push(this.descriptions.createDescription(iconAst, iconAst.name, document))
91
- } catch (e) {
92
- logError(e)
93
91
  }
92
+ } catch (e) {
93
+ logError(e)
94
94
  }
95
95
  }
96
96
 
@@ -1,4 +1,4 @@
1
- import type { likec4 as c4 } from '@likec4/core'
1
+ import { invariant, type likec4 as c4 } from '@likec4/core'
2
2
  import type { AstNode } from 'langium'
3
3
  import {
4
4
  type AstNodeDescription,
@@ -7,6 +7,7 @@ import {
7
7
  DONE_RESULT,
8
8
  EMPTY_SCOPE,
9
9
  EMPTY_STREAM,
10
+ MapScope,
10
11
  type ReferenceInfo,
11
12
  type Scope,
12
13
  type Stream,
@@ -102,6 +103,17 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
102
103
  if (parent) {
103
104
  return new StreamScope(this.scopeElementRef(parent))
104
105
  }
106
+ // if we have elementRef "this" or "it" we resolve it to the closest element
107
+ if (context.reference.$refText === 'this' || context.reference.$refText === 'it') {
108
+ const closestElement = AstUtils.getContainerOfType(container, ast.isElement)
109
+ if (closestElement) {
110
+ return new MapScope([
111
+ this.descriptions.createDescription(closestElement, context.reference.$refText)
112
+ ])
113
+ } else {
114
+ return EMPTY_SCOPE
115
+ }
116
+ }
105
117
  }
106
118
  return this.computeScope(context)
107
119
  } catch (e) {
@@ -116,26 +128,26 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
116
128
 
117
129
  protected computeScope(context: ReferenceInfo) {
118
130
  const referenceType = this.reflection.getReferenceType(context)
131
+ // computeScope is called only for elements
132
+ invariant(referenceType === ast.Element, 'Invalid reference type')
119
133
  const scopes: Stream<AstNodeDescription>[] = []
120
134
  const doc = getDocument(context.container)
121
135
  const precomputed = doc.precomputedScopes
122
136
 
123
137
  if (precomputed) {
124
138
  const byReferenceType = (desc: AstNodeDescription) => this.reflection.isSubtype(desc.type, referenceType)
125
-
126
139
  let container: AstNode | undefined = context.container
127
140
  while (container) {
128
141
  const elements = precomputed.get(container).filter(byReferenceType)
129
142
  if (elements.length > 0) {
130
143
  scopes.push(stream(elements))
131
144
  }
132
- if (referenceType === ast.Element) {
133
- if (ast.isExtendElementBody(container)) {
134
- scopes.push(this.scopeExtendElement(container.$container))
135
- }
136
- if (ast.isElementViewBody(container)) {
137
- scopes.push(this.scopeElementView(container.$container))
138
- }
145
+
146
+ if (ast.isExtendElementBody(container)) {
147
+ scopes.push(this.scopeExtendElement(container.$container))
148
+ }
149
+ if (ast.isElementViewBody(container)) {
150
+ scopes.push(this.scopeElementView(container.$container))
139
151
  }
140
152
  container = container.$container
141
153
  }
@@ -0,0 +1,24 @@
1
+ import { type AstNode, interruptAndCheck, type ValidationAcceptor, type ValidationCheck } from 'langium'
2
+ import type { CancellationToken } from 'vscode-jsonrpc'
3
+ import { logWarnError } from '../logger'
4
+
5
+ export const RESERVED_WORDS = [
6
+ 'this',
7
+ 'it',
8
+ 'self',
9
+ 'super',
10
+ 'likec4lib'
11
+ ]
12
+
13
+ export function tryOrLog<T extends AstNode>(fn: ValidationCheck<T>): ValidationCheck<T> {
14
+ return async (node: T, accept: ValidationAcceptor, cancelToken: CancellationToken) => {
15
+ if (cancelToken) {
16
+ await interruptAndCheck(cancelToken)
17
+ }
18
+ try {
19
+ await fn(node, accept, cancelToken)
20
+ } catch (e) {
21
+ logWarnError(e)
22
+ }
23
+ }
24
+ }
@@ -9,10 +9,6 @@ export const dynamicViewRulePredicate = (
9
9
  return (predicate, accept) => {
10
10
  const expr = elementExpressionFromPredicate(predicate.value)
11
11
  switch (true) {
12
- case ast.isElementRef(expr):
13
- case ast.isElementDescedantsExpression(expr):
14
- case ast.isExpandElementExpression(expr):
15
- return
16
12
  case ast.isElementKindExpression(expr):
17
13
  case ast.isElementTagExpression(expr):
18
14
  case ast.isWildcardExpression(expr): {
@@ -21,8 +17,6 @@ export const dynamicViewRulePredicate = (
21
17
  })
22
18
  return
23
19
  }
24
- default:
25
- nonexhaustive(expr)
26
20
  }
27
21
  }
28
22
  }
@@ -1,13 +1,14 @@
1
1
  import { AstUtils, type ValidationCheck } from 'langium'
2
2
  import type { ast } from '../ast'
3
3
  import type { LikeC4Services } from '../module'
4
+ import { RESERVED_WORDS, tryOrLog } from './_shared'
4
5
 
5
6
  const { getDocument } = AstUtils
6
7
 
7
8
  export const elementChecks = (services: LikeC4Services): ValidationCheck<ast.Element> => {
8
9
  const fqnIndex = services.likec4.FqnIndex
9
10
  const locator = services.workspace.AstNodeLocator
10
- return (el, accept) => {
11
+ return tryOrLog((el, accept) => {
11
12
  const fqn = fqnIndex.getFqn(el)
12
13
  if (!fqn) {
13
14
  accept('error', 'Not indexed element', {
@@ -16,6 +17,12 @@ export const elementChecks = (services: LikeC4Services): ValidationCheck<ast.Ele
16
17
  })
17
18
  return
18
19
  }
20
+ if (RESERVED_WORDS.includes(el.name)) {
21
+ accept('error', `Reserved word: ${el.name}`, {
22
+ node: el,
23
+ property: 'name'
24
+ })
25
+ }
19
26
  const doc = getDocument(el)
20
27
  const docUri = doc.uri
21
28
  const elPath = locator.getAstNodePath(el)
@@ -45,12 +52,5 @@ export const elementChecks = (services: LikeC4Services): ValidationCheck<ast.Ele
45
52
  }
46
53
  )
47
54
  }
48
- // for (let i = 3; i < el.props.length; i++) {
49
- // accept('error', `Too many properties, max 3 allowed`, {
50
- // node: el,
51
- // property: 'props',
52
- // index: i
53
- // })
54
- // }
55
- }
55
+ })
56
56
  }
@@ -5,7 +5,7 @@ import { dynamicViewRulePredicate } from './dynamic-view-rule'
5
5
  import { dynamicViewStep } from './dynamic-view-step'
6
6
  import { elementChecks } from './element'
7
7
  import { iconPropertyRuleChecks, opacityPropertyRuleChecks } from './property-checks'
8
- import { relationChecks } from './relation'
8
+ import { relationBodyChecks, relationChecks } from './relation'
9
9
  import {
10
10
  elementKindChecks,
11
11
  modelRuleChecks,
@@ -37,6 +37,7 @@ export function registerValidationChecks(services: LikeC4Services) {
37
37
  Element: elementChecks(services),
38
38
  ElementKind: elementKindChecks(services),
39
39
  Relation: relationChecks(services),
40
+ RelationBody: relationBodyChecks(services),
40
41
  Tag: tagChecks(services),
41
42
  DynamicViewPredicateIterator: dynamicViewRulePredicate(services),
42
43
  ElementPredicateWith: elementPredicateWithChecks(services),
@@ -28,7 +28,7 @@ export const iconPropertyRuleChecks = (
28
28
  })
29
29
  }
30
30
  if (
31
- ast.isStyleProperties(container) && ast.isElementBody(container.$container)
31
+ ast.isElementStyleProperty(container) && ast.isElementBody(container.$container)
32
32
  && container.$container.props.some(p => ast.isIconProperty(p))
33
33
  ) {
34
34
  accept('warning', `Redundant as icon defined on element`, {
@@ -1,57 +1,63 @@
1
1
  import { isSameHierarchy } from '@likec4/core'
2
2
  import type { ValidationCheck } from 'langium'
3
+ import { isDefined } from 'remeda'
3
4
  import { ast } from '../ast'
4
5
  import { elementRef } from '../elementRef'
5
- import { logError } from '../logger'
6
6
  import type { LikeC4Services } from '../module'
7
+ import { tryOrLog } from './_shared'
7
8
 
8
9
  export const relationChecks = (services: LikeC4Services): ValidationCheck<ast.Relation> => {
9
10
  const fqnIndex = services.likec4.FqnIndex
10
- return (el, accept) => {
11
- try {
12
- const targetEl: ast.Element | undefined = elementRef(el.target)
13
- const target = targetEl && fqnIndex.getFqn(targetEl)
14
- if (!target) {
15
- accept('error', 'Target not found (not parsed/indexed yet)', {
11
+ return tryOrLog((el, accept) => {
12
+ const targetEl: ast.Element | undefined = elementRef(el.target)
13
+ const target = targetEl && fqnIndex.getFqn(targetEl)
14
+ if (!target) {
15
+ accept('error', 'Target not resolved', {
16
+ node: el,
17
+ property: 'target'
18
+ })
19
+ }
20
+ let sourceEl
21
+ if (isDefined(el.source)) {
22
+ sourceEl = elementRef(el.source)
23
+ if (!sourceEl) {
24
+ return accept('error', 'Source not resolved', {
16
25
  node: el,
17
- property: 'target'
26
+ property: 'source'
18
27
  })
19
28
  }
20
- let sourceEl
21
- if (ast.isExplicitRelation(el)) {
22
- sourceEl = elementRef(el.source)
23
- if (!sourceEl) {
24
- return accept('error', 'Source not found (not parsed/indexed yet)', {
25
- node: el,
26
- property: 'source'
27
- })
28
- }
29
- } else {
30
- sourceEl = el.$container.$container
31
- }
32
-
33
- const source = fqnIndex.getFqn(sourceEl)
34
-
35
- if (!source) {
36
- accept('error', 'Source not found (not parsed/indexed yet)', {
29
+ } else {
30
+ if (!ast.isElementBody(el.$container)) {
31
+ return accept('error', 'Sourceless relation must be nested', {
37
32
  node: el
38
33
  })
39
34
  }
35
+ sourceEl = el.$container.$container
36
+ }
40
37
 
41
- if (source && target && isSameHierarchy(source, target)) {
42
- accept('error', 'Invalid parent-child relationship', {
43
- node: el
44
- })
45
- }
38
+ const source = fqnIndex.getFqn(sourceEl)
46
39
 
47
- if (el.tags?.values && el.body?.tags?.values) {
48
- accept('error', 'Relation cannot have tags in both header and body', {
49
- node: el,
50
- property: 'tags'
51
- })
52
- }
53
- } catch (e) {
54
- logError(e)
40
+ if (!source) {
41
+ accept('error', 'Source not resolved', {
42
+ node: el
43
+ })
44
+ }
45
+
46
+ if (source && target && isSameHierarchy(source, target)) {
47
+ accept('error', 'Invalid parent-child relationship', {
48
+ node: el
49
+ })
50
+ }
51
+ })
52
+ }
53
+
54
+ export const relationBodyChecks = (_services: LikeC4Services): ValidationCheck<ast.RelationBody> => {
55
+ return tryOrLog((body, accept) => {
56
+ const relation = body.$container
57
+ if (relation.tags?.values && body.tags?.values) {
58
+ accept('error', 'Relation cannot have tags in both header and body', {
59
+ node: body.tags
60
+ })
55
61
  }
56
- }
62
+ })
57
63
  }
@@ -1,6 +1,7 @@
1
1
  import { AstUtils, type ValidationCheck } from 'langium'
2
2
  import { ast } from '../ast'
3
3
  import type { LikeC4Services } from '../module'
4
+ import { RESERVED_WORDS, tryOrLog } from './_shared'
4
5
 
5
6
  export const specificationRuleChecks = (
6
7
  _: LikeC4Services
@@ -39,7 +40,13 @@ export const modelViewsChecks = (_: LikeC4Services): ValidationCheck<ast.ModelVi
39
40
 
40
41
  export const elementKindChecks = (services: LikeC4Services): ValidationCheck<ast.ElementKind> => {
41
42
  const index = services.shared.workspace.IndexManager
42
- return (node, accept) => {
43
+ return tryOrLog((node, accept) => {
44
+ if (RESERVED_WORDS.includes(node.name)) {
45
+ accept('error', `Reserved word: ${node.name}`, {
46
+ node: node,
47
+ property: 'name'
48
+ })
49
+ }
43
50
  const sameKind = index
44
51
  .allElements(ast.ElementKind)
45
52
  .filter(n => n.name === node.name && n.node !== node)
@@ -62,7 +69,7 @@ export const elementKindChecks = (services: LikeC4Services): ValidationCheck<ast
62
69
  }
63
70
  })
64
71
  }
65
- }
72
+ })
66
73
  }
67
74
 
68
75
  export const tagChecks = (services: LikeC4Services): ValidationCheck<ast.Tag> => {
@@ -103,6 +110,12 @@ export const relationshipChecks = (
103
110
  ): ValidationCheck<ast.RelationshipKind> => {
104
111
  const index = services.shared.workspace.IndexManager
105
112
  return (node, accept) => {
113
+ if (RESERVED_WORDS.includes(node.name)) {
114
+ accept('error', `Reserved word: ${node.name}`, {
115
+ node: node,
116
+ property: 'name'
117
+ })
118
+ }
106
119
  const sameKinds = index
107
120
  .allElements(ast.RelationshipKind)
108
121
  .filter(n => n.name === node.name)
@@ -1,6 +1,7 @@
1
1
  import { type ValidationCheck } from 'langium'
2
2
  import { ast } from '../ast'
3
3
  import type { LikeC4Services } from '../module'
4
+ import { RESERVED_WORDS } from './_shared'
4
5
 
5
6
  export const viewChecks = (services: LikeC4Services): ValidationCheck<ast.LikeC4View> => {
6
7
  const index = services.shared.workspace.IndexManager
@@ -15,6 +16,12 @@ export const viewChecks = (services: LikeC4Services): ValidationCheck<ast.LikeC4
15
16
  if (!el.name) {
16
17
  return
17
18
  }
19
+ if (RESERVED_WORDS.includes(el.name)) {
20
+ accept('error', `Reserved word: ${el.name}`, {
21
+ node: el,
22
+ property: 'name'
23
+ })
24
+ }
18
25
  const anotherViews = index
19
26
  .allElements(ast.LikeC4View)
20
27
  .filter(n => n.name === el.name)
@@ -1,6 +1,6 @@
1
1
  import type { ComputedView } from '@likec4/core/types'
2
2
  import objectHash from 'object-hash'
3
- import { isString, pick } from 'remeda'
3
+ import { isTruthy, map, mapToObj, pick, pipe } from 'remeda'
4
4
  import type { SetOptional } from 'type-fest'
5
5
 
6
6
  export function calcViewLayoutHash<V extends ComputedView>(view: SetOptional<V, 'hash'>): V {
@@ -8,26 +8,20 @@ export function calcViewLayoutHash<V extends ComputedView>(view: SetOptional<V,
8
8
  id: view.id,
9
9
  __: view.__ ?? 'element',
10
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))
11
+ nodes: pipe(
12
+ view.nodes,
13
+ map(pick(['id', 'title', 'description', 'technology', 'shape', 'icon', 'children'])),
14
+ mapToObj(({ id, icon, ...node }) => [id, { ...node, icon: isTruthy(icon) ? 'Y' : 'N' }])
15
+ ),
16
+ edges: pipe(
17
+ view.edges,
18
+ map(pick(['source', 'target', 'label', 'description', 'technology', 'dir', 'head', 'tail', 'line'])),
19
+ mapToObj(({ source, target, ...edge }) => [`${source}:${target}`, edge])
20
+ )
17
21
  }
18
22
  view.hash = objectHash(tohash, {
19
23
  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
- }
24
+ respectType: false
31
25
  })
32
26
  return view as V
33
27
  }