@likec4/language-server 1.9.0 → 1.10.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.
Files changed (52) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +1 -1
  3. package/dist/browser.d.cts +3 -4
  4. package/dist/browser.d.mts +3 -4
  5. package/dist/browser.d.ts +3 -4
  6. package/dist/browser.mjs +1 -1
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +1 -1
  12. package/dist/model-graph/index.cjs +1 -1
  13. package/dist/model-graph/index.mjs +1 -1
  14. package/dist/node.cjs +1 -1
  15. package/dist/node.d.cts +3 -4
  16. package/dist/node.d.mts +3 -4
  17. package/dist/node.d.ts +3 -4
  18. package/dist/node.mjs +1 -1
  19. package/dist/shared/{language-server.Q-wtPShM.mjs → language-server.BFBeyvV8.mjs} +486 -108
  20. package/dist/shared/{language-server.86lmJ8ZN.d.cts → language-server.BGy3FJPJ.d.cts} +43 -14
  21. package/dist/shared/{language-server.B1TZgyoH.cjs → language-server.Bfc-5M8A.cjs} +482 -104
  22. package/dist/shared/{language-server.CCB4ESN5.mjs → language-server.CbqwHp7Q.mjs} +184 -120
  23. package/dist/shared/{language-server.RjhrBZS0.d.ts → language-server.CnVuAxDh.d.ts} +43 -14
  24. package/dist/shared/{language-server.CFTY6j4e.d.mts → language-server.DEK39RmI.d.mts} +43 -14
  25. package/dist/shared/{language-server.D0bOlrCi.cjs → language-server.DJhoJBWh.cjs} +180 -116
  26. package/package.json +13 -11
  27. package/src/ast.ts +8 -6
  28. package/src/formatting/LikeC4Formatter.ts +390 -0
  29. package/src/formatting/utils.ts +26 -0
  30. package/src/generated/ast.ts +203 -11
  31. package/src/generated/grammar.ts +2 -2
  32. package/src/generated/module.ts +1 -1
  33. package/src/like-c4.langium +34 -7
  34. package/src/lsp/CompletionProvider.ts +1 -1
  35. package/src/lsp/DocumentLinkProvider.ts +27 -15
  36. package/src/lsp/SemanticTokenProvider.ts +1 -1
  37. package/src/lsp/index.ts +1 -1
  38. package/src/model/fqn-index.ts +0 -1
  39. package/src/model/model-builder.ts +43 -32
  40. package/src/model/model-parser.ts +43 -21
  41. package/src/model-graph/compute-view/compute.ts +111 -80
  42. package/src/model-graph/compute-view/predicates.ts +3 -5
  43. package/src/model-graph/dynamic-view/compute.ts +96 -60
  44. package/src/model-graph/utils/buildElementNotations.ts +1 -1
  45. package/src/model-graph/utils/uniqueTags.test.ts +42 -0
  46. package/src/model-graph/utils/uniqueTags.ts +19 -0
  47. package/src/module.ts +6 -9
  48. package/src/test/testServices.ts +27 -7
  49. package/src/validation/index.ts +2 -1
  50. package/src/validation/property-checks.ts +13 -1
  51. package/src/validation/specification.ts +3 -3
  52. package/src/view-utils/resolve-relative-paths.ts +14 -17
@@ -13,7 +13,7 @@ import type {
13
13
  RelationshipKind,
14
14
  RelationshipLineType,
15
15
  Tag,
16
- ThemeColor,
16
+ ViewID,
17
17
  ViewRulePredicate
18
18
  } from '@likec4/core'
