@likec4/language-server 1.15.1 → 1.17.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 (49) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +1 -1
  3. package/dist/browser.d.cts +2 -2
  4. package/dist/browser.d.mts +2 -2
  5. package/dist/browser.d.ts +2 -2
  6. package/dist/browser.mjs +2 -2
  7. package/dist/index.cjs +35 -5
  8. package/dist/index.d.cts +17 -4
  9. package/dist/index.d.mts +17 -4
  10. package/dist/index.d.ts +17 -4
  11. package/dist/index.mjs +34 -5
  12. package/dist/model-graph/index.cjs +1 -1
  13. package/dist/model-graph/index.d.cts +4 -2
  14. package/dist/model-graph/index.d.mts +4 -2
  15. package/dist/model-graph/index.d.ts +4 -2
  16. package/dist/model-graph/index.mjs +1 -1
  17. package/dist/shared/{language-server.BuChFlda.mjs → language-server.B8qSDsWW.mjs} +211 -96
  18. package/dist/shared/language-server.BGGRJRnr.d.mts +1338 -0
  19. package/dist/shared/{language-server.BT4WTbFI.mjs → language-server.BXFhlTPo.mjs} +139 -76
  20. package/dist/shared/{language-server.DfMwkd2l.d.mts → language-server.BgDKnNok.d.mts} +45 -11
  21. package/dist/shared/language-server.Bmpq16Gw.d.ts +1338 -0
  22. package/dist/shared/language-server.C1ZfM22X.d.cts +1338 -0
  23. package/dist/shared/{language-server.U2piOAVt.d.cts → language-server.DJo88TnT.d.cts} +45 -11
  24. package/dist/shared/{language-server.DfjkvknB.cjs → language-server.DZRuJVSg.cjs} +209 -94
  25. package/dist/shared/{language-server.B8s2wfT_.cjs → language-server.N8HLDQqz.cjs} +137 -74
  26. package/dist/shared/{language-server.j-ShR6as.d.ts → language-server.PEjk7U9s.d.ts} +45 -11
  27. package/package.json +7 -7
  28. package/src/LikeC4FileSystem.ts +36 -0
  29. package/src/Rpc.ts +2 -2
  30. package/src/ast.ts +11 -3
  31. package/src/generated/ast.ts +112 -11
  32. package/src/generated/grammar.ts +1 -1
  33. package/src/index.ts +3 -3
  34. package/src/like-c4.langium +20 -1
  35. package/src/lsp/SemanticTokenProvider.ts +26 -8
  36. package/src/model/fqn-computation.ts +6 -2
  37. package/src/model/model-builder.ts +25 -30
  38. package/src/model/model-parser.ts +91 -18
  39. package/src/model-graph/LikeC4ModelGraph.ts +22 -11
  40. package/src/model-graph/compute-view/__test__/fixture.ts +69 -19
  41. package/src/model-graph/compute-view/compute.ts +85 -73
  42. package/src/model-graph/dynamic-view/compute.ts +12 -2
  43. package/src/model-graph/utils/applyCustomElementProperties.ts +1 -3
  44. package/src/model-graph/utils/applyViewRuleStyles.ts +0 -4
  45. package/src/references/scope-computation.ts +10 -0
  46. package/src/validation/index.ts +3 -0
  47. package/src/validation/specification.ts +21 -0
  48. package/src/view-utils/index.ts +0 -1
  49. package/src/view-utils/resolve-global-rules.ts +66 -50
package/src/index.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  import { startLanguageServer as startLanguim } from 'langium/lsp'
2
- import { NodeFileSystem } from 'langium/node'
3
2
  import { createConnection, ProposedFeatures } from 'vscode-languageserver/node'
3
+ import { LikeC4FileSystem } from './LikeC4FileSystem'
4
4
  import { createLanguageServices } from './module'
5
5
 
6
6
  export { logger as lspLogger, setLogLevel } from './logger'
7
7
  export type * from './model'
8
8
  export type * from './module'
9
9
  export { createCustomLanguageServices, createLanguageServices, LikeC4Module } from './module'
