@likec4/language-server 1.4.0 → 1.6.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 (45) hide show
  1. package/README.md +1 -1
  2. package/contrib/likec4.tmLanguage.json +1 -1
  3. package/package.json +26 -14
  4. package/src/Rpc.ts +25 -2
  5. package/src/ast.ts +19 -15
  6. package/src/generated/ast.ts +390 -203
  7. package/src/generated/grammar.ts +1 -1
  8. package/src/generated-lib/icons.ts +952 -0
  9. package/src/like-c4.langium +120 -64
  10. package/src/likec4lib.ts +7 -0
  11. package/src/lsp/DocumentSymbolProvider.ts +28 -1
  12. package/src/lsp/SemanticTokenProvider.ts +41 -22
  13. package/src/model/fqn-computation.ts +29 -7
  14. package/src/model/fqn-index.ts +18 -31
  15. package/src/model/model-builder.ts +13 -9
  16. package/src/model/model-locator.ts +7 -7
  17. package/src/model/model-parser.ts +166 -69
  18. package/src/model-change/changeElementStyle.ts +6 -6
  19. package/src/model-graph/compute-view/__test__/fixture.ts +52 -24
  20. package/src/model-graph/compute-view/compute.ts +51 -20
  21. package/src/model-graph/compute-view/predicates.ts +6 -1
  22. package/src/model-graph/dynamic-view/__test__/fixture.ts +2 -2
  23. package/src/model-graph/dynamic-view/compute.ts +2 -2
  24. package/src/model-graph/utils/{applyElementCustomProperties.ts → applyCustomElementProperties.ts} +5 -3
  25. package/src/model-graph/utils/applyCustomRelationProperties.ts +50 -0
  26. package/src/model-graph/utils/applyViewRuleStyles.ts +11 -34
  27. package/src/model-graph/utils/elementExpressionToPredicate.ts +32 -0
  28. package/src/references/scope-computation.ts +113 -60
  29. package/src/references/scope-provider.ts +3 -23
  30. package/src/shared/NodeKindProvider.ts +1 -0
  31. package/src/shared/WorkspaceManager.ts +15 -6
  32. package/src/validation/dynamic-view-rule.ts +19 -26
  33. package/src/validation/element.ts +8 -4
  34. package/src/validation/index.ts +9 -6
  35. package/src/validation/property-checks.ts +23 -1
  36. package/src/validation/view-predicates/custom-element-expr.ts +21 -8
  37. package/src/validation/view-predicates/custom-relation-expr.ts +16 -0
  38. package/src/validation/view-predicates/expanded-element.ts +13 -24
  39. package/src/validation/view-predicates/incoming.ts +5 -5
  40. package/src/validation/view-predicates/index.ts +1 -0
  41. package/src/validation/view-predicates/outgoing.ts +5 -5
  42. package/src/view-utils/assignNavigateTo.ts +2 -2
  43. package/src/view-utils/manual-layout.ts +4 -2
  44. package/src/view-utils/resolve-extended-views.ts +2 -2
  45. package/src/view-utils/resolve-relative-paths.ts +3 -3
@@ -2,9 +2,9 @@ import {
2
2
  type c4,
3
3
  compareByFqnHierarchically,
4
4
  isElementView,
5
- isStrictElementView,
5
+ isScopedElementView,
6
6
  parentFqn,
7
- type StrictElementView,
7
+ type ScopedElementView,
8
8
  type ViewID
9
9
  } from '@likec4/core'
10
10
  import { deepEqual as eq } from 'fast-equals'
@@ -26,7 +26,7 @@ import {
26
26
  sort,
27
27
  values
28
28
  } from 'remeda'