19
19
  import {
@@ -30,7 +30,7 @@ import {
30
30
  parentFqn,
31
31
  whereOperatorAsPredicate
32
32
  } from '@likec4/core'
33
- import { first, flatMap, hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
33
+ import { filter, hasAtLeast, isNonNull, isTruthy, map, omit, only, pipe, reduce, sort, unique } from 'remeda'
34
34
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
35
35
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
36
36
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
@@ -39,6 +39,7 @@ import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
39
39
  import { buildComputeNodes } from '../utils/buildComputeNodes'
40
40
  import { buildElementNotations } from '../utils/buildElementNotations'
41
41
  import { sortNodes } from '../utils/sortNodes'
42
+ import { uniqueTags } from '../utils/uniqueTags'
42
43
  import {
43
44
  type ElementPredicateFn,
44
45
  excludeElementKindOrTag,
@@ -192,7 +193,7 @@ export class ComputeCtx {
192
193
  protected computeEdges(): ComputedEdge[] {
193
194
  return this.ctxEdges.map((e): ComputedEdge => {
194
195
  invariant(hasAtLeast(e.relations, 1), 'Edge must have at least one relation')
195
- const relations = e.relations.toSorted(compareRelations)
196
+ const relations = sort(e.relations, compareRelations)
196
197
  const source = e.source.id
197
198
  const target = e.target.id
198
199
 
@@ -205,70 +206,85 @@ export class ComputeCtx {
205
206
  relations: relations.map(r => r.id)
206
207
  }
207
208
 
208
- let relation:
209
- | Pick<Relation, 'title' | 'kind' | 'description' | 'technology' | 'color' | 'line' | 'head' | 'tail' | 'tags'>
210
- | {
211
- // TODO refactor with type-fest
212
- title: string
213
- description?: string | undefined
214
- technology?: string | undefined
215
- kind?: RelationshipKind | undefined
216
- color?: Color | undefined
217
- line?: RelationshipLineType | undefined
218
- head?: RelationshipArrowType | undefined
219
- tail?: RelationshipArrowType | undefined
220
- tags?: NonEmptyArray<Tag>
221
- }
222
- | undefined
223
- if (relations.length === 1) {
224
- relation = relations[0]
225
- } else {
226
- relation = relations.find(r => r.source === source && r.target === target)
227
- // relation ??= relations.toReversed().find(r => r.source === source || r.target === target)
228
- }
209
+ let relation: {
210
+ // TODO refactor with type-fest
211
+ title: string
212
+ description?: string | undefined
213
+ technology?: string | undefined
214
+ kind?: RelationshipKind | undefined
215
+ color?: Color | undefined
216
+ line?: RelationshipLineType | undefined
217
+ head?: RelationshipArrowType | undefined
218
+ tail?: RelationshipArrowType | undefined
219
+ tags?: NonEmptyArray<Tag>
220
+ navigateTo?: ViewID | undefined
221
+ } | undefined
222
+ relation = only(relations) ?? pipe(
223
+ relations,
224
+ filter(r => r.source === source && r.target === target),
225
+ only()
226
+ )
229
227
 
230
228
  // This edge represents mutliple relations
231
229
  // We use label if only it is the same for all relations
232
230
  if (!relation) {
233
- relation = relations.reduce((acc, r) => {
234
- if (r.color && acc.color !== r.color) {
235
- acc.color = undefined
236
- }
237
- if (r.kind && acc.kind !== r.kind) {
238
- acc.kind = undefined
239
- }
240
- if (r.head && acc.head !== r.head) {
241
- acc.head = undefined
242
- }
243
- if (r.tail && acc.tail !== r.tail) {
244
- acc.tail = undefined
245
- }
246
- if (r.line && acc.line !== r.line) {
247
- acc.line = undefined
248
- }
249
- if (r.description && acc.description !== r.description) {
250
- acc.description = undefined
251
- }
252
- if (r.technology && acc.technology !== r.technology) {
253
- acc.technology = undefined
254
- }
255
- if (isTruthy(r.title) && acc.title !== r.title) {
256
- acc.title = '[...]'
257
- }
258
- return acc
259
- }, {
260
- title: first(flatMap(relations, r => isTruthy(r.title) ? r.title : [])) ?? '[...]',
261
- description: first(flatMap(relations, r => isTruthy(r.description) ? r.description : [])),
262
- technology: first(flatMap(relations, r => isTruthy(r.technology) ? r.technology : [])),
263
- kind: first(flatMap(relations, r => isTruthy(r.kind) ? r.kind : [])),
264
- head: first(flatMap(relations, r => isTruthy(r.head) ? r.head : [])),
265
- tail: first(flatMap(relations, r => isTruthy(r.tail) ? r.tail : [])),
266
- color: first(flatMap(relations, r => isTruthy(r.color) ? r.color : [])),
267
- line: first(flatMap(relations, r => isTruthy(r.line) ? r.line : []))
268
- })
231
+ const allprops = pipe(
232
+ relations,
233
+ reduce((acc, r) => {
234
+ if (isTruthy(r.title) && !acc.title.includes(r.title)) {
235
+ acc.title.push(r.title)
236
+ }
237
+ if (isTruthy(r.description) && !acc.description.includes(r.description)) {
238
+ acc.description.push(r.description)
239
+ }
240
+ if (isTruthy(r.technology) && !acc.technology.includes(r.technology)) {
241
+ acc.technology.push(r.technology)
242
+ }
243
+ if (isTruthy(r.kind) && !acc.kind.includes(r.kind)) {
244
+ acc.kind.push(r.kind)
245
+ }
246
+ if (isTruthy(r.color) && !acc.color.includes(r.color)) {
247
+ acc.color.push(r.color)
248
+ }
249
+ if (isTruthy(r.line) && !acc.line.includes(r.line)) {
250
+ acc.line.push(r.line)
251
+ }
252
+ if (isTruthy(r.head) && !acc.head.includes(r.head)) {
253
+ acc.head.push(r.head)
254
+ }
255
+ if (isTruthy(r.tail) && !acc.tail.includes(r.tail)) {
256
+ acc.tail.push(r.tail)
257
+ }
258
+ if (isTruthy(r.navigateTo) && !acc.navigateTo.includes(r.navigateTo)) {
259
+ acc.navigateTo.push(r.navigateTo)
260
+ }
261
+ return acc
262
+ }, {
263
+ title: [] as string[],
264
+ description: [] as string[],
265
+ technology: [] as string[],
266
+ kind: [] as RelationshipKind[],
267
+ head: [] as RelationshipArrowType[],
268
+ tail: [] as RelationshipArrowType[],
269
+ color: [] as Color[],
270
+ line: [] as RelationshipLineType[],
271
+ navigateTo: [] as ViewID[]
272
+ })
273
+ )
274
+ relation = {
275
+ title: only(allprops.title) ?? '[...]',
276
+ description: only(allprops.description),
277
+ technology: only(allprops.technology),
278
+ kind: only(allprops.kind),
279
+ head: only(allprops.head),
280
+ tail: only(allprops.tail),
281
+ color: only(allprops.color),
282
+ line: only(allprops.line),
283
+ navigateTo: only(allprops.navigateTo)
284
+ }
269
285
  }
270
286
 
271
- const tags = unique(flatMap(relations, r => r.tags ?? []))
287
+ const tags = uniqueTags(relations)
272
288
 
273
289
  return Object.assign(
274
290
  edge,
@@ -280,7 +296,8 @@ export class ComputeCtx {
280
296
  relation.line && { line: relation.line },
281
297
  relation.head && { head: relation.head },
282
298
  relation.tail && { tail: relation.tail },
283
- hasAtLeast(tags, 1) && { tags }
299
+ relation.navigateTo && { navigateTo: relation.navigateTo },
300
+ tags && { tags }
284
301
  )
285
302
  })
286
303
  }
@@ -349,29 +366,40 @@ export class ComputeCtx {
349
366
  }
350
367
  }
351
368
 
352
- protected excludeImplicit(...excludes: Element[]) {
353
- for (const el of excludes) {
354
- this.implicits.delete(el)
355
- }
356
- }
369
+ // protected excludeImplicit(...excludes: Element[]) {
370
+ // for (const el of excludes) {
371
+ // this.implicits.delete(el)
372
+ // }
373
+ // }
357
374
 
358
375
  protected excludeRelation(...relations: Relation[]) {
376
+ if (relations.length === 0) {
377
+ return
378
+ }
359
379
  const excludedImplicits = new Set<Element>()
360
- for (const relation of relations) {
361
- let edge
362
- while ((edge = this.ctxEdges.find(e => e.relations.includes(relation)))) {
363
- if (edge.relations.length === 1) {
380
+ const ctxEdges = pipe(
381
+ this.ctxEdges,
382
+ map(edge => {
383
+ const edgerelations = edge.relations.filter(r => !relations.includes(r))
384
+ if (edgerelations.length === 0) {
364
385
  excludedImplicits.add(edge.source)
365
386
  excludedImplicits.add(edge.target)
366
- this.ctxEdges.splice(this.ctxEdges.indexOf(edge), 1)
367
- continue
387
+ return null
368
388
  }
369
- edge.relations = edge.relations.filter(r => r !== relation)
370
- }
371
- }
389
+ if (edgerelations.length !== edge.relations.length) {
390
+ return {
391
+ ...edge,
392
+ relations: edgerelations
393
+ }
394
+ }
395
+ return edge
396
+ }),
397
+ filter(isNonNull)
398
+ )
372
399
  if (excludedImplicits.size === 0) {
373
400
  return
374
401
  }
402
+ this.ctxEdges = ctxEdges
375
403
  const remaining = this.includedElements
376
404
  if (remaining.size === 0) {
377
405
  this.implicits.clear()
@@ -541,9 +569,12 @@ export class ComputeCtx {
541
569
  }
542
570
 
543
571
  protected getEdgeLabel(
544
- relation: { title: String | undefined; technology?: String | undefined }
545
- ): { label: String } | false {
546
- const labelParts: String[] = []
572
+ relation: {
573
+ title: string
574
+ technology?: string | undefined
575
+ }
576
+ ) {
577
+ const labelParts: string[] = []
547
578
 
548
579
  if (isTruthy(relation.title)) {
549
580
  labelParts.push(relation.title)
@@ -553,6 +584,6 @@ export class ComputeCtx {
553
584
  labelParts.push(`[${relation.technology}]`)
554
585
  }
555
586
 
556
- return labelParts.length > 0 && { label: labelParts.join('\n') }
587
+ return labelParts.length > 0 ? { label: labelParts.join('\n') } : {}
557
588
  }
558
589
  }
@@ -1,6 +1,6 @@
1
1
  import type { Element, Relation } from '@likec4/core'
2
2
  import { Expr, isAncestor, nonexhaustive, parentFqn } from '@likec4/core'
3
- import { allPass, filter as remedaFilter, flatMap, isNullish as isNil, map, pipe } from 'remeda'
3
+ import { allPass, filter as remedaFilter, flatMap, isNullish as isNil, map, pipe, unique } from 'remeda'
4
4
  import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
5
5
  import type { ComputeCtx } from './compute'
6
6
 
@@ -318,13 +318,11 @@ const filterEdges = (edges: ReadonlyArray<ComputeCtx.Edge>, where?: RelationPred
318
318
  }
319
319
 
320
320
  const filterRelations = (edges: ComputeCtx.Edge[], where?: RelationPredicateFn) => {
321
- if (!where) {
322
- return edges.flatMap(e => e.relations)
323
- }
324
321
  return pipe(
325
322
  edges,
326
323
  flatMap(e => e.relations),
327
- remedaFilter(where)
324
+ where ? remedaFilter(where) : Identity,
325
+ unique()
328
326
  )
329
327
  }
330
328
 
@@ -3,13 +3,14 @@ import type {
3
3
  ComputedDynamicView,
4
4
  ComputedEdge,
5
5
  DynamicView,
6
+ DynamicViewStep,
6
7
  Element,
7
8
  NonEmptyArray,
8
9
  RelationID,
9
10
  RelationshipArrowType,
10
11
  RelationshipLineType,
11
12
  Tag,
12
- ThemeColor
13
+ ViewID
13
14
  } from '@likec4/core'
14
15
  import {
15
16
  ancestorsFqn,
@@ -18,12 +19,13 @@ import {
18
19
  DefaultLineStyle,
19
20
  DefaultRelationshipColor,
20
21
  isDynamicViewIncludeRule,
22
+ isDynamicViewParallelSteps,
21
23
  isViewRuleAutoLayout,
22
24
  nonNullable,
23
25
  parentFqn,
24
26
  StepEdgeId
25
27
  } from '@likec4/core'
26
- import { hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
28
+ import { filter, flatMap, hasAtLeast, isTruthy, map, omit, only, pipe, unique } from 'remeda'
27
29
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
28
30
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
29
31
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
@@ -34,6 +36,7 @@ import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
34
36
 
35
37
  export namespace DynamicViewComputeCtx {
36
38
  export interface Step {
39
+ id: StepEdgeId
37
40
  source: Element
38
41
  target: Element
39
42
  title: string | null
@@ -45,6 +48,7 @@ export namespace DynamicViewComputeCtx {
45
48
  tail?: RelationshipArrowType
46
49
  relations: RelationID[]
47
50
  isBackward: boolean
51
+ navigateTo?: ViewID
48
52
  tags?: NonEmptyArray<Tag>
49
53
  }
50
54
  }
@@ -63,6 +67,47 @@ export class DynamicViewComputeCtx {
63
67
  protected graph: LikeC4ModelGraph
64
68
  ) {}
65
69
 
70
+ private addStep(
71
+ {
72
+ source: stepSource,
73
+ target: stepTarget,
74
+ title: stepTitle,
75
+ isBackward,
76
+ navigateTo: stepNavigateTo,
77
+ ...step
78
+ }: DynamicViewStep,
79
+ index: number,
80
+ parent?: number
81
+ ) {
82
+ const id = parent ? StepEdgeId(parent, index) : StepEdgeId(index)
83
+ const source = this.graph.element(stepSource)
84
+ const target = this.graph.element(stepTarget)
85
+
86
+ this.explicits.add(source)
87
+ this.explicits.add(target)
88
+
89
+ const {
90
+ title,
91
+ relations,
92
+ tags,
93
+ navigateTo: derivedNavigateTo
94
+ } = this.findRelations(source, target)
95
+
96
+ const navigateTo = isTruthy(stepNavigateTo) && stepNavigateTo !== this.view.id ? stepNavigateTo : derivedNavigateTo
97
+
98
+ this.steps.push({
99
+ id,
100
+ ...step,
101
+ source,
102
+ target,
103
+ title: stepTitle ?? title,
104
+ relations: relations ?? [],
105
+ isBackward: isBackward ?? false,
106
+ ...(navigateTo ? { navigateTo } : {}),
107
+ ...(tags ? { tags } : {})
108
+ })
109
+ }
110
+
66
111
  protected compute(): ComputedDynamicView {
67
112
  const {
68
113
  docUri: _docUri, // exclude docUri
@@ -71,32 +116,21 @@ export class DynamicViewComputeCtx {
71
116
  ...view
72
117
  } = this.view
73
118
 
74
- for (
75
- let {
76
- source: stepSource,
77
- target: stepTarget,
78
- title: stepTitle,
79
- isBackward,
80
- ...step
81
- } of viewSteps
82
- ) {
83
- const source = this.graph.element(stepSource)
84
- const target = this.graph.element(stepTarget)
85
-
86
- this.explicits.add(source)
87
- this.explicits.add(target)
88
-
89
- const { title, relations, tags } = this.findRelations(source, target)
90
-
91
- this.steps.push({
92
- ...step,
93
- source,
94
- target,
95
- title: isTruthy(stepTitle) ? stepTitle : title,
96
- relations: relations ?? [],
97
- isBackward: isBackward ?? false,
98
- ...(tags ? { tags } : {})
99
- })
119
+ let stepNum = 1
120
+ for (const step of viewSteps) {
121
+ if (isDynamicViewParallelSteps(step)) {
122
+ if (step.__parallel.length === 0) {
123
+ continue
124
+ }
125
+ if (step.__parallel.length === 1) {
126
+ this.addStep(step.__parallel[0]!, stepNum)
127
+ } else {
128
+ step.__parallel.forEach((s, i) => this.addStep(s, i + 1, stepNum))
129
+ }
130
+ } else {
131
+ this.addStep(step, stepNum)
132
+ }
133
+ stepNum++
100
134
  }
101
135
 
102
136
  for (const rule of rules) {
@@ -111,12 +145,10 @@ export class DynamicViewComputeCtx {
111
145
  const elements = [...this.explicits]
112
146
  const nodesMap = buildComputeNodes(elements)
113
147
 
114
- const edges = this.steps.map(({ source, target, relations, title, isBackward, ...step }, index) => {
148
+ const edges = this.steps.map(({ source, target, relations, title, isBackward, ...step }) => {
115
149
  const sourceNode = nonNullable(nodesMap.get(source.id), `Source node ${source.id} not found`)
116
150
  const targetNode = nonNullable(nodesMap.get(target.id), `Target node ${target.id} not found`)
117
- const stepNum = index + 1
118
151
  const edge: ComputedEdge = {
119
- id: StepEdgeId(stepNum),
120
152
  parent: commonAncestor(source.id, target.id),
121
153
  source: source.id,
122
154
  target: target.id,
@@ -183,49 +215,53 @@ export class DynamicViewComputeCtx {
183
215
  title: string | null
184
216
  tags: NonEmptyArray<Tag> | null
185
217
  relations: NonEmptyArray<RelationID> | null
218
+ navigateTo: ViewID | null
186
219
  } {
187
220
  const relationships = unique(this.graph.edgesBetween(source, target).flatMap(e => e.relations))
188
- const alltags = unique(relationships.flatMap(r => r.tags ?? []))
189
- const tags = hasAtLeast(alltags, 1) ? alltags : null
190
-
191
- const relations = hasAtLeast(relationships, 1) ? map(relationships, r => r.id) : null
192
221
  if (relationships.length === 0) {
193
222
  return {
194
223
  title: null,
195
- tags,
196
- relations
224
+ tags: null,
225
+ relations: null,
226
+ navigateTo: null
197
227
  }
198
228
  }
199
- let relation
200
- if (relationships.length === 1) {
201
- relation = relationships[0]
202
- } else {
203
- relation = relationships.find(r => r.source === source.id && r.target === target.id)
204
- }
229
+ const alltags = pipe(
230
+ relationships,
231
+ flatMap(r => r.tags),
232
+ filter(isTruthy),
233
+ unique()
234
+ )
235
+ const tags = hasAtLeast(alltags, 1) ? alltags : null
236
+ const relations = hasAtLeast(relationships, 1) ? map(relationships, r => r.id) : null
205
237
 
206
- if (relation && isTruthy(relation.title)) {
207
- return {
208
- title: relation.title,
209
- tags,
210
- relations
211
- }
212
- }
238
+ // Most closest relation
239
+ const relation = only(relationships) || relationships.find(r => r.source === source.id && r.target === target.id)
213
240
 
214
241
  // This edge represents mutliple relations
215
242
  // We use label if only it is the same for all relations
216
- const labels = unique(relationships.flatMap(r => (isTruthy(r.title) ? r.title : [])))
217
- if (labels.length === 1) {
218
- return {
219
- title: labels[0]!,
220
- tags,
221
- relations
222
- }
223
- }
243
+ const title = isTruthy(relation?.title) ? relation.title : pipe(
244
+ relationships,
245
+ map(r => r.title),
246
+ filter(isTruthy),
247
+ unique(),
248
+ only()
249
+ )
250
+
251
+ const navigateTo = !!relation?.navigateTo && relation.navigateTo !== this.view.id ? relation.navigateTo : pipe(
252
+ relationships,
253
+ map(r => r.navigateTo),
254
+ filter(isTruthy),
255
+ filter(v => v !== this.view.id),
256
+ unique(),
257
+ only()
258
+ )
224
259
 
225
260
  return {
226
- title: null,
261
+ title: title ?? null,
227
262
  tags,
228
- relations
263
+ relations,
264
+ navigateTo: navigateTo ?? null
229
265
  }
230
266
  }
231
267
  }
@@ -52,8 +52,8 @@ export function buildElementNotations(nodes: ComputedNode[]): ElementNotation[]
52
52
  }))
53
53
  ),
54
54
  sortBy(
55
- prop('title'),
56
55
  prop('shape'),
56
+ prop('title'),
57
57
  [
58
58
  n => n.kinds.length,
59
59
  'desc'
@@ -0,0 +1,42 @@
1
+ import { compareNatural } from '@likec4/core'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { uniqueTags } from './uniqueTags'
4
+
5
+ describe('uniqueTags function', () => {
6
+ it('returns unique tags from an array of elements', () => {
7
+ const input = [
8
+ { tags: ['tag1', 'tag2', 'tag3'] },
9
+ { tags: ['tag2', 'tag3', 'tag4'] },
10
+ { tags: ['tag3', 'tag4', 'tag5'] }
11
+ ] as const
12
+ const result = uniqueTags(input)
13
+ expect(result).toEqual(['tag1', 'tag2', 'tag3', 'tag4', 'tag5'])
14
+ })
15
+
16
+ it('should return unique tags naturally sorted', () => {
17
+ const input = [
18
+ { tags: ['tag1', 'tag20', 'tag30'] },
19
+ { tags: ['tag2', 'tag23', 'tag34'] },
20
+ { tags: ['tag3'] }
21
+ ] as const
22
+ const result = uniqueTags(input)
23
+ expect(result).toEqual([
24
+ 'tag1',
25
+ 'tag2',
26
+ 'tag3',
27
+ 'tag20',
28
+ 'tag23',
29
+ 'tag30',
30
+ 'tag34'
31
+ ].sort(compareNatural))
32
+ })
33
+
34
+ it('returns null if the tags array is null', () => {
35
+ const input = [
36
+ { tags: null },
37
+ {}
38
+ ]
39
+ const result = uniqueTags(input)
40
+ expect(result).toBeNull()
41
+ })
42
+ })
@@ -0,0 +1,19 @@
1
+ import { compareNatural, hasAtLeast, type NonEmptyReadonlyArray, type Tag } from '@likec4/core'
2
+ import { flatMap, pipe, sort, unique } from 'remeda'
3
+ import type { LiteralUnion } from 'type-fest'
4
+
5
+ /**
6
+ * Extracts unique tags from an array of elements.
7
+ * and sort in natural order; returns null if no tags are present.
8
+ */
9
+ export function uniqueTags<T extends { tags?: NonEmptyReadonlyArray<LiteralUnion<Tag, string>> | null }>(
10
+ elements: ReadonlyArray<T>
11
+ ) {
12
+ const tags = pipe(
13
+ elements,
14
+ flatMap(e => e.tags ?? []),
15
+ unique(),
16
+ sort(compareNatural)
17
+ )
18
+ return hasAtLeast(tags, 1) ? tags : null
19
+ }
package/src/module.ts CHANGED
@@ -17,7 +17,6 @@ import {
17
17
  LikeC4DocumentLinkProvider,
18
18
  LikeC4DocumentSymbolProvider,
19
19
  LikeC4HoverProvider,
20
- LikeC4RenameProvider,
21
20
  LikeC4SemanticTokenProvider
22
21
  } from './lsp'
23
22
  import { FqnIndex, LikeC4ModelBuilder, LikeC4ModelLocator, LikeC4ModelParser } from './model'
@@ -26,6 +25,7 @@ import { LikeC4ScopeComputation, LikeC4ScopeProvider } from './references'
26
25
  import { Rpc } from './Rpc'
27
26
  import { LikeC4WorkspaceManager, NodeKindProvider, WorkspaceSymbolProvider } from './shared'
28
27
  import { registerValidationChecks } from './validation'
28
+ import { LikeC4Formatter } from './formatting/LikeC4Formatter'
29
29
 
30
30
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
31
  type Constructor<T, Arguments extends unknown[] = any[]> = new(...arguments_: Arguments) => T
@@ -70,7 +70,7 @@ export interface LikeC4AddedServices {
70
70
  ModelChanges: LikeC4ModelChanges
71
71
  }
72
72
  lsp: {
73
- RenameProvider: LikeC4RenameProvider
73
+ // RenameProvider: LikeC4RenameProvider
74
74
  CompletionProvider: LikeC4CompletionProvider
75
75
  DocumentHighlightProvider: LikeC4DocumentHighlightProvider
76
76
  DocumentSymbolProvider: LikeC4DocumentSymbolProvider
@@ -103,14 +103,15 @@ export const LikeC4Module: Module<LikeC4Services, PartialLangiumServices & LikeC
103
103
  ModelLocator: bind(LikeC4ModelLocator)
104
104
  },
105
105
  lsp: {
106
- RenameProvider: bind(LikeC4RenameProvider),
106
+ // RenameProvider: bind(LikeC4RenameProvider),
107
107
  CompletionProvider: bind(LikeC4CompletionProvider),
108
108
  DocumentHighlightProvider: bind(LikeC4DocumentHighlightProvider),
109
109
  DocumentSymbolProvider: bind(LikeC4DocumentSymbolProvider),
110
110
  SemanticTokenProvider: bind(LikeC4SemanticTokenProvider),
111
111
  HoverProvider: bind(LikeC4HoverProvider),
112
112
  CodeLensProvider: bind(LikeC4CodeLensProvider),
113
- DocumentLinkProvider: bind(LikeC4DocumentLinkProvider)
113
+ DocumentLinkProvider: bind(LikeC4DocumentLinkProvider),
114
+ Formatter: bind(LikeC4Formatter)
114
115
  },
115
116
  references: {
116
117
  ScopeComputation: bind(LikeC4ScopeComputation),
@@ -177,11 +178,7 @@ export function createLanguageServices(context: LanguageServicesContext = {}): {
177
178
  shared.ServiceRegistry.register(likec4)
178
179
  registerValidationChecks(likec4)
179
180
 
180
- if (!context.connection) {
181
- // We don't run inside a language server
182
- // Therefore, initialize the configuration provider instantly
183
- shared.workspace.ConfigurationProvider.initialized({})
184
- } else {
181
+ if (context.connection) {
185
182
  likec4.Rpc.init()
186
183
  }
187
184