10
-
10
+ export { LikeC4FileSystem }
11
11
  export function startLanguageServer() {
12
12
  /* browser specific setup code */
13
13
  const connection = createConnection(ProposedFeatures.all)
14
14
 
15
15
  // Inject the shared services and language-specific services
16
- const services = createLanguageServices({ connection, ...NodeFileSystem })
16
+ const services = createLanguageServices({ connection, ...LikeC4FileSystem })
17
17
 
18
18
  // Start the language server with the shared services
19
19
  startLanguim(services.shared)
@@ -267,6 +267,7 @@ ViewLayoutDirection returns string:
267
267
 
268
268
  ViewRule:
269
269
  ViewRulePredicate |
270
+ ViewRuleGlobalPredicateRef |
270
271
  ViewRuleGroup |
271
272
  ViewRuleStyleOrGlobalRef |
272
273
  ViewRuleAutoLayout
@@ -287,6 +288,7 @@ ViewRuleGroup:
287
288
 
288
289
  DynamicViewRule:
289
290
  DynamicViewIncludePredicate |
291
+ DynamicViewGlobalPredicateRef |
290
292
  ViewRuleStyleOrGlobalRef |
291
293
  ViewRuleAutoLayout
292
294
  ;
@@ -318,6 +320,9 @@ Predicate:
318
320
  ElementPredicate
319
321
  ;
320
322
 
323
+ ViewRuleGlobalPredicateRef:
324
+ 'global' 'predicate' predicate=[GlobalPredicateGroup];
325
+
321
326
  ElementPredicate:
322
327
  ElementPredicates;
323
328
 
@@ -443,6 +448,9 @@ DynamicViewIncludePredicate:
443
448
  'include' predicates=DynamicViewPredicateIterator
444
449
  ;
445
450
 
451
+ DynamicViewGlobalPredicateRef:
452
+ 'global' 'predicate' predicate=[GlobalDynamicPredicateGroup];
453
+
446
454
  DynamicViewPredicateIterator:
447
455
  value=ElementPredicate ({infer DynamicViewPredicateIterator.prev=current} ',' (value=ElementPredicate)?)*
448
456
  ;
@@ -506,10 +514,21 @@ RelationNavigateToProperty:
506
514
  Globals:
507
515
  name='global' '{'
508
516
  (
517
+ predicates+=(GlobalPredicateGroup | GlobalDynamicPredicateGroup)*
509
518
  styles+=(GlobalStyle | GlobalStyleGroup)*
510
519
  )*
511
520
  '}';
512
521
 
522
+ GlobalPredicateGroup:
523
+ 'predicateGroup' name=IdTerminal '{'
524
+ predicates+=ViewRulePredicate*
525
+ '}';
526
+
527
+ GlobalDynamicPredicateGroup:
528
+ 'dynamicPredicateGroup' name=IdTerminal '{'
529
+ predicates+=DynamicViewIncludePredicate*
530
+ '}';
531
+
513
532
  GlobalStyleId:
514
533
  name=IdTerminal;
515
534
 
@@ -538,7 +557,7 @@ OpacityProperty:
538
557
 
539
558
  // Element properties -------------------------------------
540
559
  IconProperty:
541
- key='icon' ':'? (libicon=[LibIcon:IconId] | value=Uri) ';'?;
560
+ key='icon' ':'? (libicon=[LibIcon:IconId] | value=('none'|Uri)) ';'?;
542
561
 
543
562
  ShapeProperty:
544
563
  key='shape' ':'? value=ElementShape ';'?;
@@ -196,6 +196,32 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
196
196
  property: 'key',
197
197
  type: SemanticTokenTypes.property
198
198
  })
199
+ if (ast.isIconProperty(node)) {
200
+ if (node.libicon) {
201
+ acceptor({
202
+ node,
203
+ property: 'libicon',
204
+ type: SemanticTokenTypes.enum,
205
+ modifier: [SemanticTokenModifiers.defaultLibrary]
206
+ })
207
+ } else {
208
+ if (node.value === 'none') {
209
+ acceptor({
210
+ node,
211
+ property: 'value',
212
+ type: SemanticTokenTypes.enum,
213
+ modifier: [SemanticTokenModifiers.defaultLibrary]
214
+ })
215
+ } else {
216
+ acceptor({
217
+ node,
218
+ property: 'value',
219
+ type: SemanticTokenTypes.string
220
+ })
221
+ }
222
+ }
223
+ return 'prune'
224
+ }
199
225
  if ('value' in node && node.value) {
200
226
  acceptor({
201
227
  node,
@@ -203,14 +229,6 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
203
229
  type: SemanticTokenTypes.string
204
230
  })
205
231
  }
