@likec4/language-server 1.20.1 → 1.20.2

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 (53) hide show
  1. package/README.md +19 -0
  2. package/bin/likec4-language-server.mjs +5 -0
  3. package/dist/LikeC4FileSystem.js +9 -9
  4. package/dist/Rpc.d.ts +2 -4
  5. package/dist/Rpc.js +27 -36
  6. package/dist/ast.d.ts +1 -0
  7. package/dist/ast.js +5 -1
  8. package/dist/bundled.mjs +5924 -0
  9. package/dist/formatting/LikeC4Formatter.d.ts +9 -0
  10. package/dist/formatting/LikeC4Formatter.js +131 -14
  11. package/dist/generated/ast.d.ts +13 -2
  12. package/dist/generated/ast.js +18 -1
  13. package/dist/generated/grammar.js +1 -1
  14. package/dist/lsp/CompletionProvider.js +11 -3
  15. package/dist/model/deployments-index.d.ts +2 -1
  16. package/dist/model/deployments-index.js +3 -10
  17. package/dist/model/fqn-index.d.ts +2 -1
  18. package/dist/model/fqn-index.js +24 -17
  19. package/dist/model/model-builder.d.ts +2 -1
  20. package/dist/model/model-builder.js +32 -30
  21. package/dist/model/model-parser.d.ts +1 -1
  22. package/dist/model/model-parser.js +9 -6
  23. package/dist/model/parser/PredicatesParser.js +7 -1
  24. package/dist/utils/disposable.d.ts +8 -0
  25. package/dist/utils/disposable.js +25 -0
  26. package/dist/utils/index.d.ts +1 -0
  27. package/dist/utils/index.js +1 -0
  28. package/dist/validation/_shared.js +4 -1
  29. package/dist/validation/index.d.ts +2 -2
  30. package/dist/validation/index.js +4 -1
  31. package/dist/validation/specification.d.ts +1 -0
  32. package/dist/validation/specification.js +30 -0
  33. package/package.json +33 -27
  34. package/src/LikeC4FileSystem.ts +14 -13
  35. package/src/Rpc.ts +28 -38
  36. package/src/ast.ts +6 -1
  37. package/src/formatting/LikeC4Formatter.ts +198 -17
  38. package/src/generated/ast.ts +35 -2
  39. package/src/generated/grammar.ts +1 -1
  40. package/src/like-c4.langium +14 -3
  41. package/src/lsp/CompletionProvider.ts +27 -18
  42. package/src/model/deployments-index.ts +4 -17
  43. package/src/model/fqn-index.ts +26 -19
  44. package/src/model/model-builder.ts +32 -31
  45. package/src/model/model-parser.ts +14 -11
  46. package/src/model/parser/PredicatesParser.ts +30 -24
  47. package/src/utils/disposable.ts +30 -0
  48. package/src/utils/index.ts +1 -0
  49. package/src/validation/_shared.ts +5 -2
  50. package/src/validation/index.ts +6 -2
  51. package/src/validation/specification.ts +34 -0
  52. package/contrib/likec4.tmLanguage.json +0 -73
  53. package/dist/like-c4.langium +0 -852
@@ -1,36 +1,37 @@
1
1
  import { fdir } from 'fdir'
2
2
  import { type FileSystemNode, URI } from 'langium'
3
3
  import { NodeFileSystemProvider } from 'langium/node'
4
- import { stat } from 'node:fs/promises'
4
+ import { LikeC4LanguageMetaData } from './generated/module'
5
5
  import { logger } from './logger'
6
6
 
7
7
  export const LikeC4FileSystem = {
8
8
  fileSystemProvider: () => new SymLinkTraversingFileSystemProvider(),
9
9
  }
10
10
 
11
+ const hasExtension = (path: string) => LikeC4LanguageMetaData.fileExtensions.some((ext) => path.endsWith(ext))
11
12
  /**
12
13
  * A file system provider that follows symbolic links.
13
14
  * @see https://github.com/likec4/likec4/pull/1213
14
15
  */