29
- import { type CancellationToken, Disposable } from 'vscode-languageserver'
29
+ import { type CancellationToken, Disposable } from 'vscode-languageserver-protocol'
30
30
  import type {
31
31
  ParsedAstElement,
32
32
  ParsedAstRelation,
@@ -107,7 +107,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
107
107
 
108
108
  const elements = pipe(
109
109
  docs,
110
- flatMap(d => d.c4Elements.map(toModelElement(d))),
110
+ flatMap(d => map(d.c4Elements, toModelElement(d))),
111
111
  filter(isTruthy),
112
112
  sort(compareByFqnHierarchically),
113
113
  reduce(
@@ -140,10 +140,13 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
140
140
  ...model
141
141
  }: ParsedAstRelation): c4.Relation | null => {
142
142
  if (isNullish(elements[source]) || isNullish(elements[target])) {
143
+ logger.warn(
144
+ `Invalid relation ${id}, source: ${source}(${!!elements[source]}), target: ${target}(${!!elements[target]})`
145
+ )
143
146
  return null
144
147
  }
145
148
 
146
- if (!!kind && kind in c4Specification.relationships) {
149
+ if (!isNullish(kind) && kind in c4Specification.relationships) {
147
150
  return {
148
151
  ...(links && { links: resolveLinks(doc, links) }),
149
152
  ...c4Specification.relationships[kind],
@@ -165,14 +168,15 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
165
168
  }
166
169
 
167
170
  const relations = pipe(
168
- flatMap(docs, d => map(d.c4Relations, toModelRelation(d))),
171
+ docs,
172
+ flatMap(d => map(d.c4Relations, toModelRelation(d))),
169
173
  filter(isTruthy),
170
174
  mapToObj(r => [r.id, r])
171
175
  )
172
176
 
173
177
  const toC4View = (doc: LangiumDocument) => {
174
178
  const docUri = doc.uri.toString()
175
- return (parsedAstView: ParsedAstView): c4.View => {
179
+ return (parsedAstView: ParsedAstView): c4.LikeC4View => {
176
180
  let {
177
181
  id,
178
182
  title,
@@ -188,7 +192,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
188
192
  } = parsedAstView
189
193
 
190
194
  if (parsedAstView.__ === 'element' && isNullish(title) && 'viewOf' in parsedAstView) {
191
- title ??= elements[parsedAstView.viewOf]?.title ?? null
195
+ title = elements[parsedAstView.viewOf]?.title ?? null
192
196
  }
193
197
 
194
198
  if (isNullish(title) && id === 'index') {
@@ -359,7 +363,7 @@ export class LikeC4ModelBuilder {
359
363
  }
360
364
 
361
365
  const allElementViews = values(model.views).filter(
362
- (v): v is StrictElementView => isStrictElementView(v) && v.id !== viewId
366
+ (v): v is ScopedElementView => isScopedElementView(v) && v.id !== viewId
363
367
  )
364
368
 
365
369
  let computedView = result.view
@@ -33,18 +33,18 @@ export class LikeC4ModelLocator {
33
33
  return doc.c4Elements.find(e => e.id === fqn) ?? null
34
34
  }
35
35
 
36
- public locateElement(fqn: c4.Fqn, property = 'name'): Location | null {
36
+ public locateElement(fqn: c4.Fqn, _property = 'name'): Location | null {
37
37
  const entry = this.fqnIndex.byFqn(fqn).head()
38
38
  if (!entry) {
39
39
  return null
40
40
  }
41
- const propertyNode = findNodeForProperty(entry.el.$cstNode, property) ?? entry.el.$cstNode
42
- if (!propertyNode) {
43
- return null
44
- }
41
+ // const propertyNode = findNodeForProperty(entry.el.$cstNode, property) ?? entry.el.$cstNode
42
+ // if (!propertyNode) {
43
+ // return null
44
+ // }
45
45
  return {
46
- uri: entry.doc.uri.toString(),
47
- range: propertyNode.range
46
+ uri: entry.documentUri.toString(),
47
+ range: entry.nameSegment?.range!
48
48
  }
49
49
  }
50
50
 
@@ -1,7 +1,7 @@
1
1
  import { type c4, InvalidModelError, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
2
2
  import type { AstNode, LangiumDocument } from 'langium'
3
3
  import { AstUtils, CstUtils } from 'langium'
4
- import { isTruthy } from 'remeda'
4
+ import { isTruthy, mapToObj } from 'remeda'
5
5
  import stripIndent from 'strip-indent'
6
6
  import type {
7
7
  ChecksFromDiagnostics,
@@ -37,14 +37,14 @@ const { getDocument } = AstUtils
37
37
  export type ModelParsedListener = () => void
38
38
 
39
39
  function toSingleLine<T extends string | undefined>(str: T): T {
40
- return (str ? removeIndent(str).split('\n').join(' ') : undefined) as T
40
+ return (isTruthy(str) ? removeIndent(str).split('\n').join(' ') : undefined) as T
41
41
  }
42
42
 
43
43
  function removeIndent<T extends string | undefined>(str: T): T {
44
- return (str ? stripIndent(str).trim() : undefined) as T
44
+ return (isTruthy(str) ? stripIndent(str).trim() : undefined) as T
45
45
  }
46
46
 
47
- type IsValidFn = ChecksFromDiagnostics['isValid']
47
+ export type IsValidFn = ChecksFromDiagnostics['isValid']
48
48
 
49
49
  export class LikeC4ModelParser {
50
50
  private fqnIndex: FqnIndex
@@ -142,14 +142,23 @@ export class LikeC4ModelParser {
142
142
 
143
143
  let [title, description, technology] = astNode.props ?? []
144
144
 
145
- const bodyProps = astNode.body?.props.filter(ast.isElementStringProperty) ?? []
145
+ const bodyProps = mapToObj(astNode.body?.props.filter(ast.isElementStringProperty) ?? [], p => [p.key, p.value])
146
146
 
147
- title = toSingleLine(title ?? bodyProps.find(p => p.key === 'title')?.value)
148
- description = removeIndent(description ?? bodyProps.find(p => p.key === 'description')?.value)
149
- technology = toSingleLine(technology ?? bodyProps.find(p => p.key === 'technology')?.value)
147
+ title = toSingleLine(title ?? bodyProps.title)
148
+ description = removeIndent(bodyProps.description ?? description)
149
+ technology = toSingleLine(bodyProps.technology ?? technology)
150
150
 
151
151
  const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
152
152
 
153
+ // Property has higher priority than from style
154
+ const iconProp = astNode.body?.props.find(ast.isIconProperty)
155
+ if (iconProp) {
156
+ const value = iconProp.libicon?.ref?.name ?? iconProp.value
157
+ if (isTruthy(value)) {
158
+ style.icon = value as c4.IconUrl
159
+ }
160
+ }
161
+
153
162
  return {
154
163
  id,
155
164
  kind,
@@ -171,7 +180,7 @@ export class LikeC4ModelParser {
171
180
  const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
172
181
  const kind = astNode.kind?.ref?.name as c4.RelationshipKind
173
182
  const astPath = this.getAstNodePath(astNode)
174
- const title = toSingleLine(
183
+ const title = removeIndent(
175
184
  astNode.title ?? astNode.body?.props.find((p): p is ast.RelationStringProperty => p.key === 'title')?.value
176
185
  ) ?? ''
177
186
  const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty)
@@ -209,39 +218,70 @@ export class LikeC4ModelParser {
209
218
  }
210
219
  }
211
220
 
212
- private parseElementExpr(astNode: ast.ElementExpr): c4.ElementExpression {
213
- if (ast.isWildcardExpr(astNode)) {
221
+ // TODO validate view rules
222
+ private parseViewRulePredicate(astNode: ast.ViewRulePredicate, _isValid: IsValidFn): c4.ViewRulePredicate {
223
+ const exprs = [] as c4.Expression[]
224
+ let exprNode: ast.Expressions | undefined = astNode.exprs
225
+ while (exprNode) {
226
+ try {
227
+ if (isTruthy(exprNode.value)) {
228
+ exprs.unshift(this.parseExpression(exprNode.value))
229
+ }
230
+ } catch (e) {
231
+ logWarnError(e)
232
+ }
233
+ exprNode = exprNode.prev
234
+ }
235
+ return ast.isIncludePredicate(astNode) ? { include: exprs } : { exclude: exprs }
236
+ }
237
+
238
+ private parseElementExpressionsIterator(astNode: ast.ElementExpressionsIterator): c4.ElementExpression[] {
239
+ const exprs = [] as c4.ElementExpression[]
240
+ let iter: ast.ElementExpressionsIterator | undefined = astNode
241
+ while (iter) {
242
+ try {
243
+ exprs.unshift(this.parseElementExpr(iter.value))
244
+ } catch (e) {
245
+ logWarnError(e)
246
+ }
247
+ iter = iter.prev
248
+ }
249
+ return exprs
250
+ }
251
+
252
+ private parseElementExpr(astNode: ast.ElementExpression): c4.ElementExpression {
253
+ if (ast.isWildcardExpression(astNode)) {
214
254
  return {
215
255
  wildcard: true
216
256
  }
217
257
  }
218
- if (ast.isElementKindExpr(astNode)) {
219
- // invariant(astNode.kind.ref, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
258
+ if (ast.isElementKindExpression(astNode)) {
259
+ invariant(astNode.kind, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
220
260
  return {
221
261
  elementKind: astNode.kind.$refText as c4.ElementKind,
222
262
  isEqual: astNode.isEqual
223
263
  }
224
264
  }
225
- if (ast.isElementTagExpr(astNode)) {
265
+ if (ast.isElementTagExpression(astNode)) {
266
+ invariant(astNode.tag, 'ElementTagExpr tag is not resolved: ' + astNode.$cstNode?.text)
226
267
  let elementTag = astNode.tag.$refText
227
268
  if (elementTag.startsWith('#')) {
228
269
  elementTag = elementTag.slice(1)
229
270
  }
230
- // invariant(astNode.tag.ref, 'ElementTagExpr tag is not resolved: ' + astNode.$cstNode?.text)
231
271
  return {
232
272
  elementTag: elementTag as c4.Tag,
233
273
  isEqual: astNode.isEqual
234
274
  }
235
275
  }
236
- if (ast.isExpandElementExpr(astNode)) {
237
- const elementNode = elementRef(astNode.parent)
238
- invariant(elementNode, 'Element not found ' + astNode.parent.$cstNode?.text)
276
+ if (ast.isExpandElementExpression(astNode)) {
277
+ const elementNode = elementRef(astNode.expand)
278
+ invariant(elementNode, 'Element not found ' + astNode.expand.$cstNode?.text)
239
279
  const expanded = this.resolveFqn(elementNode)
240
280
  return {
241
281
  expanded
242
282
  }
243
283
  }
244
- if (ast.isDescedantsExpr(astNode)) {
284
+ if (ast.isElementDescedantsExpression(astNode)) {
245
285
  const elementNode = elementRef(astNode.parent)
246
286
  invariant(elementNode, 'Element not found ' + astNode.parent.$cstNode?.text)
247
287
  const element = this.resolveFqn(elementNode)
@@ -261,20 +301,25 @@ export class LikeC4ModelParser {
261
301
  nonexhaustive(astNode)
262
302
  }
263
303
 
264
- private parseCustomElementExpr(astNode: ast.CustomElementExpr): c4.CustomElementExpr {
304
+ private parseCustomElementExpr(astNode: ast.CustomElementExpression): c4.CustomElementExpr {
265
305
  let targetRef
266
- if (ast.isElementRef(astNode.target)) {
267
- targetRef = astNode.target
268
- } else if (ast.isExpandElementExpr(astNode.target)) {
269
- targetRef = astNode.target.parent
270
- } else {
271
- invariant(false, 'ElementRef expected as target of custom element')
272
- }
273
- // invariant(ast.isElementRef(astNode.target), 'ElementRef expected as target of custom element')
306
+ switch (true) {
307
+ case ast.isElementRef(astNode.target):
308
+ targetRef = astNode.target
309
+ break
310
+ case ast.isExpandElementExpression(astNode.target):
311
+ targetRef = astNode.target.expand
312
+ break
313
+ case ast.isElementDescedantsExpression(astNode.target):
314
+ targetRef = astNode.target.parent
315
+ break
316
+ default:
317
+ throw new Error('Unsupported target of custom element')
318
+ }
274
319
  const elementNode = elementRef(targetRef)
275
320
  invariant(elementNode, 'element not found: ' + astNode.$cstNode?.text)
276
321
  const element = this.resolveFqn(elementNode)
277
- const props = astNode.body?.props ?? []
322
+ const props = astNode.custom.props ?? []
278
323
  return props.reduce(
279
324
  (acc, prop) => {
280
325
  if (ast.isNavigateToProperty(prop)) {
@@ -290,7 +335,10 @@ export class LikeC4ModelParser {
290
335
  return acc
291
336
  }
292
337
  if (ast.isIconProperty(prop)) {
293
- acc.custom[prop.key] = prop.value as c4.IconUrl
338
+ const value = prop.libicon?.ref?.name ?? prop.value
339
+ if (isTruthy(value)) {
340
+ acc.custom[prop.key] = value as c4.IconUrl
341
+ }
294
342
  return acc
295
343
  }
296
344
  if (ast.isColorProperty(prop)) {
@@ -320,54 +368,90 @@ export class LikeC4ModelParser {
320
368
  )
321
369
  }
322
370
 
323
- private parsePredicateExpr(astNode: ast.ViewRulePredicateExpr): c4.Expression {
324
- if (ast.isRelationExpr(astNode)) {
371
+ private parseExpression(astNode: ast.Expression): c4.Expression {
372
+ if (ast.isCustomRelationExpression(astNode)) {
373
+ return this.parseCustomRelationExpr(astNode)
374
+ }
375
+ if (ast.isCustomElementExpression(astNode)) {
376
+ return this.parseCustomElementExpr(astNode)
377
+ }
378
+ if (ast.isElementExpression(astNode)) {
379
+ return this.parseElementExpr(astNode)
380
+ }
381
+ if (ast.isRelationExpression(astNode)) {
382
+ return this.parseRelationExpr(astNode)
383
+ }
384
+ nonexhaustive(astNode)
385
+ }
386
+
387
+ private parseCustomRelationExpr(astNode: ast.CustomRelationExpression): c4.CustomRelationExpr {
388
+ const relation = this.parseRelationExpr(astNode.relation)
389
+ const props = astNode.custom.props ?? []
390
+ return props.reduce(
391
+ (acc, prop) => {
392
+ if (ast.isRelationStringProperty(prop)) {
393
+ const value = removeIndent(prop.value)
394
+ if (isTruthy(value)) {
395
+ acc.customRelation['title'] = value
396
+ }
397
+ return acc
398
+ }
399
+ if (ast.isArrowProperty(prop)) {
400
+ acc.customRelation[prop.key] = prop.value
401
+ return acc
402
+ }
403
+ if (ast.isColorProperty(prop)) {
404
+ acc.customRelation[prop.key] = prop.value
405
+ return acc
406
+ }
407
+ if (ast.isLineProperty(prop)) {
408
+ acc.customRelation[prop.key] = prop.value
409
+ return acc
410
+ }
411
+ nonexhaustive(prop)
412
+ },
413
+ {
414
+ customRelation: {
415
+ relation
416
+ }
417
+ } as c4.CustomRelationExpr
418
+ )
419
+ }
420
+
421
+ private parseRelationExpr(astNode: ast.RelationExpression): c4.RelationExpression {
422
+ if (ast.isDirectedRelationExpression(astNode)) {
325
423
  return {
326
- source: this.parseElementExpr(astNode.source),
424
+ source: this.parseElementExpr(astNode.source.from),
327
425
  target: this.parseElementExpr(astNode.target),
328
- isBidirectional: astNode.isBidirectional
426
+ isBidirectional: astNode.source.isBidirectional
329
427
  }
330
428
  }
331
- if (ast.isInOutExpr(astNode)) {
429
+ if (ast.isInOutRelationExpression(astNode)) {
332
430
  return {
333
431
  inout: this.parseElementExpr(astNode.inout.to)
334
432
  }
335
433
  }
336
- if (ast.isOutgoingExpr(astNode)) {
434
+ if (ast.isOutgoingRelationExpression(astNode)) {
337
435
  return {
338
436
  outgoing: this.parseElementExpr(astNode.from)
339
437
  }
340
438
  }
341
- if (ast.isIncomingExpr(astNode)) {
439
+ if (ast.isIncomingRelationExpression(astNode)) {
342
440
  return {
343
441
  incoming: this.parseElementExpr(astNode.to)
344
442
  }
345
443
  }
346
- if (ast.isCustomElementExpr(astNode)) {
347
- return this.parseCustomElementExpr(astNode)
348
- }
349
- if (ast.isElementExpr(astNode)) {
350
- return this.parseElementExpr(astNode)
351
- }
352
444
  nonexhaustive(astNode)
353
445
  }
354
446
 
355
447
  private parseViewRule(astRule: ast.ViewRule, isValid: IsValidFn): c4.ViewRule {
356
- if (ast.isIncludePredicate(astRule) || ast.isExcludePredicate(astRule)) {
357
- const exprs = astRule.expressions.flatMap(n => {
358
- try {
359
- return isValid(n) ? this.parsePredicateExpr(n) : []
360
- } catch (e) {
361
- logWarnError(e)
362
- return []
363
- }
364
- })
365
- return ast.isIncludePredicate(astRule) ? { include: exprs } : { exclude: exprs }
448
+ if (ast.isViewRulePredicate(astRule)) {
449
+ return this.parseViewRulePredicate(astRule, isValid)
366
450
  }
367
451
  if (ast.isViewRuleStyle(astRule)) {
368
- const styleProps = toElementStyle(astRule.styleprops)
452
+ const styleProps = toElementStyle(astRule.props)
369
453
  return {
370
- targets: astRule.targets.map(n => this.parseElementExpr(n)),
454
+ targets: this.parseElementExpressionsIterator(astRule.target),
371
455
  style: {
372
456
  ...styleProps
373
457
  }
@@ -398,18 +482,22 @@ export class LikeC4ModelParser {
398
482
  if (!targetEl) {
399
483
  throw new Error('Invalid reference to target')
400
484
  }
485
+ const title = removeIndent(node.title) ?? null
401
486
  let source = this.resolveFqn(sourceEl)
402
487
  let target = this.resolveFqn(targetEl)
403
488
  if (node.isBackward) {
404
- ;[source, target] = [target, source]
489
+ return {
490
+ source: target,
491
+ target: source,
492
+ title,
493
+ isBackward: true
494
+ }
405
495
  }
406
496
 
407
- const title = toSingleLine(node.title) ?? null
408
497
  return {
409
498
  source,
410
499
  target,
411
- title,
412
- isBackward: node.isBackward
500
+ title
413
501
  }
414
502
  }
415
503
 
@@ -518,15 +606,24 @@ export class LikeC4ModelParser {
518
606
  try {
519
607
  if (ast.isDynamicViewRulePredicate(n)) {
520
608
  const include = [] as (c4.ElementExpression | c4.CustomElementExpr)[]
521
- for (const expr of n.expressions) {
522
- if (ast.isElementExpr(expr)) {
523
- include.push(this.parseElementExpr(expr))
524
- continue
525
- }
526
- if (ast.isCustomElementExpr(expr)) {
527
- include.push(this.parseCustomElementExpr(expr))
528
- continue
609
+ let iter: ast.DynamicViewRulePredicateIterator | undefined = n.exprs
610
+ while (iter) {
611
+ try {
612
+ switch (true) {
613
+ case ast.isElementExpression(iter.value):
614
+ isValid(iter.value) && include.unshift(this.parseElementExpr(iter.value))
615
+ break
616
+
617
+ case ast.isCustomElementExpression(iter.value):
618
+ isValid(iter.value) && include.unshift(this.parseCustomElementExpr(iter.value))
619
+ break
620
+ default:
621
+ nonexhaustive(iter.value)
622
+ }
623
+ } catch (e) {
624
+ logWarnError(e)
529
625
  }
626
+ iter = iter.prev
530
627
  }
531
628
  if (include.length > 0) {
532
629
  acc.push({ include })
@@ -534,8 +631,8 @@ export class LikeC4ModelParser {
534
631
  return acc
535
632
  }
536
633
  if (ast.isViewRuleStyle(n)) {
537
- const styleProps = toElementStyle(n.styleprops)
538
- const targets = n.targets.map(n => this.parseElementExpr(n))
634
+ const styleProps = toElementStyle(n.props)
635
+ const targets = this.parseElementExpressionsIterator(n.target)
539
636
  if (targets.length > 0) {
540
637
  acc.push({
541
638
  targets,
@@ -1,6 +1,6 @@
1
1
  import { type Fqn, invariant, isAncestor, type NonEmptyArray, nonNullable, type ViewChanges } from '@likec4/core'
2
2
  import { GrammarUtils } from 'langium'
3
- import { entries, filter, findLast, last } from 'remeda'
3
+ import { entries, filter, findLast, isTruthy, last } from 'remeda'
4
4
  import { type Range, TextEdit } from 'vscode-languageserver-protocol'
5
5
  import { ast, type ParsedAstView, type ParsedLikeC4LangiumDocument } from '../ast'
6
6
  import type { FqnIndex } from '../model'
@@ -12,7 +12,7 @@ const asViewStyleRule = (target: string, style: ViewChanges.ChangeElementStyle['
12
12
  const indentStr = indent > 0 ? ' '.repeat(indent) : ''
13
13
  return [
14
14
  indentStr + `style ${target} {`,
15
- ...entries.strict(style).map(([key, value]) =>
15
+ ...entries(style).map(([key, value]) =>
16
16
  indentStr + ` ${key} ${key === 'opacity' ? value.toString() + '%' : value}`
17
17
  ),
18
18
  indentStr + `}`
@@ -37,8 +37,8 @@ const isMatchingViewRule =
37
37
  if (!ast.isViewRuleStyle(rule)) {
38
38
  return false
39
39
  }
40
- const [target, ...rest] = rule.targets
41
- if (!target || rest.length > 0 || !ast.isElementRef(target)) {
40
+ const target = rule.target.value
41
+ if (!target || isTruthy(rule.target.prev) || !ast.isElementRef(target)) {
42
42
  return false
43
43
  }
44
44
  const ref = target.el.ref
@@ -125,9 +125,9 @@ export function changeElementStyle(services: LikeC4Services, {
125
125
  for (const { rule } of existing) {
126
126
  const ruleCstNode = rule.$cstNode
127
127
  invariant(ruleCstNode, 'RuleCstNode not found')
128
- for (const [key, _value] of entries.strict(style)) {
128
+ for (const [key, _value] of entries(style)) {
129
129
  const value = key === 'opacity' ? _value.toString() + '%' : _value
130
- const ruleProp = rule.styleprops.find(p => p.key === key)
130
+ const ruleProp = rule.props.find(p => p.key === key)
131
131
  // replace existing property
132
132
  if (ruleProp && ruleProp.$cstNode) {
133
133
  const { range: { start, end } } = nonNullable(