@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
package/src/ast.ts CHANGED
@@ -89,6 +89,7 @@ export interface ParsedAstRelation {
89
89
  head?: c4.RelationshipArrowType
90
90
  tail?: c4.RelationshipArrowType
91
91
  links?: c4.NonEmptyArray<ParsedLink>
92
+ navigateTo?: c4.ViewID
92
93
  metadata?: { [key: string]: string }
93
94
  }
94
95
 
@@ -114,7 +115,7 @@ export interface ParsedAstDynamicView {
114
115
  description: string | null
115
116
  tags: c4.NonEmptyArray<c4.Tag> | null
116
117
  links: c4.NonEmptyArray<ParsedLink> | null
117
- steps: c4.DynamicViewStep[]
118
+ steps: c4.DynamicViewStepOrParallel[]
118
119
  rules: Array<c4.DynamicViewRule>
119
120
  manualLayout?: c4.ViewManualLayout
120
121
  }
@@ -194,14 +195,14 @@ export function cleanParsedModel(doc: LikeC4LangiumDocument) {
194
195
  return Object.assign(doc, props) as ParsedLikeC4LangiumDocument
195
196
  }
196
197
 
197
- export function isFqnIndexedDocument(doc: LangiumDocument): doc is FqnIndexedDocument {
198
- return isLikeC4LangiumDocument(doc) && doc.state >= DocumentState.IndexedContent && !!doc.c4fqnIndex
199
- }
200
-
201
198
  export function isLikeC4LangiumDocument(doc: LangiumDocument): doc is LikeC4LangiumDocument {
202
199
  return doc.textDocument.languageId === LikeC4LanguageMetaData.languageId
203
200
  }
204
201
 
202
+ export function isFqnIndexedDocument(doc: LangiumDocument): doc is FqnIndexedDocument {
203
+ return isLikeC4LangiumDocument(doc) && doc.state >= DocumentState.IndexedContent && !!doc.c4fqnIndex
204
+ }
205
+
205
206
  export function isParsedLikeC4LangiumDocument(
206
207
  doc: LangiumDocument
207
208
  ): doc is ParsedLikeC4LangiumDocument {
@@ -230,6 +231,7 @@ const isValidatableAstNode = validatableAstNodeGuards([
230
231
  ast.isRelationPredicateWith,
231
232
  ast.isElementExpression,
232
233
  ast.isRelationExpression,
234
+ ast.isDynamicViewParallelSteps,
233
235
  ast.isDynamicViewStep,
234
236
  ast.isViewProperty,
235
237
  ast.isStyleProperty,
@@ -265,7 +267,7 @@ const findInvalidContainer = (node: LikeC4AstNode): ValidatableAstNode | undefin
265
267
  }
266
268
  nd = nd.$container
267
269
  }
268
- return
270
+ return undefined
269
271
  }
270
272
 
