@likec4/language-server 1.4.0 → 1.5.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 (34) hide show
  1. package/README.md +1 -1
  2. package/contrib/likec4.tmLanguage.json +1 -1
  3. package/package.json +13 -13
  4. package/src/Rpc.ts +23 -1
  5. package/src/ast.ts +11 -12
  6. package/src/generated/ast.ts +101 -31
  7. package/src/generated/grammar.ts +1 -1
  8. package/src/like-c4.langium +58 -33
  9. package/src/lsp/SemanticTokenProvider.ts +12 -14
  10. package/src/model/fqn-computation.ts +29 -7
  11. package/src/model/fqn-index.ts +18 -31
  12. package/src/model/model-builder.ts +13 -9
  13. package/src/model/model-locator.ts +7 -7
  14. package/src/model/model-parser.ts +63 -18
  15. package/src/model-change/changeElementStyle.ts +2 -2
  16. package/src/model-graph/compute-view/__test__/fixture.ts +51 -23
  17. package/src/model-graph/compute-view/compute.ts +47 -16
  18. package/src/model-graph/compute-view/predicates.ts +6 -1
  19. package/src/model-graph/dynamic-view/compute.ts +2 -2
  20. package/src/model-graph/utils/{applyElementCustomProperties.ts → applyCustomElementProperties.ts} +5 -3
  21. package/src/model-graph/utils/applyCustomRelationProperties.ts +50 -0
  22. package/src/model-graph/utils/applyViewRuleStyles.ts +11 -34
  23. package/src/model-graph/utils/elementExpressionToPredicate.ts +32 -0
  24. package/src/references/scope-provider.ts +3 -23
  25. package/src/validation/dynamic-view-rule.ts +5 -7
  26. package/src/validation/element.ts +8 -4
  27. package/src/validation/index.ts +2 -0
  28. package/src/validation/view-predicates/custom-element-expr.ts +17 -6
  29. package/src/validation/view-predicates/custom-relation-expr.ts +15 -0
  30. package/src/validation/view-predicates/index.ts +1 -0
  31. package/src/view-utils/assignNavigateTo.ts +2 -2
  32. package/src/view-utils/manual-layout.ts +4 -2
  33. package/src/view-utils/resolve-extended-views.ts +2 -2
  34. package/src/view-utils/resolve-relative-paths.ts +3 -3
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  BorderStyle,
3
3
  ComputedView,
4
+ CustomRelationExpr as C4CustomRelationExpr,
4
5
  Element,
5
6
  ElementExpression as C4ElementExpression,
6
7
  ElementKind,