15
16
  class SymLinkTraversingFileSystemProvider extends NodeFileSystemProvider {
16
17
  override async readDirectory(folderPath: URI): Promise<FileSystemNode[]> {
17
- const crawled = await new fdir()
18
- .withSymlinks()
19
- .withFullPaths()
20
- .crawl(folderPath.fsPath)
21
- .withPromise()
22
18
  const entries = [] as FileSystemNode[]
23
- for (const path of crawled) {
24
- try {
25
- const stats = await stat(path)
19
+ try {
20
+ const crawled = await new fdir()
21
+ .withSymlinks()
22
+ .withFullPaths()
23
+ .filter(hasExtension)
24
+ .crawl(folderPath.fsPath)
25
+ .withPromise()
26
+ for (const path of crawled) {
26
27
  entries.push({
27
- isFile: stats.isFile(),
28
- isDirectory: stats.isDirectory(),
28
+ isFile: true,
29
+ isDirectory: false,
29
30
  uri: URI.file(path),
30
31
  })
31
- } catch (error) {
32
- logger.error(error)
33
32
  }
33
+ } catch (error) {
34
+ logger.error(error)
34
35
  }
35
36
  return entries
36
37
  }
package/src/Rpc.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { debounce } from 'remeda'
1
+ import { debounce, funnel } from 'remeda'
2
2
  import { logError, logger } from './logger'
3
3
  import type { LikeC4Services } from './module'
4
4
 
@@ -16,11 +16,12 @@ import {
16
16
  locate,
17
17
  onDidChangeModel,
18
18
  } from './protocol'
19
+ import { ADisposable } from './utils'
19
20
 
20
- export class Rpc implements Disposable {
21
- private disposables = [] as Array<Disposable>
22
-
23
- constructor(private services: LikeC4Services) {}
21
+ export class Rpc extends ADisposable {
22
+ constructor(private services: LikeC4Services) {
23
+ super()
24
+ }
24
25
 
25
26
  init() {
26
27
  const modelBuilder = this.services.likec4.ModelBuilder
@@ -36,30 +37,28 @@ export class Rpc implements Disposable {
36
37
  const LangiumDocuments = this.services.shared.workspace.LangiumDocuments
37
38
  const DocumentBuilder = this.services.shared.workspace.DocumentBuilder
38
39
 
39
- const notifyModelParsed = debounce(
40
+ const notifyModelParsed = funnel(
40
41
  () => {
41
42
  connection.sendNotification(onDidChangeModel, '').catch(e => {
42
- logger.error(`[ServerRpc] error sending onDidChangeModel: ${e}`)
43
+ logger.warn(`[ServerRpc] error sending onDidChangeModel: ${e}`)
43
44
  return Promise.resolve()
44
45
  })
45
46
  },
46
47
  {
47
- timing: 'trailing',
48
- waitMs: 300,
49
- maxWaitMs: 1000,
48
+ minQuietPeriodMs: 250,
49
+ maxBurstDurationMs: 1000,
50
+ minGapMs: 200,
50
51
  },
51
52
  )
52
53
 
53
54
  let isFirstBuild = true
54
55
 
55
- this.disposables.push(
56
- Disposable.create(() => {
57
- notifyModelParsed.cancel()
58
- }),
56
+ this.onDispose(
59
57
  modelBuilder.onModelParsed(() => notifyModelParsed.call()),
60
58
  connection.onRequest(fetchComputedModel, async ({ cleanCaches }, cancelToken) => {
61
59
  if (cleanCaches) {
62
60
  this.services.WorkspaceCache.clear()
61
+ this.services.DocumentCache.clear()
63
62
  }
64
63
  const model = await modelBuilder.buildComputedModel(cancelToken)
65
64
  return { model }
@@ -105,40 +104,31 @@ export class Rpc implements Disposable {
105
104
  }
106
105
  }),
107
106
  )
108
- await interruptAndCheck(cancelToken)
109
107
  }
110
108
  isFirstBuild = false
109
+ await interruptAndCheck(cancelToken)
111
110
  await DocumentBuilder.update(changed, deleted, cancelToken)
112
111
  }),
113
112
  connection.onRequest(locate, params => {
114
- if ('element' in params) {
115
- return modelLocator.locateElement(params.element, params.property ?? 'name')
116
- }
117
- if ('relation' in params) {
118
- return modelLocator.locateRelation(params.relation)
119
- }
120
- if ('view' in params) {
121
- return modelLocator.locateView(params.view)
113
+ switch (true) {
114
+ case 'element' in params:
115
+ return modelLocator.locateElement(params.element, params.property ?? 'name')
116
+ case 'relation' in params:
117
+ return modelLocator.locateRelation(params.relation)
118
+ case 'view' in params:
119
+ return modelLocator.locateView(params.view)
120
+ case 'deployment' in params:
121
+ return modelLocator.locateDeploymentElement(params.deployment, params.property ?? 'name')
122
+ default:
123
+ nonexhaustive(params)
122
124
  }
123
- if ('deployment' in params) {
124
- return modelLocator.locateDeploymentElement(params.deployment, params.property ?? 'name')
125
- }
126
- nonexhaustive(params)
127
125
  }),
128
126
  connection.onRequest(changeView, async (request, _cancelToken) => {
129
127
  return await modelEditor.applyChange(request)
130
128
  }),
129
+ Disposable.create(() => {
130
+ notifyModelParsed.cancel()
131
+ }),
131
132
  )
132
133
  }
133
-
134
- dispose() {
135
- let item
136
- while (item = this.disposables.pop()) {
137
- try {
138
- item.dispose()
139
- } catch (e) {
140
- logError(e)
141
- }
142
- }
143
- }
144
134
  }
package/src/ast.ts CHANGED
@@ -2,7 +2,7 @@ import type * as c4 from '@likec4/core'
2
2
  import { DefaultArrowType, DefaultLineStyle, DefaultRelationshipColor, LinkedList, nonexhaustive } from '@likec4/core'
3
3
  import type { AstNode, AstNodeDescription, DiagnosticInfo, LangiumDocument, MultiMap } from 'langium'
4
4
  import { DocumentState } from 'langium'
5
- import { clamp, isDefined, isNullish, isTruthy } from 'remeda'
5
+ import { clamp, isBoolean, isDefined, isNullish, isTruthy } from 'remeda'
6
6
  import type { ConditionalPick, SetRequired, ValueOf, Writable } from 'type-fest'
7
7
  import type { Diagnostic } from 'vscode-languageserver-types'
8
8
  import type { LikeC4Grammar } from './generated/ast'
@@ -42,6 +42,7 @@ type ParsedElementStyle = {
42
42
  color?: c4.Color
43
43
  border?: c4.BorderStyle
44
44
  opacity?: number
45
+ multiple?: boolean
45
46
  }
46
47
 
47
48
  export interface ParsedAstSpecification {
@@ -363,6 +364,10 @@ export function toElementStyle(props: Array<ast.StyleProperty> | undefined, isVa
363
364
  result.opacity = parseAstOpacityProperty(prop)
364
365
  break
365
366
  }
367
+ case ast.isMultipleProperty(prop): {
368
+ result.multiple = isBoolean(prop.value) ? prop.value : false
369
+ break
370
+ }
366
371
  default:
367
372
  nonexhaustive(prop)
368
373
  }
@@ -1,5 +1,5 @@
1
- import { type AstNode, GrammarUtils } from 'langium'
2
- import { AbstractFormatter, Formatting, type NodeFormatter } from 'langium/lsp'
1
+ import { type AstNode, type CompositeCstNode, GrammarUtils } from 'langium'
2
+ import { type NodeFormatter, AbstractFormatter, Formatting } from 'langium/lsp'
3
3
  import { filter, isTruthy } from 'remeda'
4
4
  import * as ast from '../generated/ast'
5
5
  import * as utils from './utils'
@@ -9,7 +9,7 @@ const FormattingOptions = {
9
9
  oneSpace: Formatting.oneSpace(),
10
10
  noSpace: Formatting.noSpace(),
11
11
  indent: Formatting.indent({ allowMore: true }),
12
- noIndent: Formatting.noIndent()
12
+ noIndent: Formatting.noIndent(),
13
13
  }
14
14
  type Predicate<T extends AstNode> = (x: unknown) => x is T
15
15
 
@@ -29,12 +29,23 @@ export class LikeC4Formatter extends AbstractFormatter {
29
29
  this.formatRelation(node)
30
30
  this.formatMetadataProperty(node)
31
31
 
32
+ // Deployment
33
+ this.formatDeploymentNodeDeclaration(node)
34
+ this.formatDeployedInstance(node)
35
+ this.formatDeploymentRelation(node)
36
+
32
37
  // Views
33
38
  this.formatView(node)
34
39
  this.formatViewRuleGroup(node)
35
40
  this.formatViewRuleGlobalStyle(node)
41
+ this.formatViewRuleGlobalPredicate(node)
36
42
  this.formatIncludeExcludeExpressions(node)
43
+ this.formatDeploymentViewRulePredicateExpressions(node)
37
44
  this.formatWhereExpression(node)
45
+ this.formatWhereExpressionV2(node)
46
+ this.formatWhereRelationExpression(node)
47
+ this.formatWhereElementExpression(node)
48
+ this.formatRelationExpression(node)
38
49
  this.formatAutolayoutProperty(node)
39
50
  this.formatWithPredicate(node)
40
51
 
@@ -57,6 +68,23 @@ export class LikeC4Formatter extends AbstractFormatter {
57
68
  })
58
69
  }
59
70
 
71
+ protected formatDeploymentRelation(node: AstNode) {
72
+ this.on(node, ast.isDeploymentRelation, (n, f) => {
73
+ const sourceNodes = n?.source?.$cstNode ? [n?.source?.$cstNode] : []
74
+
75
+ f.cst(sourceNodes).append(FormattingOptions.oneSpace)
76
+
77
+ f.keywords(']->').prepend(FormattingOptions.noSpace)
78
+ f.keywords('-[').append(FormattingOptions.noSpace)
79
+
80
+ f.nodes(...filter([
81
+ n.target,
82
+ n.tags,
83
+ ], isTruthy)).prepend(FormattingOptions.oneSpace)
84
+ f.properties('title', 'technology').prepend(FormattingOptions.oneSpace)
85
+ })
86
+ }
87
+
60
88
  protected formatRelation(node: AstNode) {
61
89
  this.on(node, ast.isRelation, (n, f) => {
62
90
  const sourceNodes = n?.source?.$cstNode ? [n?.source?.$cstNode] : []
@@ -67,7 +95,7 @@ export class LikeC4Formatter extends AbstractFormatter {
67
95
 
68
96
  f.nodes(...filter([
69
97
  n.target,
70
- n.tags
98
+ n.tags,
71
99
  ], isTruthy)).prepend(FormattingOptions.oneSpace)
72
100
  f.properties('title', 'technology').prepend(FormattingOptions.oneSpace)
73
101
  })
@@ -110,9 +138,10 @@ export class LikeC4Formatter extends AbstractFormatter {
110
138
  || ast.isModelViews(node)
111
139
  || ast.isLikeC4Lib(node)
112
140
  || ast.isGlobals(node)
141
+ || ast.isModelDeployments(node)
113
142
  ) {
114
143
  const formatter = this.getNodeFormatter(node)
115
- formatter.keywords('specification', 'model', 'views', 'likec4lib', 'global')
144
+ formatter.keywords('specification', 'model', 'views', 'likec4lib', 'global', 'deployments')
116
145
  .prepend(FormattingOptions.noIndent)
117
146
  }
118
147
  }
@@ -123,9 +152,13 @@ export class LikeC4Formatter extends AbstractFormatter {
123
152
  || ast.isSpecificationRule(node)
124
153
  || ast.isSpecificationElementKind(node)
125
154
  || ast.isSpecificationRelationshipKind(node)
155
+ || ast.isSpecificationDeploymentNodeKind(node)
126
156
  || ast.isGlobals(node)
127
157
  || ast.isGlobalStyle(node)
128
158
  || ast.isGlobalStyleGroup(node)
159
+ || ast.isGlobalPredicateGroup(node)
160
+ || ast.isGlobalDynamicPredicateGroup(node)
161
+ || ast.isGlobalStyleGroup(node)
129
162
  || ast.isModel(node)
130
163
  || ast.isElementBody(node)
131
164
  || ast.isExtendElementBody(node)
@@ -135,12 +168,17 @@ export class LikeC4Formatter extends AbstractFormatter {
135
168
  || ast.isModelViews(node)
136
169
  || ast.isElementViewBody(node)
137
170
  || ast.isDynamicViewBody(node)
171
+ || ast.isDeploymentViewBody(node)
138
172
  || ast.isViewRuleStyle(node)
139
173
  || ast.isViewRuleGroup(node)
140
174
  || ast.isCustomElementProperties(node)
141
175
  || ast.isCustomRelationProperties(node)
142
176
  || ast.isElementStyleProperty(node)
143
177
  || ast.isDynamicViewParallelSteps(node)
178
+ || ast.isModelDeployments(node)
179
+ || ast.isDeploymentNodeBody(node)
180
+ || ast.isDeploymentRelationBody(node)
181
+ || ast.isDeployedInstanceBody(node)
144
182
  ) {
145
183
  const formatter = this.getNodeFormatter(node)
146
184
  const openBrace = formatter.keywords('{')
@@ -182,6 +220,9 @@ export class LikeC4Formatter extends AbstractFormatter {
182
220
 
183
221
  this.on(node, ast.isDynamicView)
184
222
  ?.keywords('dynamic', 'view').append(FormattingOptions.oneSpace)
223
+
224
+ this.on(node, ast.isDeploymentView)
225
+ ?.keywords('deployment', 'view').append(FormattingOptions.oneSpace)
185
226
  }
186
227
 
187
228
  protected formatLeafProperty(node: AstNode) {
@@ -199,6 +240,7 @@ export class LikeC4Formatter extends AbstractFormatter {
199
240
  || ast.isShapeProperty(node)
200
241
  || ast.isBorderProperty(node)
201
242
  || ast.isOpacityProperty(node)
243
+ || ast.isMultipleProperty(node)
202
244
  ) {
203
245
  const formatter = this.getNodeFormatter(node)
204
246
  const colon = formatter.keyword(':')
@@ -214,7 +256,8 @@ export class LikeC4Formatter extends AbstractFormatter {
214
256
  'icon',
215
257
  'shape',
216
258
  'border',
217
- 'opacity'
259
+ 'opacity',
260
+ 'multiple',
218
261
  )
219
262
 
220
263
  if (colon.nodes.length === 0) {
@@ -301,6 +344,14 @@ export class LikeC4Formatter extends AbstractFormatter {
301
344
  this.on(node, ast.isGlobalStyleGroup, (n, f) => {
302
345
  f.keyword('styleGroup').append(FormattingOptions.oneSpace)
303
346
  })
347
+
348
+ this.on(node, ast.isGlobalPredicateGroup, (n, f) => {
349
+ f.keyword('predicateGroup').append(FormattingOptions.oneSpace)
350
+ })
351
+
352
+ this.on(node, ast.isGlobalDynamicPredicateGroup, (n, f) => {
353
+ f.keyword('dynamicPredicateGroup').append(FormattingOptions.oneSpace)
354
+ })
304
355
  }
305
356
 
306
357
  protected formatSpecificationRule(node: AstNode) {
@@ -308,10 +359,11 @@ export class LikeC4Formatter extends AbstractFormatter {
308
359
  ast.isSpecificationElementKind(node)
309
360
  || ast.isSpecificationRelationshipKind(node)
310
361
  || ast.isSpecificationTag(node)
362
+ || ast.isSpecificationDeploymentNodeKind(node)
311
363
  ) {
312
364
  const formatter = this.getNodeFormatter(node)
313
365
 
314
- formatter.keywords('element', 'relationship', 'tag')
366
+ formatter.keywords('element', 'relationship', 'tag', 'deploymentNode')
315
367
  .append(FormattingOptions.oneSpace)
316
368
  }
317
369
  if (
@@ -331,6 +383,39 @@ export class LikeC4Formatter extends AbstractFormatter {
331
383
  ) {
332
384
  formatter.keyword('with').prepend(FormattingOptions.oneSpace)
333
385
  }
386
+ }
387
+
388
+ protected formatDeploymentNodeDeclaration(node: AstNode) {
389
+ this.on(node, ast.isDeploymentNode, (n, f) => {
390
+ const kind = GrammarUtils.findNodeForProperty(n.$cstNode, 'kind')
391
+ const name = GrammarUtils.findNodeForProperty(n.$cstNode, 'name')
392
+
393
+ if (name && kind) {
394
+ // system sys1
395
+ if (utils.compareRanges(name, kind) > 0) {
396
+ f.cst([kind]).append(FormattingOptions.oneSpace)
397
+ }
398
+ // sys1 = system
399
+ else {
400
+ f.cst([name]).append(FormattingOptions.oneSpace)
401
+ f.cst([kind]).prepend(FormattingOptions.oneSpace)
402
+ }
403
+ }
404
+
405
+ f.properties('title').prepend(FormattingOptions.oneSpace)
406
+ })
407
+ }
408
+
409
+ protected formatDeployedInstance(node: AstNode) {
410
+ this.on(node, ast.isDeployedInstance, (n, f) => {
411
+ const eqNode = (<CompositeCstNode>n.$cstNode)?.content.find(c => c.text === '=')
412
+ if (eqNode) {
413
+ f.cst([eqNode]).surround(FormattingOptions.oneSpace)
414
+ }
415
+
416
+ f.keyword('instanceOf').append(FormattingOptions.oneSpace)
417
+ f.property('title').prepend(FormattingOptions.oneSpace)
418
+ })
334
419
  }
335
420
 
336
421
  protected formatViewRuleGlobalStyle(node: AstNode) {
@@ -339,6 +424,16 @@ export class LikeC4Formatter extends AbstractFormatter {
339
424
  })
340
425
  }
341
426
 
427
+ protected formatViewRuleGlobalPredicate(node: AstNode) {
428
+ const formatter = this.getNodeFormatter(node)
429
+ if (
430
+ ast.isViewRuleGlobalPredicateRef(node)
431
+ || ast.isDynamicViewGlobalPredicateRef(node)
432
+ ) {
433
+ formatter.keywords('global', 'predicate').append(FormattingOptions.oneSpace)
434
+ }
435
+ }
436
+
342
437
  protected formatViewRuleGroup(node: AstNode) {
343
438
  this.on(node, ast.isViewRuleGroup, (n, f) => {
344
439
  f.keyword('group').append(FormattingOptions.oneSpace)
@@ -349,10 +444,18 @@ export class LikeC4Formatter extends AbstractFormatter {
349
444
  this.on(node, ast.isViewRuleStyle)
350
445
  ?.keyword('style').append(FormattingOptions.oneSpace)
351
446
 
447
+ this.on(node, ast.isDeploymentViewRuleStyle)
448
+ ?.keyword('style').append(FormattingOptions.oneSpace)
449
+
352
450
  this.on(node, ast.isElementExpressionsIterator)
353
451
  ?.keyword(',')
354
452
  .prepend(FormattingOptions.noSpace)
355
453
  .append(FormattingOptions.oneSpace)
454
+
455
+ this.on(node, ast.isFqnExpressions)
456
+ ?.keyword(',')
457
+ .prepend(FormattingOptions.noSpace)
458
+ .append(FormattingOptions.oneSpace)
356
459
  }
357
460
 
358
461
  protected formatWhereExpression(node: AstNode) {
@@ -363,27 +466,47 @@ export class LikeC4Formatter extends AbstractFormatter {
363
466
  const formatter = this.getNodeFormatter(node)
364
467
  formatter.keyword('where').append(FormattingOptions.oneSpace)
365
468
  }
469
+ }
470
+
471
+ protected formatWhereExpressionV2(node: AstNode) {
366
472
  if (
367
- ast.isWhereRelationExpression(node)
368
- || ast.isWhereElementExpression(node)
473
+ ast.isRelationPredicateOrWhereV2(node)
474
+ // || ast.isElementPredicateOrWhere(node)
369
475
  ) {
370
476
  const formatter = this.getNodeFormatter(node)
371
- formatter.property('operator').surround(FormattingOptions.oneSpace)
477
+ formatter.keyword('where').append(FormattingOptions.oneSpace)
372
478
  }
479
+ }
480
+
481
+ protected formatWhereRelationExpression(node: AstNode) {
482
+ this.on(node, ast.isWhereRelationExpression, (n, f) => {
483
+ f.property('operator').surround(FormattingOptions.oneSpace)
484
+ })
485
+ this.on(node, ast.isWhereRelationNegation, (n, f) => {
486
+ f.keyword('not').append(FormattingOptions.oneSpace)
487
+ })
373
488
  if (
374
- ast.isWhereElementNegation(node)
375
- || ast.isWhereRelationNegation(node)
489
+ ast.isWhereRelation(node)
490
+ || ast.isWhereRelationTag(node)
491
+ || ast.isWhereRelationKind(node)
376
492
  ) {
377
493
  const formatter = this.getNodeFormatter(node)
378
- formatter.keyword('not').append(FormattingOptions.oneSpace)
494
+ formatter.property('operator').surround(FormattingOptions.oneSpace)
495
+ formatter.property('not').surround(FormattingOptions.oneSpace)
379
496
  }
497
+ }
498
+
499
+ protected formatWhereElementExpression(node: AstNode) {
500
+ this.on(node, ast.isWhereElementExpression, (n, f) => {
501
+ f.property('operator').surround(FormattingOptions.oneSpace)
502
+ })
503
+ this.on(node, ast.isWhereElementNegation, (n, f) => {
504
+ f.keyword('not').append(FormattingOptions.oneSpace)
505
+ })
380
506
  if (
381
507
  ast.isWhereElement(node)
382
508
  || ast.isWhereElementTag(node)
383
509
  || ast.isWhereElementKind(node)
384
- || ast.isWhereRelation(node)
385
- || ast.isWhereRelationTag(node)
386
- || ast.isWhereRelationKind(node)
387
510
  ) {
388
511
  const formatter = this.getNodeFormatter(node)
389
512
  formatter.property('operator').surround(FormattingOptions.oneSpace)
@@ -396,6 +519,7 @@ export class LikeC4Formatter extends AbstractFormatter {
396
519
  ast.isDynamicViewRule(node)
397
520
  || ast.isIncludePredicate(node)
398
521
  || ast.isExcludePredicate(node)
522
+ || ast.isDeploymentViewRulePredicate(node)
399
523
  ) {
400
524
  const formatter = this.getNodeFormatter(node)
401
525
 
@@ -404,6 +528,7 @@ export class LikeC4Formatter extends AbstractFormatter {
404
528
  .append(FormattingOptions.oneSpace)
405
529
  }
406
530
  }
531
+
407
532
  if (
408
533
  ast.isDynamicViewPredicateIterator(node)
409
534
  || ast.isPredicates(node)
@@ -422,6 +547,61 @@ export class LikeC4Formatter extends AbstractFormatter {
422
547
  }
423
548
  }
424
549
 
550
+ protected formatRelationExpression(node: AstNode) {
551
+ this.on(node, ast.isIncomingRelationExpr, (n, f) => {
552
+ f.keyword('->').append(FormattingOptions.oneSpace)
553
+ })
554
+ this.on(node, ast.isInOutRelationExpr, (n, f) => {
555
+ f.keyword('->').prepend(FormattingOptions.oneSpace)
556
+ })
557
+ this.on(node, ast.isOutgoingRelationExpr, (n, f) => {
558
+ f.keywords('->', '<->').prepend(FormattingOptions.oneSpace)
559
+ f.keywords('-[')
560
+ .prepend(FormattingOptions.oneSpace)
561
+ .append(FormattingOptions.noSpace)
562
+ f.keywords(']->')
563
+ .prepend(FormattingOptions.noSpace)
564
+ .append(FormattingOptions.oneSpace)
565
+
566
+ const kind = f.property('kind')
567
+ kind.nodes[0]?.text.startsWith('.') && kind.surround(FormattingOptions.oneSpace)
568
+ })
569
+ this.on(node, ast.isDirectedRelationExpr, (n, f) => {
570
+ f.property('target').prepend(FormattingOptions.oneSpace)
571
+ })
572
+ }
573
+
574
+ protected formatDeploymentViewRulePredicateExpressions(node: AstNode) {
575
+ if (
576
+ ast.isDynamicViewRule(node)
577
+ || ast.isIncludePredicate(node)
578
+ || ast.isExcludePredicate(node)
579
+ || ast.isDeploymentViewRulePredicate(node)
580
+ ) {
581
+ const formatter = this.getNodeFormatter(node)
582
+
583
+ if (!node.$cstNode || !utils.isMultiline(node.$cstNode)) {
584
+ formatter.keywords('include', 'exclude')
585
+ .append(FormattingOptions.oneSpace)
586
+ }
587
+ }
588
+
589
+ if (
590
+ ast.isDeploymentViewRulePredicateExpression(node)
591
+ ) {
592
+ const formatter = this.getNodeFormatter(node)
593
+ const parent = this.findPredicateExpressionRoot(node)
594
+ const isMultiline = parent?.$cstNode && utils.isMultiline(parent?.$cstNode)
595
+
596
+ if (isMultiline) {
597
+ formatter.property('value').prepend(FormattingOptions.indent)
598
+ }
599
+ formatter.keyword(',')
600
+ .prepend(FormattingOptions.noSpace)
601
+ .append(isMultiline ? FormattingOptions.newLine : FormattingOptions.oneSpace)
602
+ }
603
+ }
604
+
425
605
  private findPredicateExpressionRoot(node: AstNode): AstNode | undefined {
426
606
  let parent = node.$container
427
607
  while (true) {
@@ -430,6 +610,7 @@ export class LikeC4Formatter extends AbstractFormatter {
430
610
  || ast.isDynamicViewRule(parent)
431
611
  || ast.isIncludePredicate(parent)
432
612
  || ast.isExcludePredicate(parent)
613
+ || ast.isDeploymentViewRulePredicate(parent)
433
614
  ) {
434
615
  return parent
435
616
  }
@@ -441,7 +622,7 @@ export class LikeC4Formatter extends AbstractFormatter {
441
622
  private on<T extends AstNode>(
442
623
  node: AstNode,
443
624
  predicate: Predicate<T>,
444
- format?: (node: T, f: NodeFormatter<T>) => void
625
+ format?: (node: T, f: NodeFormatter<T>) => void,
445
626
  ): NodeFormatter<T> | undefined {
446
627
  const formatter = predicate(node) ? this.getNodeFormatter(node) : undefined
447
628