@likec4/language-server 1.7.2 → 1.7.4

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@likec4/language-server",
3
3
  "description": "LikeC4 Language Server",
4
- "version": "1.7.2",
4
+ "version": "1.7.4",
5
5
  "license": "MIT",
6
6
  "bugs": "https://github.com/likec4/likec4/issues",
7
7
  "homepage": "https://likec4.dev",
@@ -91,9 +91,9 @@
91
91
  "test:watch": "vitest"
92
92
  },
93
93
  "dependencies": {
94
- "@dagrejs/graphlib": "^2.2.2",
95
- "@likec4/core": "1.7.2",
96
- "@likec4/log": "1.7.2",
94
+ "@dagrejs/graphlib": "^2.2.3",
95
+ "@likec4/core": "1.7.4",
96
+ "@likec4/log": "1.7.4",
97
97
  "@msgpack/msgpack": "^3.0.0-beta2",
98
98
  "@smithy/util-base64": "^3.0.0",
99
99
  "@total-typescript/ts-reset": "^0.5.1",
@@ -114,9 +114,9 @@
114
114
  "vscode-uri": "3.0.8"
115
115
  },
116
116
  "devDependencies": {
117
- "@likec4/icons": "1.7.2",
118
- "@likec4/tsconfig": "1.7.2",
119
- "@types/node": "^20.14.13",
117
+ "@likec4/icons": "1.7.4",
118
+ "@likec4/tsconfig": "1.7.4",
119
+ "@types/node": "^20.14.14",
120
120
  "@types/object-hash": "^3.0.6",
121
121
  "@types/string-hash": "^1.1.3",
122
122
  "execa": "^9.3.0",
@@ -19,7 +19,7 @@ const parseEquals = (
19
19
  return not ? { neq: value } : { eq: value }
20
20
  }
21
21
 
22
- export function parseWhereClause(astNode: ast.WhereExpression): c4.WhereOperator {
22
+ export function parseWhereClause(astNode: ast.WhereExpression): c4.WhereOperator<string, string> {
23
23
  switch (true) {
24
24
  case ast.isWhereTagEqual(astNode): {
25
25
  const tag = astNode.value?.ref?.name
@@ -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, mapToObj } from 'remeda'
4
+ import { isDefined, isTruthy, mapToObj } from 'remeda'
5
5
  import stripIndent from 'strip-indent'
6
6
  import type { Writable } from 'type-fest'
7
7
  import type {
@@ -183,7 +183,7 @@ export class LikeC4ModelParser {
183
183
  const source = this.resolveFqn(coupling.source)
184
184
  const tags = this.convertTags(astNode) ?? this.convertTags(astNode.body)
185
185
  const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
186
- const kind = astNode.kind?.ref?.name as c4.RelationshipKind
186
+ const kind = astNode.kind?.ref?.name as (c4.RelationshipKind | undefined)
187
187
  const astPath = this.getAstNodePath(astNode)
188
188
 
189
189
  const bodyProps = mapToObj(
@@ -354,31 +354,41 @@ export class LikeC4ModelParser {
354
354
  return acc
355
355
  }
356
356
  if (ast.isElementStringProperty(prop)) {
357
- const value = prop.key === 'description' ? removeIndent(prop.value) : toSingleLine(prop.value)
358
- acc.custom[prop.key] = value.trim()
357
+ if (isDefined(prop.value)) {
358
+ let value = prop.key === 'description' ? removeIndent(prop.value) : toSingleLine(prop.value)
359
+ acc.custom[prop.key] = value || ''
360
+ }
359
361
  return acc
360
362
  }
361
363
  if (ast.isIconProperty(prop)) {
362
364
  const value = prop.libicon?.ref?.name ?? prop.value
363
- if (isTruthy(value)) {
365
+ if (isDefined(value)) {
364
366
  acc.custom[prop.key] = value as c4.IconUrl
365
367
  }
366
368
  return acc
367
369
  }
368
370
  if (ast.isColorProperty(prop)) {
369
- acc.custom[prop.key] = prop.value
371
+ if (isDefined(prop.value)) {
372
+ acc.custom[prop.key] = prop.value
373
+ }
370
374
  return acc
371
375
  }
372
376
  if (ast.isShapeProperty(prop)) {
373
- acc.custom[prop.key] = prop.value
377
+ if (isDefined(prop.value)) {
378
+ acc.custom[prop.key] = prop.value
379
+ }
374
380
  return acc
375
381
  }
376
382
  if (ast.isBorderProperty(prop)) {
377
- acc.custom[prop.key] = prop.value
383
+ if (isDefined(prop.value)) {
384
+ acc.custom[prop.key] = prop.value
385
+ }
378
386
  return acc
379
387
  }
380
388
  if (ast.isOpacityProperty(prop)) {
381
- acc.custom[prop.key] = parseAstOpacityProperty(prop)
389
+ if (isDefined(prop.value)) {
390
+ acc.custom[prop.key] = parseAstOpacityProperty(prop)
391
+ }
382
392
  return acc
383
393
  }
384
394
 
@@ -442,22 +452,27 @@ export class LikeC4ModelParser {
442
452
  return props.reduce(
443
453
  (acc, prop) => {
444
454
  if (ast.isRelationStringProperty(prop)) {
445
- const value = removeIndent(prop.value)
446
- if (isTruthy(value)) {
447
- acc.customRelation[prop.key] = value
455
+ if (isDefined(prop.value)) {
456
+ acc.customRelation[prop.key] = removeIndent(prop.value) ?? ''
448
457
  }
449
458
  return acc
450
459
  }
451
460
  if (ast.isArrowProperty(prop)) {
452
- acc.customRelation[prop.key] = prop.value
461
+ if (isTruthy(prop.value)) {
462
+ acc.customRelation[prop.key] = prop.value
463
+ }
453
464
  return acc
454
465
  }
455
466
  if (ast.isColorProperty(prop)) {
456
- acc.customRelation[prop.key] = prop.value
467
+ if (isTruthy(prop.value)) {
468
+ acc.customRelation[prop.key] = prop.value
469
+ }
457
470
  return acc
458
471
  }
459
472
  if (ast.isLineProperty(prop)) {
460
- acc.customRelation[prop.key] = prop.value
473
+ if (isTruthy(prop.value)) {
474
+ acc.customRelation[prop.key] = prop.value
475
+ }
461
476
  return acc
462
477
  }
463
478
  nonexhaustive(prop)
@@ -1,4 +1,5 @@
1
1
  import { invariant, nonexhaustive } from '@likec4/core'
2
+ import { logger } from '@likec4/log'
2
3
  import { Location, Range, TextEdit } from 'vscode-languageserver-types'
3
4
  import { type ParsedLikeC4LangiumDocument } from '../ast'
4
5
  import type { LikeC4ModelLocator } from '../model'
@@ -47,32 +48,36 @@ export class LikeC4ModelChanges {
47
48
  const lspConnection = this.services.shared.lsp.Connection
48
49
  invariant(lspConnection, 'LSP Connection not available')
49
50
  let result: Location | null = null
50
- await this.services.shared.workspace.WorkspaceLock.write(async () => {
51
- const { doc, edits, modifiedRange } = this.convertToTextEdit(changeView)
52
- const textDocument = {
53
- uri: doc.textDocument.uri,
54
- version: doc.textDocument.version
55
- }
56
- if (!edits.length) {
57
- return
58
- }
59
- const applyResult = await lspConnection.workspace.applyEdit({
60
- label: `LikeC4 - change view ${changeView.viewId}`,
61
- edit: {
62
- changes: {
63
- [textDocument.uri]: edits
51
+ try {
52
+ await this.services.shared.workspace.WorkspaceLock.write(async () => {
53
+ const { doc, edits, modifiedRange } = this.convertToTextEdit(changeView)
54
+ const textDocument = {
55
+ uri: doc.textDocument.uri,
56
+ version: doc.textDocument.version
57
+ }
58
+ if (!edits.length) {
59
+ return
60
+ }
61
+ const applyResult = await lspConnection.workspace.applyEdit({
62
+ label: `LikeC4 - change view ${changeView.viewId}`,
63
+ edit: {
64
+ changes: {
65
+ [textDocument.uri]: edits
66
+ }
64
67
  }
68
+ })
69
+ if (!applyResult.applied) {
70
+ lspConnection.window.showErrorMessage(`Failed to apply changes ${applyResult.failureReason}`)
71
+ return
72
+ }
73
+ result = {
74
+ uri: textDocument.uri,
75
+ range: modifiedRange
65
76
  }
66
77
  })
67
- if (!applyResult.applied) {
68
- lspConnection.window.showErrorMessage(`Failed to apply changes ${applyResult.failureReason}`)
69
- return
70
- }
71
- result = {
72
- uri: textDocument.uri,
73
- range: modifiedRange
74
- }
75
- })
78
+ } catch (e) {
79
+ logger.error(`Failed to apply change ${changeView.change.op} ${changeView.viewId}`, e)
80
+ }
76
81
  return result
77
82
  }
78
83
 
@@ -83,7 +88,7 @@ export class LikeC4ModelChanges {
83
88
  } {
84
89
  const lookup = this.locator.locateViewAst(viewId)
85
90
  if (!lookup) {
86
- throw new Error(`View not found: ${viewId}`)
91
+ throw new Error(`LikeC4ModelChanges: view not found: ${viewId}`)
87
92
  }
88
93
  switch (change.op) {
89
94
  case 'change-element-style': {
@@ -1,4 +1,4 @@
1
- import { type Fqn, invariant, isAncestor, type NonEmptyArray, nonNullable, type ViewChanges } from '@likec4/core'
1
+ import { type Fqn, invariant, isAncestor, type NonEmptyArray, nonNullable, type ViewChange } from '@likec4/core'
2
2
  import { GrammarUtils } from 'langium'
3
3
  import { entries, filter, findLast, isTruthy, last } from 'remeda'
4
4
  import { type Range, TextEdit } from 'vscode-languageserver-types'
@@ -8,7 +8,7 @@ import type { LikeC4Services } from '../module'
8
8
 
9
9
  const { findNodeForKeyword, findNodeForProperty } = GrammarUtils
10
10
 
11
- const asViewStyleRule = (target: string, style: ViewChanges.ChangeElementStyle['style'], indent = 0) => {
11
+ const asViewStyleRule = (target: string, style: ViewChange.ChangeElementStyle['style'], indent = 0) => {
12
12
  const indentStr = indent > 0 ? ' '.repeat(indent) : ''
13
13
  return [
14
14
  indentStr + `style ${target} {`,
@@ -24,7 +24,7 @@ type ChangeElementStyleArg = {
24
24
  doc: ParsedLikeC4LangiumDocument
25
25
  viewAst: ast.LikeC4View
26
26
  targets: NonEmptyArray<Fqn>
27
- style: ViewChanges.ChangeElementStyle['style']
27
+ style: ViewChange.ChangeElementStyle['style']
28
28
  }
29
29
 
30
30
  /**
@@ -116,7 +116,7 @@ export function changeElementStyle(services: LikeC4Services, {
116
116
  )
117
117
  modifiedRange.start = {
118
118
  line: insertPos.line + 1,
119
- character: indent + 1
119
+ character: indent
120
120
  }
121
121
  modifiedRange.end = {
122
122
  line: insertPos.line + linesToInsert.length,
@@ -133,18 +133,12 @@ export function changeElementStyle(services: LikeC4Services, {
133
133
  const ruleProp = rule.props.find(p => p.key === key)
134
134
  // replace existing property
135
135
  if (ruleProp && ruleProp.$cstNode) {
136
- const { range: { start, end } } = nonNullable(
137
- findNodeForProperty(ruleProp.$cstNode, 'value'),
138
- 'cant find value cst node'
139
- )
136
+ const { range: { start, end } } = ruleProp.$cstNode
140
137
  includeRange({
141
138
  start,
142
- end: {
143
- line: start.line,
144
- character: start.character + value.length
145
- }
139
+ end
146
140
  })
147
- edits.push(TextEdit.replace({ start, end }, value))
141
+ edits.push(TextEdit.replace({ start, end }, key + ' ' + value))
148
142
  continue
149
143
  }
150
144
  // insert new style property right after the opening brace
@@ -35,7 +35,7 @@ export function changeViewLayout(_services: LikeC4Services, {
35
35
 
36
36
  const insertPos = findNodeForKeyword(viewAst.body.$cstNode, '}')?.range.start
37
37
  invariant(insertPos, 'Closing brace not found')
38
- const insert = `\n autoLayout ${newlayout}\n`
38
+ const insert = ` autoLayout ${newlayout}\n` + ' '.repeat(insertPos.character)
39
39
 
40
40
  return TextEdit.insert(insertPos, insert)
41
41
  }
@@ -1,4 +1,4 @@
1
- import { invariant, type ViewChanges } from '@likec4/core'
1
+ import { invariant, type ViewChange } from '@likec4/core'
2
2
  import indentString from 'indent-string'
3
3
  import { CstUtils, GrammarUtils } from 'langium'
4
4
  import { TextEdit } from 'vscode-languageserver-types'
@@ -12,7 +12,7 @@ export type ManualLayoutArg = {
12
12
  view: ParsedAstView
13
13
  doc: ParsedLikeC4LangiumDocument
14
14
  viewAst: ast.LikeC4View
15
- layout: ViewChanges.SaveManualLayout['layout']
15
+ layout: ViewChange.SaveManualLayout['layout']
16
16
  }
17
17
 
18
18
  export function saveManualLayout(_services: LikeC4Services, {
@@ -1,27 +1,28 @@
1
- import type {
2
- BorderStyle,
3
- ComputedView,
4
- CustomElementExpr as C4CustomElementExpr,
5
- CustomRelationExpr as C4CustomRelationExpr,
6
- Element,
7
- ElementExpression as C4ElementExpression,
8
- ElementKind,
9
- ElementShape,
10
- ElementWhereExpr,
11
- Expression as C4Expression,
12
- Fqn,
13
- Relation,
14
- RelationID,
15
- RelationshipArrowType,
16
- RelationshipLineType,
17
- RelationWhereExpr,
18
- Tag,
19
- ThemeColor,
20
- ViewID,
21
- ViewRule,
22
- ViewRulePredicate,
23
- ViewRuleStyle,
24
- WhereOperator
1
+ import {
2
+ type BorderStyle,
3
+ type ComputedView,
4
+ type CustomElementExpr as C4CustomElementExpr,
5
+ type CustomRelationExpr as C4CustomRelationExpr,
6
+ type Element,
7
+ type ElementExpression as C4ElementExpression,
8
+ type ElementKind,
9
+ type ElementShape,
10
+ type ElementWhereExpr,
11
+ type Expression as C4Expression,
12
+ type Fqn,
13
+ type NonEmptyArray,
14
+ type Relation,
15
+ type RelationID,
16
+ type RelationshipArrowType,
17
+ type RelationshipLineType,
18
+ type RelationWhereExpr,
19
+ type Tag,
20
+ type ThemeColor,
21
+ type ViewID,
22
+ type ViewRule,
23
+ type ViewRulePredicate,
24
+ type ViewRuleStyle,
25
+ type WhereOperator
25
26
  } from '@likec4/core'
26
27
  import { indexBy, isString, map, prop } from 'remeda'
27
28
  import { LikeC4ModelGraph } from '../../LikeC4ModelGraph'
@@ -107,19 +108,26 @@ model {
107
108
  }
108
109
 
109
110
  */
111
+ type TestTag = 'old' | 'next' | 'aws' | 'storage' | 'communication' | 'legacy'
112
+
110
113
  const el = ({
111
114
  id,
112
115
  kind,
113
116
  title,
114
117
  style,
118
+ tags,
115
119
  ...props
116
- }: Partial<Omit<Element, 'id' | 'kind'>> & { id: string; kind: string }): Element => ({
120
+ }: Partial<Omit<Element, 'id' | 'kind' | 'tags'>> & {
121
+ id: string
122
+ kind: string
123
+ tags?: NonEmptyArray<TestTag>
124
+ }): Element => ({
117
125
  id: id as Fqn,
118
126
  kind: kind as ElementKind,
119
127
  title: title ?? id,
120
128
  description: null,
121
129
  technology: null,
122
- tags: null,
130
+ tags: tags as NonEmptyArray<Tag> ?? null,
123
131
  links: null,
124
132
  style: {
125
133
  ...style
@@ -144,7 +152,7 @@ export const fakeElements = {
144
152
  id: 'cloud',
145
153
  kind: 'system',
146
154
  title: 'cloud',
147
- tags: ['next' as Tag, 'old' as Tag]
155
+ tags: ['next', 'old']
148
156
  }),
149
157
  'cloud.backend': el({
150
158
  id: 'cloud.backend',
@@ -171,32 +179,32 @@ export const fakeElements = {
171
179
  id: 'cloud.backend.storage',
172
180
  kind: 'component',
173
181
  title: 'storage',
174
- tags: ['old' as Tag]
182
+ tags: ['storage', 'old']
175
183
  }),
176
184
  'cloud.frontend.adminPanel': el({
177
185
  id: 'cloud.frontend.adminPanel',
178
186
  kind: 'component',
179
187
  title: 'adminPanel',
180
- tags: ['old' as Tag]
188
+ tags: ['old']
181
189
  }),
182
190
  'cloud.frontend.dashboard': el({
183
191
  id: 'cloud.frontend.dashboard',
184
192
  kind: 'component',
185
193
  title: 'dashboard',
186
- tags: ['next' as Tag]
194
+ tags: ['next']
187
195
  }),
188
196
  'amazon': el({
189
197
  id: 'amazon',
190
198
  kind: 'system',
191
199
  title: 'amazon',
192
- tags: ['aws' as Tag]
200
+ tags: ['aws']
193
201
  }),
194
202
  'amazon.s3': el({
195
203
  id: 'amazon.s3',
196
204
  kind: 'component',
197
205
  title: 's3',
198
206
  shape: 'storage',
199
- tags: ['aws' as Tag]
207
+ tags: ['aws', 'storage']
200
208
  })
201
209
  } satisfies Record<string, Element>
202
210
 
@@ -216,6 +224,7 @@ const rel = ({
216
224
  line?: RelationshipLineType
217
225
  head?: RelationshipArrowType
218
226
  tail?: RelationshipArrowType
227
+ tags?: NonEmptyArray<TestTag>
219
228
  }): Relation => ({
220
229
  id: `${source}:${target}` as RelationID,
221
230
  title: title ?? '',
@@ -238,7 +247,8 @@ export const fakeRelations = [
238
247
  rel({
239
248
  source: 'cloud.backend.storage',
240
249
  target: 'amazon.s3',
241
- title: 'uploads'
250
+ title: 'uploads',
251
+ tags: ['aws', 'storage', 'legacy']
242
252
  }),
243
253
  rel({
244
254
  source: 'customer',
@@ -248,7 +258,8 @@ export const fakeRelations = [
248
258
  rel({
249
259
  source: 'cloud.backend.graphql',
250
260
  target: 'cloud.backend.storage',
251
- title: 'stores'
261
+ title: 'stores',
262
+ tags: ['old', 'storage']
252
263
  }),
253
264
  // rel({
254
265
  // source: 'cloud.backend',
@@ -270,7 +281,8 @@ export const fakeRelations = [
270
281
  target: 'cloud.backend.graphql',
271
282
  kind: 'graphlql',
272
283
  title: 'requests',
273
- line: 'solid'
284
+ line: 'solid',
285
+ tags: ['next']
274
286
  }),
275
287
  rel({
276
288
  source: 'cloud.frontend.adminPanel',
@@ -278,29 +290,34 @@ export const fakeRelations = [
278
290
  kind: 'graphlql',
279
291
  title: 'fetches',
280
292
  line: 'dashed',
281
- tail: 'odiamond'
293
+ tail: 'odiamond',
294
+ tags: ['old']
282
295
  }),
283
296
  rel({
284
297
  source: 'cloud',
285
298
  target: 'amazon',
286
299
  title: 'uses',
287
300
  head: 'diamond',
288
- tail: 'odiamond'
301
+ tail: 'odiamond',
302
+ tags: ['aws']
289
303
  }),
290
304
  rel({
291
305
  source: 'cloud.backend',
292
306
  target: 'email',
293
- title: 'schedule'
307
+ title: 'schedule',
308
+ tags: ['communication']
294
309
  }),
295
310
  rel({
296
311
  source: 'cloud',
297
312
  target: 'email',
298
- title: 'uses'
313
+ title: 'uses',
314
+ tags: ['communication']
299
315
  }),
300
316
  rel({
301
317
  source: 'email',
302
318
  target: 'cloud',
303
- title: 'notifies'
319
+ title: 'notifies',
320
+ tags: ['communication']
304
321
  })
305
322
  ]
306
323
 
@@ -380,7 +397,10 @@ export function $customRelation(
380
397
  }
381
398
  }
382
399
 
383
- export function $where(expr: Expression | C4Expression, operator: WhereOperator): ElementWhereExpr | RelationWhereExpr {
400
+ export function $where(
401
+ expr: Expression | C4Expression,
402
+ operator: WhereOperator<TestTag, string>
403
+ ): ElementWhereExpr | RelationWhereExpr {
384
404
  return {
385
405
  where: {
386
406
  expr: $expr(expr) as any,
@@ -5,10 +5,13 @@ import type {
5
5
  Element,
6
6
  ElementPredicateExpression,
7
7
  ElementView,
8
+ NonEmptyArray,
8
9
  Relation,
9
10
  RelationPredicateExpression,
10
11
  RelationshipArrowType,
12
+ RelationshipKind,
11
13
  RelationshipLineType,
14
+ Tag,
12
15
  ThemeColor,
13
16
  ViewRulePredicate
14
17
  } from '@likec4/core'
@@ -193,16 +196,21 @@ export class ComputeCtx {
193
196
  relations: relations.map(r => r.id)
194
197
  }
195
198
 
196
- let relation: Pick<Relation, 'title' | 'description' | 'technology' | 'color' | 'line' | 'head' | 'tail'> | {
197
- // TODO refactor with type-fest
198
- title: string
199
- description?: string | undefined
200
- technology?: string | undefined
201
- color?: ThemeColor | undefined
202
- line?: RelationshipLineType | undefined
203
- head?: RelationshipArrowType | undefined
204
- tail?: RelationshipArrowType | undefined
205
- } | undefined
199
+ let relation:
200
+ | Pick<Relation, 'title' | 'kind' | 'description' | 'technology' | 'color' | 'line' | 'head' | 'tail' | 'tags'>
201
+ | {
202
+ // TODO refactor with type-fest
203
+ title: string
204
+ description?: string | undefined
205
+ technology?: string | undefined
206
+ kind?: RelationshipKind | undefined
207
+ color?: ThemeColor | undefined
208
+ line?: RelationshipLineType | undefined
209
+ head?: RelationshipArrowType | undefined
210
+ tail?: RelationshipArrowType | undefined
211
+ tags?: NonEmptyArray<Tag>
212
+ }
213
+ | undefined
206
214
  if (relations.length === 1) {
207
215
  relation = relations[0]
208
216
  } else {
@@ -217,10 +225,13 @@ export class ComputeCtx {
217
225
  if (r.color && acc.color !== r.color) {
218
226
  acc.color = undefined
219
227
  }
220
- if (r.head && acc.head !== r.kind) {
228
+ if (r.kind && acc.kind !== r.kind) {
229
+ acc.kind = undefined
230
+ }
231
+ if (r.head && acc.head !== r.head) {
221
232
  acc.head = undefined
222
233
  }
223
- if (r.tail && acc.tail !== r.kind) {
234
+ if (r.tail && acc.tail !== r.tail) {
224
235
  acc.tail = undefined
225
236
  }
226
237
  if (r.line && acc.line !== r.line) {
@@ -240,32 +251,27 @@ export class ComputeCtx {
240
251
  title: first(flatMap(relations, r => isTruthy(r.title) ? r.title : [])) ?? '[...]',
241
252
  description: first(flatMap(relations, r => isTruthy(r.description) ? r.description : [])),
242
253
  technology: first(flatMap(relations, r => isTruthy(r.technology) ? r.technology : [])),
254
+ kind: first(flatMap(relations, r => isTruthy(r.kind) ? r.kind : [])),
243
255
  head: first(flatMap(relations, r => isTruthy(r.head) ? r.head : [])),
244
256
  tail: first(flatMap(relations, r => isTruthy(r.tail) ? r.tail : [])),
245
257
  color: first(flatMap(relations, r => isTruthy(r.color) ? r.color : [])),
246
258
  line: first(flatMap(relations, r => isTruthy(r.line) ? r.line : []))
247
259
  })
248
- // return Object.assign(
249
- // edge,
250
- // isTruthy(shared.title) && { label: shared.title },
251
- // isTruthy(shared.description) && { description: shared.description },
252
- // isTruthy(shared.technology) && { technology: shared.technology },
253
- // shared.color && { color: shared.color },
254
- // shared.line && { line: shared.line },
255
- // shared.head && { head: shared.head },
256
- // shared.tail && { tail: shared.tail }
257
- // )
258
260
  }
259
261
 
262
+ const tags = unique(flatMap(relations, r => r.tags ?? []))
263
+
260
264
  return Object.assign(
261
265
  edge,
262
266
  isTruthy(relation.title) && { label: relation.title },
263
267
  isTruthy(relation.description) && { description: relation.description },
264
268
  isTruthy(relation.technology) && { description: relation.technology },
269
+ isTruthy(relation.kind) && { kind: relation.kind },
265
270
  relation.color && { color: relation.color },
266
271
  relation.line && { line: relation.line },
267
272
  relation.head && { head: relation.head },
268
- relation.tail && { tail: relation.tail }
273
+ relation.tail && { tail: relation.tail },
274
+ hasAtLeast(tags, 1) && { tags }
269
275
  )
270
276
  })
271
277
  }
@@ -3,9 +3,11 @@ import type {
3
3
  ComputedEdge,
4
4
  DynamicView,
5
5
  Element,
6
+ NonEmptyArray,
6
7
  RelationID,
7
8
  RelationshipArrowType,
8
9
  RelationshipLineType,
10
+ Tag,
9
11
  ThemeColor
10
12
  } from '@likec4/core'
11
13
  import {
@@ -20,7 +22,7 @@ import {
20
22
  parentFqn,
21
23
  StepEdgeId
22
24
  } from '@likec4/core'
23
- import { isTruthy, unique } from 'remeda'
25
+ import { hasAtLeast, isTruthy, map, unique } from 'remeda'
24
26
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
25
27
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
26
28
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
@@ -41,6 +43,7 @@ export namespace DynamicViewComputeCtx {
41
43
  tail?: RelationshipArrowType
42
44
  relations: RelationID[]
43
45
  isBackward: boolean
46
+ tags?: NonEmptyArray<Tag>
44
47
  }
45
48
  }
46
49
 
@@ -81,15 +84,16 @@ export class DynamicViewComputeCtx {
81
84
  this.explicits.add(source)
82
85
  this.explicits.add(target)
83
86
 
84
- const { title, relations } = this.findRelations(source, target)
87
+ const { title, relations, tags } = this.findRelations(source, target)
85
88
 
86
89
  this.steps.push({
87
90
  ...step,
88
91
  source,
89
92
  target,
90
93
  title: isTruthy(stepTitle) ? stepTitle : title,
91
- relations,
92
- isBackward: isBackward ?? false
94
+ relations: relations ?? [],
95
+ isBackward: isBackward ?? false,
96
+ ...(tags ? { tags } : {})
93
97
  })
94
98
  }
95
99
 
@@ -168,12 +172,20 @@ export class DynamicViewComputeCtx {
168
172
 
169
173
  private findRelations(source: Element, target: Element): {
170
174
  title: string | null
171
- relations: RelationID[]
175
+ tags: NonEmptyArray<Tag> | null
176
+ relations: NonEmptyArray<RelationID> | null
172
177
  } {
173
- const relationships = this.graph.edgesBetween(source, target).flatMap(e => e.relations)
174
- const relations = unique(relationships.map(r => r.id))
178
+ const relationships = unique(this.graph.edgesBetween(source, target).flatMap(e => e.relations))
179
+ const alltags = unique(relationships.flatMap(r => r.tags ?? []))
180
+ const tags = hasAtLeast(alltags, 1) ? alltags : null
181
+
182
+ const relations = hasAtLeast(relationships, 1) ? map(relationships, r => r.id) : null
175
183
  if (relationships.length === 0) {
176
- return { title: null, relations }
184
+ return {
185
+ title: null,
186
+ tags,
187
+ relations
188
+ }
177
189
  }
178
190
  let relation
179
191
  if (relationships.length === 1) {
@@ -185,6 +197,7 @@ export class DynamicViewComputeCtx {
185
197
  if (relation && isTruthy(relation.title)) {
186
198
  return {
187
199
  title: relation.title,
200
+ tags,
188
201
  relations
189
202
  }
190
203
  }
@@ -195,12 +208,14 @@ export class DynamicViewComputeCtx {
195
208
  if (labels.length === 1) {
196
209
  return {
197
210
  title: labels[0]!,
211
+ tags,
198
212
  relations
199
213
  }
200
214
  }
201
215
 
202
216
  return {
203
217
  title: null,
218
+ tags,
204
219
  relations
205
220
  }
206
221
  }
@@ -1,6 +1,6 @@
1
1
  import type { ComputedNode, ViewRule } from '@likec4/core'
2
2
  import { Expr, nonNullable } from '@likec4/core'
3
- import { isEmpty, isNonNullish, pickBy } from 'remeda'
3
+ import { isEmpty, isNonNullish, isNullish, omitBy, pickBy } from 'remeda'
4
4
  import { elementExprToPredicate } from './elementExpressionToPredicate'
5
5
 
6
6
  export function applyCustomElementProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
@@ -14,13 +14,12 @@ export function applyCustomElementProperties(_rules: ViewRule[], _nodes: Compute
14
14
  custom: { expr, ...props }
15
15
  } of rules
16
16
  ) {
17
+ const { border, opacity, ...rest } = omitBy(props, isNullish)
17
18
  const satisfies = elementExprToPredicate(expr)
18
19
  nodes.forEach((node, i) => {
19
20
  if (!satisfies(node)) {
20
21
  return
21
22
  }
22
-
23
- const { border, opacity, ...rest } = pickBy(props, isNonNullish)
24
23
  if (!isEmpty(rest)) {
25
24
  node = {
26
25
  ...node,
@@ -1,6 +1,6 @@
1
1
  import type { ComputedEdge, ComputedNode, Element, ViewRule } from '@likec4/core'
2
2
  import { Expr, nonexhaustive } from '@likec4/core'
3
- import { isEmpty } from 'remeda'
3
+ import { isEmpty, isNullish, omitBy } from 'remeda'
4
4
  import { elementExprToPredicate } from './elementExpressionToPredicate'
5
5
 
6
6
  function relationExpressionToPredicates(
@@ -46,27 +46,11 @@ export function applyCustomRelationProperties(
46
46
  }
47
47
  for (
48
48
  const {
49
- customRelation: { relation, title, ...props }
49
+ customRelation: { relation, title, ...customprops }
50
50
  } of rules
51
51
  ) {
52
- if (isEmpty(props) && !title) {
53
- continue
54
- }
52
+ const props = omitBy(customprops, isNullish)
55
53
  const satisfies = relationExpressionToPredicates(relation)
56
- // const isSource = elementExprToPredicate(relation.source)
57
- // const isTarget = elementExprToPredicate(relation.target)
58
- // const satisfies = (edge: ComputedEdge) => {
59
- // const source = nodes.find(n => n.id === edge.source)
60
- // const target = nodes.find(n => n.id === edge.target)
61
- // if (!source || !target) {
62
- // return false
63
- // }
64
- // let result = isSource(source) && isTarget(target)
65
- // if (!result && relation.isBidirectional) {
66
- // result = isSource(target) && isTarget(source)
67
- // }
68
- // return result
69
- // }
70
54
  edges.forEach((edge, i) => {
71
55
  const source = nodes.find(n => n.id === edge.source)
72
56
  const target = nodes.find(n => n.id === edge.target)
package/src/protocol.ts CHANGED
@@ -5,7 +5,7 @@ import type {
5
5
  LikeC4ComputedModel,
6
6
  LikeC4Model,
7
7
  RelationID,
8
- ViewChangeOp,
8
+ ViewChange,
9
9
  ViewID
10
10
  } from '@likec4/core'
11
11
  import { NotificationType, RequestType, RequestType0 } from 'vscode-jsonrpc'
@@ -53,20 +53,9 @@ export const locate = new RequestType<LocateParams, Location | null, void>('like
53
53
  export type LocateRequest = typeof locate
54
54
  // #endregion
55
55
 
56
- export namespace ChangeView {
57
- export interface ChangeAutoLayout {
58
- op: 'change-autolayout'
59
- layout: AutoLayoutDirection
60
- }
61
- }
62
-
63
- export type ChangeView =
64
- | ChangeView.ChangeAutoLayout
65
- | ViewChangeOp
66
-
67
56
  export interface ChangeViewRequestParams {
68
57
  viewId: ViewID
69
- change: ChangeView
58
+ change: ViewChange
70
59
  }
71
60
  export const changeView = new RequestType<ChangeViewRequestParams, Location | null, void>('likec4/change-view')
72
61
  export type ChangeViewRequest = typeof changeView
@@ -85,6 +85,9 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
85
85
  override getScope(context: ReferenceInfo): Scope {
86
86
  try {
87
87
  const referenceType = this.reflection.getReferenceType(context)
88
+ if (referenceType !== ast.Element) {
89
+ return this.getGlobalScope(referenceType)
90
+ }
88
91
  try {
89
92
  const container = context.container
90
93
  if (ast.isFqnElementRef(container) && context.property === 'el') {
@@ -102,7 +105,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
102
105
  }
103
106
  return this.computeScope(context)
104
107
  } catch (e) {
105
- logger.error(e)
108
+ logger.warn(e)
106
109
  return this.getGlobalScope(referenceType)
107
110
  }
108
111
  } catch (e) {