@@ -9,6 +10,8 @@ import type {
9
10
  Fqn,
10
11
  Relation,
11
12
  RelationID,
13
+ RelationshipArrowType,
14
+ RelationshipLineType,
12
15
  Tag,
13
16
  ThemeColor,
14
17
  ViewID,
@@ -16,8 +19,7 @@ import type {
16
19
  ViewRuleExpression,
17
20
  ViewRuleStyle
18
21
  } from '@likec4/core'
19
- import { pluck } from 'rambdax'
20
- import { indexBy, isString, pick } from 'remeda'
22
+ import { indexBy, isString, map, prop } from 'remeda'
21
23
  import { LikeC4ModelGraph } from '../../LikeC4ModelGraph'
22
24
  import { computeElementView } from '../index'
23
25
 
@@ -195,16 +197,23 @@ export type FakeElementIds = keyof typeof fakeElements
195
197
  const rel = ({
196
198
  source,
197
199
  target,
198
- title
200
+ title,
201
+ ...props
199
202
  }: {
200
203
  source: FakeElementIds
201
204
  target: FakeElementIds
202
205
  title?: string
206
+ kind?: string
207
+ color?: ThemeColor
208
+ line?: RelationshipLineType
209
+ head?: RelationshipArrowType
210
+ tail?: RelationshipArrowType
203
211
  }): Relation => ({
204
212
  id: `${source}:${target}` as RelationID,
205
213
  title: title ?? '',
206
214
  source: source as Fqn,
207
- target: target as Fqn
215
+ target: target as Fqn,
216
+ ...(props as any)
208
217
  })
209
218
 
210
219
  export const fakeRelations = [
@@ -251,17 +260,24 @@ export const fakeRelations = [
251
260
  rel({
252
261
  source: 'cloud.frontend.dashboard',
253
262
  target: 'cloud.backend.graphql',
254
- title: 'requests'
263
+ kind: 'graphlql',
264
+ title: 'requests',
265
+ line: 'solid'
255
266
  }),
256
267
  rel({
257
268
  source: 'cloud.frontend.adminPanel',
258
269
  target: 'cloud.backend.graphql',
259
- title: 'fetches'
270
+ kind: 'graphlql',
271
+ title: 'fetches',
272
+ line: 'dashed',
273
+ tail: 'odiamond'
260
274
  }),
261
275
  rel({
262
276
  source: 'cloud',
263
277
  target: 'amazon',
264
- title: 'uses'
278
+ title: 'uses',
279
+ head: 'diamond',
280
+ tail: 'odiamond'
265
281
  }),
266
282
  rel({
267
283
  source: 'cloud.backend',
@@ -328,7 +344,7 @@ type CustomExpr = {
328
344
  }
329
345
  }
330
346
 
331
- type Expression =
347
+ export type Expression =
332
348
  | ElementRefExpr
333
349
  | InOutExpr
334
350
  | IncomingExpr
@@ -336,7 +352,19 @@ type Expression =
336
352
  | RelationExpr
337
353
  | CustomExpr
338
354
 
339
- function toExpression(expr: Expression): C4Expression {
355
+ export function $customRelation(
356
+ relation: RelationExpr,
357
+ props: Omit<C4CustomRelationExpr['customRelation'], 'relation'>
358
+ ): C4CustomRelationExpr {
359
+ return {
360
+ customRelation: {
361
+ relation: $expr(relation) as any,
362
+ ...props
363
+ }
364
+ }
365
+ }
366
+
367
+ export function $expr(expr: Expression | C4Expression): C4Expression {
340
368
  if (!isString(expr)) {
341
369
  return expr as C4Expression
342
370
  }
@@ -346,31 +374,31 @@ function toExpression(expr: Expression): C4Expression {
346
374
  if (expr.startsWith('->')) {
347
375
  if (expr.endsWith('->')) {
348
376
  return {
349
- inout: toExpression(expr.replace(/->/g, '').trim() as ElementRefExpr) as any
377
+ inout: $expr(expr.replace(/->/g, '').trim() as ElementRefExpr) as any
350
378
  }
351
379
  }
352
380
  return {
353
- incoming: toExpression(expr.replace('-> ', '') as ElementRefExpr) as any
381
+ incoming: $expr(expr.replace('-> ', '') as ElementRefExpr) as any
354
382
  }
355
383
  }
356
384
  if (expr.endsWith(' ->')) {
357
385
  return {
358
- outgoing: toExpression(expr.replace(' ->', '') as ElementRefExpr) as any
386
+ outgoing: $expr(expr.replace(' ->', '') as ElementRefExpr) as any
359
387
  }
360
388
  }
361
389
  if (expr.includes(' <-> ')) {
362
390
  const [source, target] = expr.split(' <-> ')
363
391
  return {
364
- source: toExpression(source as ElementRefExpr) as any,
365
- target: toExpression(target as ElementRefExpr) as any,
392
+ source: $expr(source as ElementRefExpr) as any,
393
+ target: $expr(target as ElementRefExpr) as any,
366
394
  isBidirectional: true
367
395
  }
368
396
  }
369
397
  if (expr.includes(' -> ')) {
370
398
  const [source, target] = expr.split(' -> ')
371
399
  return {
372
- source: toExpression(source as ElementRefExpr) as any,
373
- target: toExpression(target as ElementRefExpr) as any
400
+ source: $expr(source as ElementRefExpr) as any,
401
+ target: $expr(target as ElementRefExpr) as any
374
402
  }
375
403
  }
376
404
  if (expr.endsWith('._')) {
@@ -390,20 +418,20 @@ function toExpression(expr: Expression): C4Expression {
390
418
  }
391
419
  }
392
420
 
393
- export function $include(expr: Expression): ViewRuleExpression {
421
+ export function $include(expr: Expression | C4Expression): ViewRuleExpression {
394
422
  return {
395
- include: [toExpression(expr)]
423
+ include: [$expr(expr)]
396
424
  }
397
425
  }
398
- export function $exclude(expr: Expression): ViewRuleExpression {
426
+ export function $exclude(expr: Expression | C4Expression): ViewRuleExpression {
399
427
  return {
400
- exclude: [toExpression(expr)]
428
+ exclude: [$expr(expr)]
401
429
  }
402
430
  }
403
431
 
404
432
  export function $style(element: ElementRefExpr, style: ViewRuleStyle['style']): ViewRuleStyle {
405
433
  return {
406
- targets: [toExpression(element) as C4ElementExpression],
434
+ targets: [$expr(element) as C4ElementExpression],
407
435
  style: Object.assign({}, style)
408
436
  }
409
437
  }
@@ -432,7 +460,7 @@ export function computeView(
432
460
  )
433
461
  }
434
462
  return Object.assign(result, {
435
- nodeIds: pluck('id', result.nodes) as string[],
436
- edgeIds: pluck('id', result.edges) as string[]
463
+ nodeIds: map(result.nodes, prop('id')) as string[],
464
+ edgeIds: map(result.edges, prop('id')) as string[]
437
465
  })
438
466
  }
@@ -14,15 +14,16 @@ import {
14
14
  Expr,
15
15
  invariant,
16
16
  isAncestor,
17
- isStrictElementView,
17
+ isScopedElementView,
18
18
  isViewRuleAutoLayout,
19
19
  isViewRuleExpression,
20
20
  nonexhaustive,
21
21
  parentFqn
22
22
  } from '@likec4/core'
23
- import { hasAtLeast, isTruthy, unique } from 'remeda'
23
+ import { first, flatMap, hasAtLeast, isTruthy, unique } from 'remeda'
24
24
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
25
- import { applyElementCustomProperties } from '../utils/applyElementCustomProperties'
25
+ import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
26
+ import { applyCustomRelationProperties } from '../utils/applyCustomRelationProperties'
26
27
  import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
27
28
  import { buildComputeNodes } from '../utils/buildComputeNodes'
28
29
  import { sortNodes } from '../utils/sortNodes'
@@ -35,6 +36,7 @@ import {
35
36
  excludeRelationExpr,
36
37
  excludeWildcardRef,
37
38
  includeCustomElement,
39
+ includeCustomRelation,
38
40
  includeElementKindOrTag,
39
41
  includeElementRef,
40
42
  includeExpandedElementExpr,
@@ -132,7 +134,7 @@ export class ComputeCtx {
132
134
  // but we need to keep the initial sort
133
135
  const initialSort = elements.flatMap(e => nodesMap.get(e.id) ?? [])
134
136
 
135
- const nodes = applyElementCustomProperties(
137
+ const nodes = applyCustomElementProperties(
136
138
  rules,
137
139
  applyViewRuleStyles(
138
140
  rules,
@@ -156,18 +158,18 @@ export class ComputeCtx {
156
158
  ...view,
157
159
  autoLayout: autoLayoutRule?.autoLayout ?? 'TB',
158
160
  nodes,
159
- edges: [...sortedEdges]
161
+ edges: applyCustomRelationProperties(rules, nodes, sortedEdges)
160
162
  }
161
163
  }
162
164
 
163
165
  protected get root() {
164
- return isStrictElementView(this.view) ? this.view.viewOf : null
166
+ return isScopedElementView(this.view) ? this.view.viewOf : null
165
167
  }
166
168
 
167
169
  protected get computedEdges(): ComputedEdge[] {
168
170
  return this.ctxEdges.map((e): ComputedEdge => {
169
171
  invariant(hasAtLeast(e.relations, 1), 'Edge must have at least one relation')
170
- const relations = [...e.relations].sort(compareRelations)
172
+ const relations = e.relations.toSorted(compareRelations)
171
173
  const source = e.source.id
172
174
  const target = e.target.id
173
175
 
@@ -185,21 +187,44 @@ export class ComputeCtx {
185
187
  relation = relations[0]
186
188
  } else {
187
189
  relation = relations.find(r => r.source === source && r.target === target)
188
- relation ??= relations.find(r => r.source === source || r.target === target)
190
+ // relation ??= relations.toReversed().find(r => r.source === source || r.target === target)
189
191
  }
190
192
 
191
193
  // This edge represents mutliple relations
192
194
  // We use label if only it is the same for all relations
193
195
  if (!relation) {
194
- const labels = unique(relations.flatMap(r => (isTruthy(r.title) ? r.title : [])))
195
- if (hasAtLeast(labels, 1)) {
196
- if (labels.length === 1) {
197
- edge.label = labels[0]
198
- } else {
199
- edge.label = '[...]'
196
+ const shared = relations.reduce((acc, r) => {
197
+ if (r.color && acc.color !== r.color) {
198
+ acc.color = undefined
200
199
  }
201
- }
202
- return edge
200
+ if (r.head && acc.head !== r.kind) {
201
+ acc.head = undefined
202
+ }
203
+ if (r.tail && acc.tail !== r.kind) {
204
+ acc.tail = undefined
205
+ }
206
+ if (r.line && acc.line !== r.line) {
207
+ acc.line = undefined
208
+ }
209
+ if (isTruthy(r.title) && acc.title !== r.title) {
210
+ acc.title = '[...]'
211
+ }
212
+ return acc
213
+ }, {
214
+ title: first(flatMap(relations, r => isTruthy(r.title) ? r.title : [])),
215
+ head: first(flatMap(relations, r => isTruthy(r.head) ? r.head : [])),
216
+ tail: first(flatMap(relations, r => isTruthy(r.tail) ? r.tail : [])),
217
+ color: first(flatMap(relations, r => isTruthy(r.color) ? r.color : [])),
218
+ line: first(flatMap(relations, r => isTruthy(r.line) ? r.line : []))
219
+ })
220
+ return Object.assign(
221
+ edge,
222
+ isTruthy(shared.title) && { label: shared.title },
223
+ shared.color && { color: shared.color },
224
+ shared.line && { line: shared.line },
225
+ shared.head && { head: shared.head },
226
+ shared.tail && { tail: shared.tail }
227
+ )
203
228
  }
204
229
 
205
230
  return Object.assign(
@@ -386,6 +411,12 @@ export class ComputeCtx {
386
411
  }
387
412
  continue
388
413
  }
414
+ if (Expr.isCustomRelationExpr(expr)) {
415
+ if (isInclude) {
416
+ includeCustomRelation.call(this, expr)
417
+ }
418
+ continue
419
+ }
389
420
  if (Expr.isExpandedElementExpr(expr)) {
390
421
  if (isInclude) {
391
422
  includeExpandedElementExpr.call(this, expr)
@@ -1,9 +1,10 @@
1
1
  import type { Element } from '@likec4/core'
2
2
  import { Expr, invariant, nonexhaustive, parentFqn } from '@likec4/core'
3
- import type { Predicate } from 'rambdax'
4
3
  import { isNullish as isNil } from 'remeda'
5
4
  import type { ComputeCtx } from './compute'
6
5
 
6
+ type Predicate<T> = (x: T) => boolean
7
+
7
8
  export function includeElementRef(this: ComputeCtx, expr: Expr.ElementRefExpr) {
8
9
  // Get the elements that are already in the Ctx before any mutations
9
10
  // Because we need to add edges between them and the new elements
@@ -402,3 +403,7 @@ export function includeCustomElement(this: ComputeCtx, expr: Expr.CustomElementE
402
403
  this.addEdges(this.graph.anyEdgesBetween(el, currentElements))
403
404
  }
404
405
  }
406
+
407
+ export function includeCustomRelation(this: ComputeCtx, expr: Expr.CustomRelationExpr) {
408
+ includeRelationExpr.call(this, expr.customRelation.relation)
409
+ }
@@ -17,7 +17,7 @@ import {
17
17
  } from '@likec4/core'
18
18
  import { isTruthy, unique } from 'remeda'
19
19
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
20
- import { applyElementCustomProperties } from '../utils/applyElementCustomProperties'
20
+ import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
21
21
  import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
22
22
  import { buildComputeNodes } from '../utils/buildComputeNodes'
23
23
 
@@ -138,7 +138,7 @@ export class DynamicViewComputeCtx {
138
138
  return edge
139
139
  })
140
140
 
141
- const nodes = applyElementCustomProperties(
141
+ const nodes = applyCustomElementProperties(
142
142
  rules,
143
143
  applyViewRuleStyles(
144
144
  rules,
@@ -1,8 +1,8 @@
1
1
  import type { ComputedNode, ViewRule } from '@likec4/core'
2
2
  import { Expr, nonNullable } from '@likec4/core'
3
- import { isEmpty, isNullish as isNil, omitBy } from 'remeda'
3
+ import { isEmpty, isNonNullish, pickBy } from 'remeda'
4
4
 
5
- export function applyElementCustomProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
5
+ export function applyCustomElementProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
6
6
  const rules = _rules.flatMap(r => ('include' in r ? r.include.filter(Expr.isCustomElement) : []))
7
7
  if (rules.length === 0) {
8
8
  return _nodes
@@ -18,10 +18,11 @@ export function applyElementCustomProperties(_rules: ViewRule[], _nodes: Compute
18
18
  continue
19
19
  }
20
20
  let node = nonNullable(nodes[nodeIdx])
21
- const { border, opacity, ...rest } = omitBy(props, isNil)
21
+ const { border, opacity, ...rest } = pickBy(props, isNonNullish)
22
22
  if (!isEmpty(rest)) {
23
23
  node = {
24
24
  ...node,
25
+ isCustomized: true,
25
26
  ...rest
26
27
  }
27
28
  }
@@ -36,6 +37,7 @@ export function applyElementCustomProperties(_rules: ViewRule[], _nodes: Compute
36
37
  if (styleOverride) {
37
38
  node = {
38
39
  ...node,
40
+ isCustomized: true,
39
41
  style: {
40
42
  ...node.style,
41
43
  ...styleOverride
@@ -0,0 +1,50 @@
1
+ import type { ComputedEdge, ComputedNode, ViewRule } from '@likec4/core'
2
+ import { Expr } from '@likec4/core'
3
+ import { isEmpty } from 'remeda'
4
+ import { elementExprToPredicate } from './elementExpressionToPredicate'
5
+
6
+ export function applyCustomRelationProperties(
7
+ _rules: ViewRule[],
8
+ nodes: ComputedNode[],
9
+ _edges: Iterable<ComputedEdge>
10
+ ): ComputedEdge[] {
11
+ const rules = _rules.flatMap(r => ('include' in r ? r.include.filter(Expr.isCustomRelationExpr) : []))
12
+ const edges = Array.from(_edges)
13
+ if (rules.length === 0) {
14
+ return edges
15
+ }
16
+ for (
17
+ const {
18
+ customRelation: { relation, title, ...props }
19
+ } of rules
20
+ ) {
21
+ if (isEmpty(props) && !title) {
22
+ continue
23
+ }
24
+ const isSource = elementExprToPredicate(relation.source)
25
+ const isTarget = elementExprToPredicate(relation.target)
26
+ const satisfies = (edge: ComputedEdge) => {
27
+ const source = nodes.find(n => n.id === edge.source)
28
+ const target = nodes.find(n => n.id === edge.target)
29
+ if (!source || !target) {
30
+ return false
31
+ }
32
+ let result = isSource(source) && isTarget(target)
33
+ if (!result && relation.isBidirectional) {
34
+ result = isSource(target) && isTarget(source)
35
+ }
36
+ return result
37
+ }
38
+ edges.forEach((edge, i) => {
39
+ if (satisfies(edge)) {
40
+ edges[i] = {
41
+ ...edge,
42
+ label: title ?? edge.label,
43
+ isCustomized: true,
44
+ ...props
45
+ }
46
+ }
47
+ })
48
+ }
49
+ return edges
50
+ }
@@ -1,7 +1,9 @@
1
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'
2
+ import { Expr, isViewRuleStyle } from '@likec4/core'
3
+ import { anyPass, filter, isDefined } from 'remeda'
4
+ import { elementExprToPredicate } from './elementExpressionToPredicate'
5
+
6
+ type Predicate<T> = (x: T) => boolean
5
7
 
6
8
  export function applyViewRuleStyles(_rules: ViewRule[], nodes: ComputedNode[]) {
7
9
  const rules = _rules.filter(isViewRuleStyle)
@@ -15,44 +17,19 @@ export function applyViewRuleStyles(_rules: ViewRule[], nodes: ComputedNode[]) {
15
17
  predicates.push(() => true)
16
18
  break
17
19
  }
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)
20
+ predicates.push(elementExprToPredicate(target))
44
21
  }
45
- filter(anyPass(predicates), nodes).forEach(n => {
22
+ filter(nodes, anyPass(predicates)).forEach(n => {
46
23
  n.shape = rule.style.shape ?? n.shape
47
24
  n.color = rule.style.color ?? n.color
48
- if (isDefined.strict(rule.style.icon)) {
25
+ if (isDefined(rule.style.icon)) {
49
26
  n.icon = rule.style.icon
50
27
  }
51
28
  let styleOverride: ComputedNode['style'] | undefined
52
- if (isDefined.strict(rule.style.border)) {
53
- styleOverride = { ...styleOverride, border: rule.style.border }
29
+ if (isDefined(rule.style.border)) {
30
+ styleOverride = { border: rule.style.border }
54
31
  }
55
- if (isDefined.strict(rule.style.opacity)) {
32
+ if (isDefined(rule.style.opacity)) {
56
33
  styleOverride = { ...styleOverride, opacity: rule.style.opacity }
57
34
  }
58
35
  if (styleOverride) {
@@ -0,0 +1,32 @@
1
+ import type { ComputedNode } from '@likec4/core'
2
+ import { Expr, nonexhaustive, parentFqn } from '@likec4/core'
3
+ import { isNullish } from 'remeda'
4
+
5
+ type Predicate<T> = (x: T) => boolean
6
+
7
+ export function elementExprToPredicate(target: Expr.ElementPredicateExpression): Predicate<ComputedNode> {
8
+ if (Expr.isWildcard(target)) {
9
+ return () => true
10
+ }
11
+ if (Expr.isElementKindExpr(target)) {
12
+ return target.isEqual ? n => n.kind === target.elementKind : n => n.kind !== target.elementKind
13
+ }
14
+ if (Expr.isElementTagExpr(target)) {
15
+ return target.isEqual
16
+ ? ({ tags }) => !!tags && tags.includes(target.elementTag)
17
+ : ({ tags }) => isNullish(tags) || !tags.includes(target.elementTag)
18
+ }
19
+ if (Expr.isExpandedElementExpr(target)) {
20
+ return n => n.id === target.expanded || parentFqn(n.id) === target.expanded
21
+ }
22
+ if (Expr.isElementRef(target)) {
23
+ const { element, isDescedants } = target
24
+ return isDescedants
25
+ ? n => n.id.startsWith(element + '.')
26
+ : n => (n.id as string) === element
27
+ }
28
+ if (Expr.isCustomElement(target)) {
29
+ return n => (n.id as string) === target.custom.element
30
+ }
31
+ nonexhaustive(target)
32
+ }
@@ -3,12 +3,10 @@ import type { AstNode } from 'langium'
3
3
  import {
4
4
  type AstNodeDescription,
5
5
  AstUtils,
6
- CstUtils,
7
6
  DefaultScopeProvider,
8
7
  DONE_RESULT,
9
8
  EMPTY_SCOPE,
10
9
  EMPTY_STREAM,
11
- GrammarUtils,
12
10
  type ReferenceInfo,
13
11
  type Scope,
14
12
  type Stream,
@@ -19,29 +17,11 @@ import {
19
17
  import { ast } from '../ast'
20
18
  import { elementRef, getFqnElementRef } from '../elementRef'
21
19
  import { logger } from '../logger'
22
- import type { FqnIndex, FqnIndexEntry } from '../model/fqn-index'
20
+ import type { FqnIndex } from '../model/fqn-index'
23
21
  import type { LikeC4Services } from '../module'
24
22
 
25
- const { findNodeForProperty } = GrammarUtils
26
- const { toDocumentSegment } = CstUtils
27
23
  const { getDocument } = AstUtils
28
24
 
29
- function toAstNodeDescription(entry: FqnIndexEntry): AstNodeDescription {
30
- const $cstNode = findNodeForProperty(entry.el.$cstNode, 'name')
31
- return {
32
- documentUri: entry.doc.uri,
33
- name: entry.name,
34
- ...(entry.el.$cstNode && {
35
- selectionSegment: toDocumentSegment(entry.el.$cstNode)
36
- }),
37
- ...($cstNode && {
38
- nameSegment: toDocumentSegment($cstNode)
39
- }),
40
- path: entry.path,
41
- type: ast.Element
42
- }
43
- }
44
-
45
25
  export class LikeC4ScopeProvider extends DefaultScopeProvider {
46
26
  private fqnIndex: FqnIndex
47
27
 
@@ -51,7 +31,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
51
31
  }
52
32
 
53
33
  private directChildrenOf(parent: c4.Fqn): Stream<AstNodeDescription> {
54
- return this.fqnIndex.directChildrenOf(parent).map(toAstNodeDescription)
34
+ return this.fqnIndex.directChildrenOf(parent)
55
35
  }
56
36
 
57
37
  // we need lazy resolving here
@@ -61,7 +41,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
61
41
  const element = of()
62
42
  const fqn = element && this.fqnIndex.getFqn(element)
63
43
  if (fqn) {
64
- return this.fqnIndex.uniqueDescedants(fqn).map(toAstNodeDescription).iterator()
44
+ return this.fqnIndex.uniqueDescedants(fqn).iterator()
65
45
  }
66
46
  return null
67
47
  },
@@ -13,17 +13,15 @@ export const dynamicViewRulePredicate = (_services: LikeC4Services): ValidationC
13
13
  case ast.isDescedantsExpr(expr):
14
14
  case ast.isCustomElementExpr(expr):
15
15
  case ast.isExpandElementExpr(expr):
16
- return
17
- case ast.isRelationExpr(expr):
18
- case ast.isInOutExpr(expr):
19
- case ast.isIncomingExpr(expr):
20
- case ast.isOutgoingExpr(expr):
16
+ continue
21
17
  case ast.isElementKindExpr(expr):
22
18
  case ast.isElementTagExpr(expr):
23
- case ast.isWildcardExpr(expr):
24
- return accept('warning', `Expression is not supported by dynamic views`, {
19
+ case ast.isWildcardExpr(expr): {
20
+ accept('warning', `Predicate is ignored, as not supported in dynamic views`, {
25
21
  node: expr
26
22
  })
23
+ continue
24
+ }
27
25
  default:
28
26
  nonexhaustive(expr)
29
27
  }
@@ -6,6 +6,7 @@ const { getDocument } = AstUtils
6
6
 
7
7
  export const elementChecks = (services: LikeC4Services): ValidationCheck<ast.Element> => {
8
8
  const fqnIndex = services.likec4.FqnIndex
9
+ const locator = services.workspace.AstNodeLocator
9
10
  return (el, accept) => {
10
11
  const fqn = fqnIndex.getFqn(el)
11
12
  if (!fqn) {
@@ -15,12 +16,15 @@ export const elementChecks = (services: LikeC4Services): ValidationCheck<ast.Ele
15
16
  })
16
17
  return
17
18
  }
19
+ const doc = getDocument(el)
20
+ const docUri = doc.uri
21
+ const elPath = locator.getAstNodePath(el)
18
22
  const withSameFqn = fqnIndex
19
23
  .byFqn(fqn)
20
- .filter(v => v.el !== el)
24
+ .filter(v => v.documentUri !== docUri || v.path !== elPath)
21
25
  .head()
22
26
  if (withSameFqn) {
23
- const isAnotherDoc = withSameFqn.doc.uri !== getDocument(el).uri
27
+ const isAnotherDoc = withSameFqn.documentUri !== docUri
24
28
  accept(
25
29
  'error',
26
30
  `Duplicate element name ${el.name !== fqn ? el.name + ' (' + fqn + ')' : el.name}`,
@@ -31,8 +35,8 @@ export const elementChecks = (services: LikeC4Services): ValidationCheck<ast.Ele
31
35
  relatedInformation: [
32
36
  {
33
37
  location: {
34
- range: withSameFqn.el.$cstNode!.range,
35
- uri: withSameFqn.doc.uri.toString()
38
+ range: (withSameFqn.nameSegment?.range ?? withSameFqn.selectionSegment?.range)!,
39
+ uri: withSameFqn.documentUri.toString()
36
40
  },
37
41
  message: `conflicting element`
38
42
  }
@@ -17,6 +17,7 @@ import {
17
17
  import { viewChecks } from './view'
18
18
  import {
19
19
  customElementExprChecks,
20
+ customRelationExprChecks,
20
21
  expandElementExprChecks,
21
22
  incomingExpressionChecks,
22
23
  outgoingExpressionChecks
@@ -38,6 +39,7 @@ export function registerValidationChecks(services: LikeC4Services) {
38
39
  Tag: tagChecks(services),
39
40
  DynamicViewRulePredicate: dynamicViewRulePredicate(services),
40
41
  CustomElementExpr: customElementExprChecks(services),
42
+ CustomRelationExpr: customRelationExprChecks(services),
41
43
  ExpandElementExpr: expandElementExprChecks(services),
42
44
  RelationshipKind: relationshipChecks(services),
43
45
  IncomingExpr: incomingExpressionChecks(services),