@likec4/language-server 1.15.1 → 1.16.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 (43) 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 +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 +2 -2
  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.7iILaJYc.d.ts +1338 -0
  18. package/dist/shared/{language-server.B8s2wfT_.cjs → language-server.Bd3NZ8uH.cjs} +123 -72
  19. package/dist/shared/{language-server.BT4WTbFI.mjs → language-server.C5gxpVUH.mjs} +125 -74
  20. package/dist/shared/language-server.CmBZHwSl.d.cts +1338 -0
  21. package/dist/shared/{language-server.U2piOAVt.d.cts → language-server.CnkCWVtf.d.cts} +44 -10
  22. package/dist/shared/{language-server.DfMwkd2l.d.mts → language-server.DIaiY0-C.d.mts} +44 -10
  23. package/dist/shared/language-server.DViE1Zxi.d.mts +1338 -0
  24. package/dist/shared/{language-server.BuChFlda.mjs → language-server.D_13fWJQ.mjs} +177 -84
  25. package/dist/shared/{language-server.j-ShR6as.d.ts → language-server.DwyQ1FtY.d.ts} +44 -10
  26. package/dist/shared/{language-server.DfjkvknB.cjs → language-server.Dym6GL4P.cjs} +175 -82
  27. package/package.json +7 -7
  28. package/src/ast.ts +11 -3
  29. package/src/generated/ast.ts +111 -10
  30. package/src/generated/grammar.ts +1 -1
  31. package/src/like-c4.langium +19 -0
  32. package/src/model/model-builder.ts +25 -30
  33. package/src/model/model-parser.ts +91 -18
  34. package/src/model-graph/LikeC4ModelGraph.ts +22 -11
  35. package/src/model-graph/compute-view/__test__/fixture.ts +63 -19
  36. package/src/model-graph/compute-view/compute.ts +77 -72
  37. package/src/model-graph/dynamic-view/compute.ts +4 -1
  38. package/src/model-graph/utils/applyViewRuleStyles.ts +0 -4
  39. package/src/references/scope-computation.ts +10 -0
  40. package/src/validation/index.ts +3 -0
  41. package/src/validation/specification.ts +21 -0
  42. package/src/view-utils/index.ts +0 -1
  43. package/src/view-utils/resolve-global-rules.ts +66 -50
@@ -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
 
@@ -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,7 @@ import {
11
11
  type ElementWhereExpr,
12
12
  type Expression as C4Expression,
13
13
  type Fqn,
14
+ type GlobalStyleID,
14
15
  type IncomingExpr as C4IncomingExpr,
15
16
  type InOutExpr as C4InOutExpr,
16
17
  isElementRef,
@@ -28,6 +29,7 @@ import {
28
29
  type Tag,
29
30
  type ViewID,
30
31
  type ViewRule,
32
+ type ViewRuleGlobalStyle,
31
33
  type ViewRuleGroup,
32
34
  type ViewRulePredicate,
33
35
  type ViewRuleStyle,
@@ -331,11 +333,37 @@ export const fakeRelations = [
331
333
  })
332
334
  ]
333
335
 
336
+ export const globalStyles = {
337
+ 'mute_old': [{
338
+ targets: [$expr({
339
+ elementTag: 'old' as Tag,
340
+ isEqual: true
341
+ })],
342
+ style: {
343
+ color: 'muted'
344
+ }
345
+ }],
346
+ 'red_next': [{
347
+ targets: [$expr({
348
+ elementTag: 'next' as Tag,
349
+ isEqual: true
350
+ })],
351
+ style: {
352
+ color: 'red'
353
+ }
354
+ }]
355
+ } as const
356
+
334
357
  export type FakeRelationIds = (typeof fakeRelations)[number]['id']
335
358
 
336
359
  export const fakeModel = new LikeC4ModelGraph({
337
360
  elements: fakeElements,
338
- relations: indexBy(fakeRelations, r => r.id)
361
+ relations: indexBy(fakeRelations, r => r.id),
362
+ globals: {
363
+ predicates: {},
364
+ dynamicPredicates: {},
365
+ styles: globalStyles
366
+ }
339
367
  })
340
368
 
341
369
  const emptyView = {
@@ -510,30 +538,34 @@ type CustomProps = {
510
538
  }
511
539
  export function $include(expr: Expression | C4Expression, props?: CustomProps): ViewRulePredicate {
512
540
  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
- }
541
+ _expr = props?.with ? $with(_expr, props.with) : _expr
542
+ return {
543
+ include: [_expr]
544
+ }
545
+ }
546
+ export function $with(expr: C4Expression, props?: CustomProps['with']): C4CustomRelationExpr | C4CustomElementExpr {
547
+ if (isRelationExpression(expr) || isRelationWhere(expr)) {
548
+ return {
549
+ customRelation: {
550
+ relation: expr,
551
+ ...props as any
520
552
  }
521
- } else if (isElementRef(_expr) || isElementWhere(_expr)) {
522
- _expr = {
523
- custom: {
524
- expr: _expr,
525
- ...props.with as any
526
- }
553
+ }
554
+ } else if (isElementRef(expr) || isElementWhere(expr)) {
555
+ return {
556
+ custom: {
557
+ expr: expr,
558
+ ...props as any
527
559
  }
528
560
  }
529
561
  }
530
- return {
531
- include: [_expr]
532
- }
562
+
563
+ throw 'Unsupported type of internal expression'
533
564
  }
534
- export function $exclude(expr: Expression | C4Expression): ViewRulePredicate {
565
+ export function $exclude(expr: Expression | C4Expression, where?: WhereOperator<TestTag, string>): ViewRulePredicate {
566
+ let _expr = where ? $where(expr, where) : $expr(expr)
535
567
  return {
536
- exclude: [$expr(expr)]
568
+ exclude: [_expr]
537
569
  }
538
570
  }
539
571
  export function $group(groupRules: ViewRuleGroup['groupRules']): ViewRuleGroup {
@@ -550,6 +582,18 @@ export function $style(element: ElementRefExpr, style: ViewRuleStyle['style']):
550
582
  }
551
583
  }
552
584
 
585
+ type GlobalStyles = keyof typeof globalStyles
586
+ type GlobalExpr = `style ${GlobalStyles}`
587
+ export function $global(expr: GlobalExpr): ViewRuleGlobalStyle {
588
+ const [_t, id] = expr.split(' ') as [string, string]
589
+ if (_t === 'style') {
590
+ return {
591
+ styleId: id as GlobalStyleID
592
+ }
593
+ }
594
+ throw new Error('Invalid global expression')
595
+ }
596
+
553
597
  export function computeView(
554
598
  ...args: [FakeElementIds, ViewRule | ViewRule[]] | [ViewRule | ViewRule[]]
555
599
  ) {