206
- if (ast.isIconProperty(node) && node.libicon) {
207
- acceptor({
208
- node,
209
- property: 'libicon',
210
- type: SemanticTokenTypes.enum,
211
- modifier: [SemanticTokenModifiers.defaultLibrary]
212
- })
213
- }
214
232
  return 'prune'
215
233
  }
216
234
  if (
@@ -52,7 +52,9 @@ export function computeDocumentFqn(document: LikeC4LangiumDocument, services: Li
52
52
  if (isDefined(el.body) && !isEmpty(el.body.elements)) {
53
53
  const fqn = getFqnElementRef(el.element)
54
54
  for (const child of el.body.elements) {
55
- traverseStack.push([child, fqn])
55
+ if (!ast.isRelation(child)) {
56
+ traverseStack.push([child, fqn])
57
+ }
56
58
  }
57
59
  }
58
60
  continue
@@ -66,7 +68,9 @@ export function computeDocumentFqn(document: LikeC4LangiumDocument, services: Li
66
68
  ElementOps.writeId(el, fqn)
67
69
  if (isDefined(el.body) && !isEmpty(el.body.elements)) {
68
70
  for (const child of el.body.elements) {
69
- traverseStack.push([child, fqn])
71
+ if (!ast.isRelation(child)) {
72
+ traverseStack.push([child, fqn])
73
+ }
70
74
  }
71
75
  }
72
76
  continue
@@ -13,10 +13,8 @@ import { deepEqual as eq } from 'fast-equals'
13
13
  import type { Cancellation, LangiumDocument, LangiumDocuments, URI, WorkspaceCache } from 'langium'
14
14
  import { Disposable, DocumentState, interruptAndCheck } from 'langium'
15
15
  import {
16
- entries,
17
16
  filter,
18
17
  flatMap,
19
- forEach,
20
18
  groupBy,
21
19
  indexBy,
22
20
  isEmpty,
@@ -46,21 +44,37 @@ import { isParsedLikeC4LangiumDocument } from '../ast'
46
44
  import { logError, logger, logWarnError } from '../logger'
47
45
  import { computeDynamicView, computeView, LikeC4ModelGraph } from '../model-graph'
48
46
  import type { LikeC4Services } from '../module'
49
- import { assignNavigateTo, resolveGlobalRules, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
47
+ import { assignNavigateTo, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
50
48
 
51
49
  function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4Model {
50
+ // Merge specifications and globals from all documents
52
51
  const c4Specification: ParsedAstSpecification = {
53
52
  tags: new Set(),
54
53
  elements: {},
55
54
  relationships: {},
56
55
  colors: {}
57
56
  }
58
- forEach(map(docs, prop('c4Specification')), spec => {
57
+ const globals: c4.ModelGlobals = {
58
+ predicates: {},
59
+ dynamicPredicates: {},
60
+ styles: {}
61
+ }
62
+ for (const doc of docs) {
63
+ const {
64
+ c4Specification: spec,
65
+ c4Globals
66
+ } = doc
67
+
59
68
  spec.tags.forEach(t => c4Specification.tags.add(t))
60
69
  Object.assign(c4Specification.elements, spec.elements)
61
70
  Object.assign(c4Specification.relationships, spec.relationships)
62
71
  Object.assign(c4Specification.colors, spec.colors)
63
- })
72
+
73
+ Object.assign(globals.predicates, c4Globals.predicates)
74
+ Object.assign(globals.dynamicPredicates, c4Globals.dynamicPredicates)
75
+ Object.assign(globals.styles, c4Globals.styles)
76
+ }
77
+
64
78
  function resolveLinks(doc: LangiumDocument, links: c4.NonEmptyArray<ParsedLink>) {
65
79
  return map(
66
80
  links,
@@ -211,23 +225,6 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
211
225
  indexBy(prop('id'))
212
226
  )
213
227
 
214
- const parsedGlobals: {
215
- styles: Record<c4.GlobalStyleID, c4.ViewRuleStyle[]>
216
- } = {
217
- styles: {}
218
- }
219
- Object.assign(parsedGlobals.styles, ...docs.map(d => d.c4Globals.styles))
220
-
221
- const globals: {
222
- styles: Record<c4.GlobalStyleID, c4.GlobalStyle>
223
- } = {
224
- styles: pipe(
225
- entries(parsedGlobals.styles),
226
- map(([id, styles]) => ({ id, styles })),
227
- indexBy(prop('id'))
228
- )
229
- }
230
-
231
228
  function toC4View(doc: LangiumDocument) {
232
229
  const docUri = doc.uri.toString()
233
230
  return (parsedAstView: ParsedAstView): c4.LikeC4View => {
@@ -399,10 +396,9 @@ export class LikeC4ModelBuilder {
399
396
 
400
397
  const allViews = [] as c4.ComputedView[]
401
398
  for (const view of values(model.views)) {
402
- const resolvedView = resolveGlobalRules(view, model.globals.styles)
403
- const result = isElementView(resolvedView)
404
- ? computeView(resolvedView, index)
405
- : computeDynamicView(resolvedView, index)
399
+ const result = isElementView(view)
400
+ ? computeView(view, index)
401
+ : computeDynamicView(view, index)
406
402
  if (!result.isSuccess) {
407
403
  logWarnError(result.error)
408
404
  continue
@@ -467,10 +463,9 @@ export class LikeC4ModelBuilder {
467
463
  return null
468
464
  }
469
465
  const index = new LikeC4ModelGraph(model)
470
- const resolvedView = resolveGlobalRules(view, model.globals.styles)
471
- const result = isElementView(resolvedView)
472
- ? computeView(resolvedView, index)
473
- : computeDynamicView(resolvedView, index)
466
+ const result = isElementView(view)
467
+ ? computeView(view, index)
468
+ : computeDynamicView(view, index)
474
469
  if (!result.isSuccess) {
475
470
  logError(result.error)
476
471
  return null
@@ -11,6 +11,7 @@ import type {
11
11
  ParsedAstDynamicView,
12
12
  ParsedAstElement,
13
13
  ParsedAstElementView,
14
+ ParsedAstGlobals,
14
15
  ParsedAstRelation,
15
16
  ParsedLikeC4LangiumDocument,
16
17
  ParsedLink
@@ -288,6 +289,25 @@ export class LikeC4ModelParser {
288
289
  const { parseResult, c4Globals } = doc
289
290
 
290
291
  const globals = parseResult.value.globals.filter(isValid)
292
+
293
+ const elRelPredicates = globals.flatMap(r => r.predicates.filter(isValid))
294
+ for (const predicate of elRelPredicates) {
295
+ try {
296
+ const globalPredicateId = predicate.name as c4.GlobalPredicateId
297
+ if (!isTruthy(globalPredicateId)) {
298
+ continue
299
+ }
300
+ if (globalPredicateId in c4Globals.predicates) {
301
+ logger.warn(`Global predicate named "${globalPredicateId}" is already defined`)
302
+ continue
303
+ }
304
+
305
+ this.parseAndStoreGlobalPredicateGroupOrDynamic(predicate, globalPredicateId, c4Globals, isValid)
306
+ } catch (e) {
307
+ logWarnError(e)
308
+ }
309
+ }
310
+
291
311
  const styles = globals.flatMap(r => r.styles.filter(isValid))
292
312
  for (const style of styles) {
293
313
  try {
@@ -310,12 +330,49 @@ export class LikeC4ModelParser {
310
330
  }
311
331
  }
312
332
 
333
+ private parseAndStoreGlobalPredicateGroupOrDynamic(
334
+ astRule: ast.GlobalPredicateGroup | ast.GlobalDynamicPredicateGroup,
335
+ id: c4.GlobalPredicateId,
336
+ c4Globals: ParsedAstGlobals,
337
+ isValid: IsValidFn
338
+ ) {
339
+ if (ast.isGlobalPredicateGroup(astRule)) {
340
+ const predicates = this.parseGlobalPredicateGroup(astRule, isValid)
341
+ if (predicates.length > 0) {
342
+ c4Globals.predicates[id] = predicates as c4.NonEmptyArray<c4.ViewRulePredicate>
343
+ }
344
+ return
345
+ }
346
+ if (ast.isGlobalDynamicPredicateGroup(astRule)) {
347
+ const predicates = this.parseGlobalDynamicPredicateGroup(astRule, isValid)
348
+ if (predicates.length > 0) {
349
+ c4Globals.dynamicPredicates[id] = predicates as c4.NonEmptyArray<c4.DynamicViewIncludeRule>
350
+ }
351
+ return
352
+ }
353
+ nonexhaustive(astRule)
354
+ }
355
+
356
+ private parseGlobalPredicateGroup(
357
+ astRule: ast.GlobalPredicateGroup,
358
+ isValid: IsValidFn
359
+ ): c4.ViewRulePredicate[] {
360
+ return astRule.predicates.map(p => this.parseViewRulePredicate(p, isValid))
361
+ }
362
+
363
+ private parseGlobalDynamicPredicateGroup(
364
+ astRule: ast.GlobalDynamicPredicateGroup,
365
+ isValid: IsValidFn
366
+ ): c4.DynamicViewIncludeRule[] {
367
+ return astRule.predicates.map(p => this.parseDynamicViewIncludePredicate(p, isValid))
368
+ }
369
+
313
370
  private parseGlobalStyleOrGroup(
314
371
  astRule: ast.GlobalStyle | ast.GlobalStyleGroup,
315
372
  isValid: IsValidFn
316
373
  ): c4.ViewRuleStyle[] {
317
374
  if (ast.isGlobalStyle(astRule)) {
318
- return [this.parseGlobalStyle(astRule, isValid)]
375
+ return [this.parseViewRuleStyle(astRule, isValid)]
319
376
  }
320
377
  if (ast.isGlobalStyleGroup(astRule)) {
321
378
  return astRule.styles.map(s => this.parseViewRuleStyle(s, isValid))
@@ -323,19 +380,17 @@ export class LikeC4ModelParser {
323
380
  nonexhaustive(astRule)
324
381
  }
325
382
 
326
- private parseGlobalStyle(astRule: ast.GlobalStyle, isValid: IsValidFn): c4.ViewRuleStyle {
327
- const styleProps = astRule.props.filter(ast.isStyleProperty)
328
- const targets = astRule.target
329
- const notation = astRule.props.find(ast.isNotationProperty)
330
- return this.parseRuleStyle(styleProps, targets, isValid, notation)
331
- }
332
-
333
383
  private parseViews(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
334
384
  const viewBlocks = doc.parseResult.value.views.filter(v => isValid(v))
335
385
  for (const viewBlock of viewBlocks) {
336
- const localStyles = viewBlock.styles
337
- .flatMap(s => this.parseViewRuleStyleOrGlobalRef(s, isValid))
338
- const stylesToApply = localStyles
386
+ const localStyles = viewBlock.styles.flatMap(s => {
387
+ try {
388
+ return this.parseViewRuleStyleOrGlobalRef(s, isValid)
389
+ } catch (e) {
390
+ logWarnError(e)
391
+ return []
392
+ }
393
+ })
339
394
 
340
395
  for (const view of viewBlock.views) {
341
396
  try {
@@ -344,8 +399,8 @@ export class LikeC4ModelParser {
344
399
  }
345
400
  doc.c4Views.push(
346
401
  ast.isElementView(view)
347
- ? this.parseElementView(view, stylesToApply, isValid)
348
- : this.parseDynamicElementView(view, stylesToApply, isValid)
402
+ ? this.parseElementView(view, localStyles, isValid)
403
+ : this.parseDynamicElementView(view, localStyles, isValid)
349
404
  )
350
405
  } catch (e) {
351
406
  logWarnError(e)
@@ -415,14 +470,14 @@ export class LikeC4ModelParser {
415
470
  }
416
471
  }
417
472
  if (ast.isElementKindExpression(astNode)) {
418
- invariant(astNode.kind, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
473
+ invariant(astNode.kind?.ref, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
419
474
  return {
420
- elementKind: astNode.kind.$refText as c4.ElementKind,
475
+ elementKind: astNode.kind.ref.name as c4.ElementKind,
421
476
  isEqual: astNode.isEqual
422
477
  }
423
478
  }
424
479
  if (ast.isElementTagExpression(astNode)) {
425
- invariant(astNode.tag, 'ElementTagExpr tag is not resolved: ' + astNode.$cstNode?.text)
480
+ invariant(astNode.tag?.ref, 'ElementTagExpr tag is not resolved: ' + astNode.$cstNode?.text)
426
481
  let elementTag = astNode.tag.$refText
427
482
  if (elementTag.startsWith('#')) {
428
483
  elementTag = elementTag.slice(1)
@@ -653,6 +708,9 @@ export class LikeC4ModelParser {
653
708
  if (ast.isViewRulePredicate(astRule)) {
654
709
  return this.parseViewRulePredicate(astRule, isValid)
655
710
  }
711
+ if (ast.isViewRuleGlobalPredicateRef(astRule)) {
712
+ return this.parseViewRuleGlobalPredicateRef(astRule, isValid)
713
+ }
656
714
  if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
657
715
  return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
658
716
  }
@@ -665,7 +723,19 @@ export class LikeC4ModelParser {
665
723
  nonexhaustive(astRule)
666
724
  }
667
725
 
668
- private parseViewRuleStyleOrGlobalRef(astRule: ast.ViewRuleStyleOrGlobalRef, isValid: IsValidFn): c4.ViewRuleStyleOrGlobalRef {
726
+ private parseViewRuleGlobalPredicateRef(
727
+ astRule: ast.ViewRuleGlobalPredicateRef | ast.DynamicViewGlobalPredicateRef,
728
+ _isValid: IsValidFn
729
+ ): c4.ViewRuleGlobalPredicateRef {
730
+ return {
731
+ predicateId: astRule.predicate.$refText as c4.GlobalPredicateId
732
+ }
733
+ }
734
+
735
+ private parseViewRuleStyleOrGlobalRef(
736
+ astRule: ast.ViewRuleStyleOrGlobalRef,
737
+ isValid: IsValidFn
738
+ ): c4.ViewRuleStyleOrGlobalRef {
669
739
  if (ast.isViewRuleStyle(astRule)) {
670
740
  return this.parseViewRuleStyle(astRule, isValid)
671
741
  }
@@ -675,7 +745,7 @@ export class LikeC4ModelParser {
675
745
  nonexhaustive(astRule)
676
746
  }
677
747
 
678
- private parseViewRuleStyle(astRule: ast.ViewRuleStyle, isValid: IsValidFn): c4.ViewRuleStyle {
748
+ private parseViewRuleStyle(astRule: ast.ViewRuleStyle | ast.GlobalStyle, isValid: IsValidFn): c4.ViewRuleStyle {
679
749
  const styleProps = astRule.props.filter(ast.isStyleProperty)
680
750
  const targets = astRule.target
681
751
  const notation = astRule.props.find(ast.isNotationProperty)
@@ -971,6 +1041,9 @@ export class LikeC4ModelParser {
971
1041
  if (ast.isDynamicViewIncludePredicate(astRule)) {
972
1042
  return this.parseDynamicViewIncludePredicate(astRule, isValid)
973
1043
  }
1044
+ if (ast.isDynamicViewGlobalPredicateRef(astRule)) {
1045
+ return this.parseViewRuleGlobalPredicateRef(astRule, isValid)
1046
+ }
974
1047
  if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
975
1048
  return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
976
1049
  }
@@ -5,6 +5,7 @@ import {
5
5
  type Fqn,
6
6
  invariant,
7
7
  isSameHierarchy,
8
+ type ModelGlobals,
8
9
  parentFqn,
9
10
  type Relation,
10
11
  type RelationID
@@ -14,7 +15,8 @@ import { isArray, isString } from 'remeda'
14
15
  type Params = {
15
16
  elements: Record<Fqn, Element>
16
17
  relations: Record<RelationID, Relation>
17
- // views: ElementView[]
18
+ // Optional for tests
19
+ globals?: ModelGlobals
18
20
  }
19
21
 
20
22
  type RelationEdge = {
@@ -41,24 +43,33 @@ function intersection<T>(a: Set<T>, b: Set<T>) {
41
43
  * Subject to change.
42
44
  */
43
45
  export class LikeC4ModelGraph {
44
- #elements = new Map<Fqn, Element>()
46
+ readonly #elements = new Map<Fqn, Element>()
45
47
  // Parent element for given FQN
46
- #parents = new Map<Fqn, Element>()
48
+ readonly #parents = new Map<Fqn, Element>()
47
49
  // Children elements for given FQN
48
- #children = new Map<Fqn, Element[]>()
49
- #rootElements = new Set<Element>()
50
+ readonly #children = new Map<Fqn, Element[]>()
51
+ readonly #rootElements = new Set<Element>()
50
52
 
51
- #relations = new Map<RelationID, Relation>()
53
+ readonly #relations = new Map<RelationID, Relation>()
52
54
  // Incoming to an element or its descendants
53
- #incoming = new MapRelations()
55
+ readonly #incoming = new MapRelations()
54
56
  // Outgoing from an element or its descendants
55
- #outgoing = new MapRelations()
57
+ readonly #outgoing = new MapRelations()
56
58
  // Relationships inside the element, among descendants
57
- #internal = new MapRelations()
59
+ readonly #internal = new MapRelations()
58
60
 
59
- #cacheAscendingSiblings = new Map<Fqn, Element[]>()
61
+ readonly #cacheAscendingSiblings = new Map<Fqn, Element[]>()
60
62
 
61
- constructor({ elements, relations }: Params) {
63
+ public readonly globals: ModelGlobals
64
+
65
+ constructor(
66
+ { elements, relations, globals }: Params
67
+ ) {
68
+ this.globals = globals ?? {
69
+ predicates: {},
70
+ dynamicPredicates: {},
71
+ styles: {}
72
+ }
62
73
  for (const el of Object.values(elements)) {
63
74
  this.addElement(el)
64
75
  }
@@ -11,6 +11,8 @@ import {
11
11
  type ElementWhereExpr,
12
12
  type Expression as C4Expression,
13
13
  type Fqn,
14
+ type GlobalStyleID,
15
+ type IconUrl,
14
16
  type IncomingExpr as C4IncomingExpr,
15
17
  type InOutExpr as C4InOutExpr,
16
18
  isElementRef,
@@ -28,6 +30,7 @@ import {
28
30
  type Tag,
29
31
  type ViewID,
30
32
  type ViewRule,
33
+ type ViewRuleGlobalStyle,
31
34
  type ViewRuleGroup,
32
35
  type ViewRulePredicate,
33
36
  type ViewRuleStyle,
@@ -161,6 +164,7 @@ export const fakeElements = {
161
164
  id: 'cloud',
162
165
  kind: 'system',
163
166
  title: 'cloud',
167
+ icon: 'none',
164
168
  tags: ['next', 'old']
165
169
  }),
166
170
  'cloud.backend': el({
@@ -177,6 +181,7 @@ export const fakeElements = {
177
181
  'cloud.backend.graphql': el({
178
182
  id: 'cloud.backend.graphql',
179
183
  kind: 'component',
184
+ icon: 'tech:graphql' as IconUrl,
180
185
  title: 'graphql'
181
186
  }),
182
187
  'email': el({
@@ -200,12 +205,14 @@ export const fakeElements = {
200
205
  id: 'cloud.frontend.dashboard',
201
206
  kind: 'component',
202
207
  title: 'dashboard',
208
+ icon: 'tech:react' as IconUrl,
203
209
  tags: ['next']
204
210
  }),
205
211
  'amazon': el({
206
212
  id: 'amazon',
207
213
  kind: 'system',
208
214
  title: 'amazon',
215
+ icon: 'tech:aws' as IconUrl,
209
216
  tags: ['aws']
210
217
  }),
211
218
  'amazon.s3': el({
@@ -213,6 +220,7 @@ export const fakeElements = {
213
220
  kind: 'component',
214
221
  title: 's3',
215
222
  shape: 'storage',
223
+ icon: 'aws:s3' as IconUrl,
216
224
  tags: ['aws', 'storage']
217
225
  })
218
226
  } satisfies Record<string, Element>
@@ -331,11 +339,37 @@ export const fakeRelations = [
331
339
  })
332
340
  ]
333
341
 
342
+ export const globalStyles = {
343
+ 'mute_old': [{
344
+ targets: [$expr({
345
+ elementTag: 'old' as Tag,
346
+ isEqual: true
347
+ })],
348
+ style: {
349
+ color: 'muted'
350
+ }
351
+ }],
352
+ 'red_next': [{
353
+ targets: [$expr({
354
+ elementTag: 'next' as Tag,
355
+ isEqual: true
356
+ })],
357
+ style: {
358
+ color: 'red'
359
+ }
360
+ }]
361
+ } as const
362
+
334
363
  export type FakeRelationIds = (typeof fakeRelations)[number]['id']
335
364
 
336
365
  export const fakeModel = new LikeC4ModelGraph({
337
366
  elements: fakeElements,
338
- relations: indexBy(fakeRelations, r => r.id)
367
+ relations: indexBy(fakeRelations, r => r.id),
368
+ globals: {
369
+ predicates: {},
370
+ dynamicPredicates: {},
371
+ styles: globalStyles
372
+ }
339
373
  })
340
374
 
341
375
  const emptyView = {
@@ -510,30 +544,34 @@ type CustomProps = {
510
544
  }
511
545
  export function $include(expr: Expression | C4Expression, props?: CustomProps): ViewRulePredicate {
512
546
  let _expr = props?.where ? $where(expr, props.where) : $expr(expr)
513
- if (props?.with) {
514
- if (isRelationExpression(_expr) || isRelationWhere(_expr)) {
515
- _expr = {
516
- customRelation: {
517
- relation: _expr,
518
- ...props.with as any
519
- }
547
+ _expr = props?.with ? $with(_expr, props.with) : _expr
548
+ return {
549
+ include: [_expr]
550
+ }
551
+ }
552
+ export function $with(expr: C4Expression, props?: CustomProps['with']): C4CustomRelationExpr | C4CustomElementExpr {
553
+ if (isRelationExpression(expr) || isRelationWhere(expr)) {
554
+ return {
555
+ customRelation: {
556
+ relation: expr,
557
+ ...props as any
520
558
  }
521
- } else if (isElementRef(_expr) || isElementWhere(_expr)) {
522
- _expr = {
523
- custom: {
524
- expr: _expr,
525
- ...props.with as any
526
- }
559
+ }
560
+ } else if (isElementRef(expr) || isElementWhere(expr)) {
561
+ return {
562
+ custom: {
563
+ expr: expr,
564
+ ...props as any
527
565
  }
528
566
  }
529
567
  }
530
- return {
531
- include: [_expr]
532
- }
568
+
569
+ throw 'Unsupported type of internal expression'
533
570
  }
534
- export function $exclude(expr: Expression | C4Expression): ViewRulePredicate {
571
+ export function $exclude(expr: Expression | C4Expression, where?: WhereOperator<TestTag, string>): ViewRulePredicate {
572
+ let _expr = where ? $where(expr, where) : $expr(expr)
535
573
  return {
536
- exclude: [$expr(expr)]
574
+ exclude: [_expr]
537
575
  }
538
576
  }
539
577
  export function $group(groupRules: ViewRuleGroup['groupRules']): ViewRuleGroup {
@@ -550,6 +588,18 @@ export function $style(element: ElementRefExpr, style: ViewRuleStyle['style']):
550
588
  }
551
589
  }
552
590
 
591
+ type GlobalStyles = keyof typeof globalStyles
592
+ type GlobalExpr = `style ${GlobalStyles}`
593
+ export function $global(expr: GlobalExpr): ViewRuleGlobalStyle {
594
+ const [_t, id] = expr.split(' ') as [string, string]
595
+ if (_t === 'style') {
596
+ return {
597
+ styleId: id as GlobalStyleID
598
+ }
599
+ }
600
+ throw new Error('Invalid global expression')
601
+ }
602
+
553
603
  export function computeView(
554
604
  ...args: [FakeElementIds, ViewRule | ViewRule[]] | [ViewRule | ViewRule[]]
555
605
  ) {