271
273
  export function checksFromDiagnostics(doc: LikeC4LangiumDocument) {
@@ -0,0 +1,390 @@
1
+ import { type AstNode, GrammarUtils } from 'langium'
2
+ import { AbstractFormatter, Formatting, type NodeFormatter } from 'langium/lsp'
3
+ import * as ast from '../generated/ast'
4
+ import * as utils from './utils'
5
+
6
+ const FormattingOptions = {
7
+ newLine: Formatting.newLine({ allowMore: true }),
8
+ oneSpace: Formatting.oneSpace(),
9
+ noSpace: Formatting.noSpace(),
10
+ indent: Formatting.indent({ allowMore: true }),
11
+ noIndent: Formatting.noIndent()
12
+ }
13
+ type Predicate<T extends AstNode> = (x: unknown) => x is T
14
+
15
+ export class LikeC4Formatter extends AbstractFormatter {
16
+ protected format(node: AstNode): void {
17
+ this.removeIndentFromTopLevelStatements(node)
18
+ this.indentContentInBraces(node)
19
+
20
+ this.formatSpecificationRule(node)
21
+ this.formatElementDeclaration(node)
22
+ this.formatRelation(node)
23
+ this.formatView(node)
24
+ this.formatViewRuleStyle(node)
25
+ this.formatIncludeExcludeExpressions(node)
26
+ this.formatWhereExpression(node)
27
+ this.formatWithPredicate(node)
28
+ this.formatLeafProperty(node)
29
+ this.formatMetadataProperty(node)
30
+ this.formatAutolayoutProperty(node)
31
+ this.formatLinkProperty(node)
32
+ this.formatNavigateToProperty(node)
33
+ this.formatTags(node)
34
+ }
35
+
36
+ protected formatTags(node: AstNode) {
37
+ this.on(node, ast.isTags, (n, f) => {
38
+ f.cst(GrammarUtils.findNodesForProperty(n.$cstNode, 'values').slice(1))
39
+ .prepend(FormattingOptions.oneSpace)
40
+
41
+ f.keywords(',')
42
+ .prepend(FormattingOptions.noSpace)
43
+ .append(FormattingOptions.oneSpace)
44
+ })
45
+ }
46
+
47
+ protected formatRelation(node: AstNode) {
48
+ this.on(node, ast.isRelation, (n, f) => {
49
+ const sourceNodes = n?.source?.$cstNode ? [n?.source?.$cstNode] : []
50
+
51
+ f.cst(sourceNodes).append(FormattingOptions.oneSpace)
52
+ f.keywords(']->').prepend(FormattingOptions.noSpace)
53
+ f.keywords('-[').append(FormattingOptions.noSpace)
54
+
55
+ f.properties('target', 'title', 'technology', 'tags').prepend(FormattingOptions.oneSpace)
56
+ })
57
+
58
+ this.on(node, ast.isDynamicViewStep, (n, f) => {
59
+ f.properties('source').append(FormattingOptions.oneSpace)
60
+ f.keywords(']->').prepend(FormattingOptions.noSpace)
61
+ f.keywords('-[').append(FormattingOptions.noSpace)
62
+ f.properties('target', 'title').prepend(FormattingOptions.oneSpace)
63
+ })
64
+
65
+ this.on(node, ast.isDirectedRelationExpression)
66
+ ?.property('target').prepend(FormattingOptions.oneSpace)
67
+
68
+ this.on(node, ast.isOutgoingRelationExpression, (n, f) => {
69
+ f.property('from').append(FormattingOptions.oneSpace)
70
+ f.keywords(']->').prepend(FormattingOptions.noSpace)
71
+ f.keywords('-[').append(FormattingOptions.noSpace)
72
+ })
73
+
74
+ this.on(node, ast.isIncomingRelationExpression)
75
+ ?.keywords('->').append(FormattingOptions.oneSpace)
76
+
77
+ this.on(node, ast.isInOutRelationExpression)
78
+ ?.keyword('->').prepend(FormattingOptions.oneSpace)
79
+ }
80
+
81
+ protected removeIndentFromTopLevelStatements(node: AstNode) {
82
+ if (
83
+ ast.isModel(node)
84
+ || ast.isSpecificationRule(node)
85
+ || ast.isModelViews(node)
86
+ || ast.isLikeC4Lib(node)
87
+ ) {
88
+ const formatter = this.getNodeFormatter(node)
89
+ formatter.keywords('specification', 'model', 'views', 'likec4lib')
90
+ .prepend(FormattingOptions.noIndent)
91
+ }
92
+ }
93
+
94
+ protected indentContentInBraces(node: AstNode) {
95
+ if (
96
+ ast.isLikeC4Lib(node)
97
+ || ast.isSpecificationRule(node)
98
+ || ast.isSpecificationElementKind(node)
99
+ || ast.isSpecificationRelationshipKind(node)
100
+ || ast.isModel(node)
101
+ || ast.isElementBody(node)
102
+ || ast.isExtendElementBody(node)
103
+ || ast.isRelationBody(node)
104
+ || ast.isRelationStyleProperty(node)
105
+ || ast.isMetadataBody(node)
106
+ || ast.isModelViews(node)
107
+ || ast.isElementViewBody(node)
108
+ || ast.isDynamicViewBody(node)
109
+ || ast.isViewRuleStyle(node)
110
+ || ast.isCustomElementProperties(node)
111
+ || ast.isCustomRelationProperties(node)
112
+ || ast.isElementStyleProperty(node)
113
+ ) {
114
+ const formatter = this.getNodeFormatter(node)
115
+ const openBrace = formatter.keywords('{')
116
+ const closeBrace = formatter.keywords('}')
117
+
118
+ const interiorNodes = formatter.interior(openBrace, closeBrace)
119
+
120
+ // Workaround for tags as they are parsed as overlapping regions.
121
+ // E.g. '#tag1, #tag2' will be parsed as two nodes: '#tag1' and '#tag1, #tag2'
122
+ let perviousNode = null
123
+ for (const interiorNode of interiorNodes.nodes) {
124
+ if (!perviousNode || !utils.areOverlap(perviousNode, interiorNode)) {
125
+ formatter.cst([interiorNode]).prepend(FormattingOptions.indent)
126
+ }
127
+ perviousNode = interiorNode
128
+ }
129
+
130
+ openBrace
131
+ .prepend(FormattingOptions.noIndent)
132
+ .prepend(FormattingOptions.oneSpace)
133
+ closeBrace
134
+ .prepend(FormattingOptions.noIndent)
135
+ .prepend(Formatting.newLine({ allowMore: true }))
136
+ }
137
+ }
138
+
139
+ protected appendKeywordsWithSpace(node: AstNode) {
140
+ this.on(node, ast.isElementKind)
141
+ ?.keywords('element').append(FormattingOptions.oneSpace)
142
+ }
143
+
144
+ protected formatView(node: AstNode) {
145
+ this.on(node, ast.isElementView, (n, f) => {
146
+ if (n.extends || n.viewOf || n.name) {
147
+ f.keywords('view').append(FormattingOptions.oneSpace)
148
+ }
149
+ f.keywords('of', 'extends').surround(FormattingOptions.oneSpace)
150
+ })
151
+
152
+ this.on(node, ast.isDynamicView)
153
+ ?.keywords('dynamic', 'view').append(FormattingOptions.oneSpace)
154
+ }
155
+
156
+ protected formatLeafProperty(node: AstNode) {
157
+ if (
158
+ ast.isElementStringProperty(node)
159
+ || ast.isRelationStringProperty(node)
160
+ || ast.isViewStringProperty(node)
161
+ || ast.isNotationProperty(node)
162
+ || ast.isSpecificationElementStringProperty(node)
163
+ || ast.isSpecificationRelationshipStringProperty(node)
164
+ || ast.isColorProperty(node)
165
+ || ast.isLineProperty(node)
166
+ || ast.isArrowProperty(node)
167
+ || ast.isIconProperty(node)
168
+ || ast.isShapeProperty(node)
169
+ || ast.isBorderProperty(node)
170
+ || ast.isOpacityProperty(node)
171
+ ) {
172
+ const formatter = this.getNodeFormatter(node)
173
+ formatter.keywords(
174
+ 'title',
175
+ 'description',
176
+ 'technology',
177
+ 'notation',
178
+ 'color',
179
+ 'line',
180
+ 'head',
181
+ 'tail',
182
+ 'icon',
183
+ 'shape',
184
+ 'border',
185
+ 'opacity'
186
+ )
187
+ .append(FormattingOptions.oneSpace)
188
+
189
+ formatter.keyword(':')
190
+ .prepend(FormattingOptions.noSpace)
191
+ .append(FormattingOptions.oneSpace)
192
+
193
+ formatter.keyword(';')
194
+ .prepend(FormattingOptions.noSpace)
195
+ .append(FormattingOptions.newLine)
196
+ }
197
+ }
198
+
199
+ protected formatLinkProperty(node: AstNode) {
200
+ this.on(node, ast.isLinkProperty, (n, f) => {
201
+ f.keyword('link').append(FormattingOptions.oneSpace)
202
+ f.property('value').append(FormattingOptions.oneSpace)
203
+ f.keyword(':')
204
+ .prepend(FormattingOptions.noSpace)
205
+ .append(FormattingOptions.oneSpace)
206
+
207
+ f.keyword(';')
208
+ .prepend(FormattingOptions.noSpace)
209
+ .append(FormattingOptions.newLine)
210
+ })
211
+ }
212
+
213
+ protected formatNavigateToProperty(node: AstNode) {
214
+ this.on(node, ast.isNavigateToProperty)
215
+ ?.property('key').append(FormattingOptions.oneSpace)
216
+ }
217
+
218
+ protected formatAutolayoutProperty(node: AstNode) {
219
+ this.on(node, ast.isViewRuleAutoLayout)
220
+ ?.keyword('autoLayout').append(FormattingOptions.oneSpace)
221
+ }
222
+
223
+ protected formatMetadataProperty(node: AstNode) {
224
+ this.on(node, ast.isMetadataAttribute, (n, f) => {
225
+ f.property('key').append(FormattingOptions.oneSpace)
226
+ f.keyword(':')
227
+ .prepend(FormattingOptions.noSpace)
228
+ .append(FormattingOptions.oneSpace)
229
+ f.keyword(';')
230
+ .prepend(FormattingOptions.noSpace)
231
+ .append(FormattingOptions.newLine)
232
+ })
233
+ }
234
+
235
+ protected formatElementDeclaration(node: AstNode) {
236
+ this.on(node, ast.isElement, (n, f) => {
237
+ const kind = GrammarUtils.findNodeForProperty(n.$cstNode, 'kind')
238
+ const name = GrammarUtils.findNodeForProperty(n.$cstNode, 'name')
239
+
240
+ if (name && kind) {
241
+ // system sys1
242
+ if (utils.compareRanges(name, kind) > 0) {
243
+ f.cst([kind]).append(FormattingOptions.oneSpace)
244
+ }
245
+ // sys1 = system
246
+ else {
247
+ f.cst([name]).append(FormattingOptions.oneSpace)
248
+ f.cst([kind]).prepend(FormattingOptions.oneSpace)
249
+ }
250
+ }
251
+
252
+ f.properties('props').prepend(FormattingOptions.oneSpace)
253
+ })
254
+ }
255
+
256
+ protected formatSpecificationRule(node: AstNode) {
257
+ if (
258
+ ast.isSpecificationElementKind(node)
259
+ || ast.isSpecificationRelationshipKind(node)
260
+ || ast.isSpecificationTag(node)
261
+ ) {
262
+ const formatter = this.getNodeFormatter(node)
263
+
264
+ formatter.keywords('element', 'relationship', 'tag')
265
+ .append(FormattingOptions.oneSpace)
266
+ }
267
+ if (
268
+ ast.isSpecificationColor(node)
269
+ ) {
270
+ const formatter = this.getNodeFormatter(node)
271
+ formatter.keyword('color').append(FormattingOptions.oneSpace)
272
+ formatter.property('name').append(FormattingOptions.oneSpace)
273
+ }
274
+ }
275
+
276
+ protected formatWithPredicate(node: AstNode) {
277
+ const formatter = this.getNodeFormatter(node)
278
+ if (
279
+ ast.isElementPredicateWith(node)
280
+ || ast.isRelationPredicateWith(node)
281
+ ) {
282
+ formatter.keyword('with').prepend(FormattingOptions.oneSpace)
283
+ }
284
+ }
285
+
286
+ protected formatViewRuleStyle(node: AstNode) {
287
+ this.on(node, ast.isViewRuleStyle)
288
+ ?.keyword('style').append(FormattingOptions.oneSpace)
289
+
290
+ this.on(node, ast.isElementExpressionsIterator)
291
+ ?.keyword(',')
292
+ .prepend(FormattingOptions.noSpace)
293
+ .append(FormattingOptions.oneSpace)
294
+ }
295
+
296
+ protected formatWhereExpression(node: AstNode) {
297
+ if (
298
+ ast.isRelationPredicateOrWhere(node)
299
+ || ast.isElementPredicateOrWhere(node)
300
+ ) {
301
+ const formatter = this.getNodeFormatter(node)
302
+ formatter.keyword('where').append(FormattingOptions.oneSpace)
303
+ }
304
+ if (
305
+ ast.isWhereRelationExpression(node)
306
+ || ast.isWhereElementExpression(node)
307
+ ) {
308
+ const formatter = this.getNodeFormatter(node)
309
+ formatter.property('operator').surround(FormattingOptions.oneSpace)
310
+ }
311
+ if (
312
+ ast.isWhereElementNegation(node)
313
+ || ast.isWhereRelationNegation(node)
314
+ ) {
315
+ const formatter = this.getNodeFormatter(node)
316
+ formatter.keyword('not').append(FormattingOptions.oneSpace)
317
+ }
318
+ if (
319
+ ast.isWhereElement(node)
320
+ || ast.isWhereElementTag(node)
321
+ || ast.isWhereElementKind(node)
322
+ || ast.isWhereRelation(node)
323
+ || ast.isWhereRelationTag(node)
324
+ || ast.isWhereRelationKind(node)
325
+ ) {
326
+ const formatter = this.getNodeFormatter(node)
327
+ formatter.property('operator').surround(FormattingOptions.oneSpace)
328
+ formatter.property('not').surround(FormattingOptions.oneSpace)
329
+ }
330
+ }
331
+
332
+ protected formatIncludeExcludeExpressions(node: AstNode) {
333
+ if (
334
+ ast.isDynamicViewRule(node)
335
+ || ast.isIncludePredicate(node)
336
+ || ast.isExcludePredicate(node)
337
+ ) {
338
+ const formatter = this.getNodeFormatter(node)
339
+
340
+ if (!node.$cstNode || !utils.isMultiline(node.$cstNode)) {
341
+ formatter.keywords('include', 'exclude')
342
+ .append(FormattingOptions.oneSpace)
343
+ }
344
+ }
345
+ if (
346
+ ast.isDynamicViewPredicateIterator(node)
347
+ || ast.isPredicates(node)
348
+ || ast.isPredicates(node)
349
+ ) {
350
+ const formatter = this.getNodeFormatter(node)
351
+ const parent = this.findPredicateExpressionRoot(node)
352
+ const isMultiline = parent?.$cstNode && utils.isMultiline(parent?.$cstNode)
353
+
354
+ if (isMultiline) {
355
+ formatter.property('value').prepend(FormattingOptions.indent)
356
+ }
357
+ formatter.keyword(',')
358
+ .prepend(FormattingOptions.noSpace)
359
+ .append(isMultiline ? FormattingOptions.newLine : FormattingOptions.oneSpace)
360
+ }
361
+ }
362
+
363
+ private findPredicateExpressionRoot(node: AstNode): AstNode | undefined {
364
+ let parent = node.$container
365
+ while (true) {
366
+ if (
367
+ !parent
368
+ || ast.isDynamicViewRule(parent)
369
+ || ast.isIncludePredicate(parent)
370
+ || ast.isExcludePredicate(parent)
371
+ ) {
372
+ return parent
373
+ }
374
+
375
+ parent = parent.$container
376
+ }
377
+ }
378
+
379
+ private on<T extends AstNode>(
380
+ node: AstNode,
381
+ predicate: Predicate<T>,
382
+ format?: (node: T, f: NodeFormatter<T>) => void
383
+ ): NodeFormatter<T> | undefined {
384
+ const formatter = predicate(node) ? this.getNodeFormatter(node) : undefined
385
+
386
+ format && formatter && format(node as T, formatter)
387
+
388
+ return formatter
389
+ }
390
+ }
@@ -0,0 +1,26 @@
1
+ import type { CstNode } from "langium"
2
+ import type { Position, Range } from "vscode-languageserver-types"
3
+
4
+
5
+ export function areOverlap(a: CstNode, b: CstNode): boolean {
6
+ ;[a, b] = compareRanges(a, b) > 0 ? [b, a] : [a, b]
7
+
8
+ return isInRagne(a.range, b.range.start)
9
+ }
10
+
11
+ export function compareRanges(a: CstNode, b: CstNode): number {
12
+ const lineDiff = a.range.start.line - b.range.start.line
13
+
14
+ return lineDiff !== 0 ? lineDiff : a.range.start.character - b.range.start.character
15
+ }
16
+
17
+ export function isInRagne(range: Range, pos: Position): boolean {
18
+ return !(pos.line < range.start.line
19
+ || pos.line > range.end.line
20
+ || pos.line == range.start.line && pos.character < range.start.character
21
+ || pos.line == range.end.line && pos.character > range.end.character)
22
+ }
23
+
24
+ export function isMultiline(node: CstNode): boolean {
25
+ return node.range.start.line != node.range.end.line
26
+ }