@likec4/language-server 1.2.2 → 1.3.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 (36) hide show
  1. package/package.json +19 -8
  2. package/src/ast.ts +2 -0
  3. package/src/generated/ast.ts +157 -123
  4. package/src/generated/grammar.ts +2 -2
  5. package/src/generated/module.ts +1 -1
  6. package/src/like-c4.langium +53 -34
  7. package/src/logger.ts +21 -7
  8. package/src/lsp/CompletionProvider.ts +7 -0
  9. package/src/lsp/SemanticTokenProvider.ts +78 -17
  10. package/src/lsp/index.ts +1 -0
  11. package/src/model/model-builder.ts +3 -39
  12. package/src/model/model-parser.ts +19 -4
  13. package/src/model-change/ModelChanges.ts +58 -53
  14. package/src/model-change/changeElementStyle.ts +5 -6
  15. package/src/model-change/saveManualLayout.ts +43 -0
  16. package/src/model-graph/LikeC4ModelGraph.ts +304 -0
  17. package/src/model-graph/compute-view/__test__/fixture.ts +438 -0
  18. package/src/model-graph/compute-view/compute.ts +430 -0
  19. package/src/model-graph/compute-view/index.ts +33 -0
  20. package/src/model-graph/compute-view/predicates.ts +404 -0
  21. package/src/model-graph/dynamic-view/__test__/fixture.ts +56 -0
  22. package/src/model-graph/dynamic-view/compute.ts +198 -0
  23. package/src/model-graph/dynamic-view/index.ts +29 -0
  24. package/src/model-graph/index.ts +3 -0
  25. package/src/model-graph/utils/applyElementCustomProperties.ts +49 -0
  26. package/src/model-graph/utils/applyViewRuleStyles.ts +68 -0
  27. package/src/model-graph/utils/buildComputeNodes.ts +61 -0
  28. package/src/model-graph/utils/sortNodes.ts +105 -0
  29. package/src/module.ts +3 -0
  30. package/src/protocol.ts +3 -18
  31. package/src/references/scope-computation.ts +29 -11
  32. package/src/references/scope-provider.ts +22 -16
  33. package/src/validation/view.ts +9 -4
  34. package/src/view-utils/manual-layout.ts +93 -0
  35. package/contrib/likec4.monarch.ts +0 -41
  36. package/src/lsp/DocumentLinkProvider.test.ts +0 -66
@@ -6,34 +6,35 @@ import type { LikeC4Services } from '../module'
6
6
  import type { ChangeViewRequestParams } from '../protocol'
7
7
  import { changeElementStyle } from './changeElementStyle'
8
8
  import { changeViewLayout } from './changeViewLayout'
9
+ import { saveManualLayout } from './saveManualLayout'
9
10
 
10
- function unionRangeOfAllEdits(ranges: Range[]): Range {
11
- let startLine = Number.MAX_SAFE_INTEGER
12
- let endLine = Number.MIN_SAFE_INTEGER
11
+ // function unionRangeOfAllEdits(ranges: Range[]): Range {
12
+ // let startLine = Number.MAX_SAFE_INTEGER
13
+ // let endLine = Number.MIN_SAFE_INTEGER
13
14
 
14
- let startCharacter = Number.MAX_SAFE_INTEGER
15
- let endCharacter = Number.MIN_SAFE_INTEGER
15
+ // let startCharacter = Number.MAX_SAFE_INTEGER
16
+ // let endCharacter = Number.MIN_SAFE_INTEGER
16
17
 
17
- for (const { start, end } of ranges) {
18
- if (start.line <= startLine) {
19
- if (startLine == start.line) {
20
- startCharacter = Math.min(start.character, startCharacter)
21
- } else {
22
- startLine = start.line
23
- startCharacter = start.character
24
- }
25
- }
26
- if (endLine <= end.line) {
27
- if (endLine == end.line) {
28
- endCharacter = Math.max(end.character, endCharacter)
29
- } else {
30
- endLine = end.line
31
- endCharacter = end.character
32
- }
33
- }
34
- }
35
- return Range.create(startLine, startCharacter, endLine, endCharacter)
36
- }
18
+ // for (const { start, end } of ranges) {
19
+ // if (start.line <= startLine) {
20
+ // if (startLine == start.line) {
21
+ // startCharacter = Math.min(start.character, startCharacter)
22
+ // } else {
23
+ // startLine = start.line
24
+ // startCharacter = start.character
25
+ // }
26
+ // }
27
+ // if (endLine <= end.line) {
28
+ // if (endLine == end.line) {
29
+ // endCharacter = Math.max(end.character, endCharacter)
30
+ // } else {
31
+ // endLine = end.line
32
+ // endCharacter = end.character
33
+ // }
34
+ // }
35
+ // }
36
+ // return Range.create(startLine, startCharacter, endLine, endCharacter)
37
+ // }
37
38
 
38
39
  export class LikeC4ModelChanges {
39
40
  private locator: LikeC4ModelLocator
@@ -47,7 +48,7 @@ export class LikeC4ModelChanges {
47
48
  invariant(lspConnection, 'LSP Connection not available')
48
49
  let result: Location | null = null
49
50
  await this.services.shared.workspace.WorkspaceLock.write(async () => {
50
- const { doc, edits } = this.convertToTextEdit(changeView)
51
+ const { doc, edits, modifiedRange } = this.convertToTextEdit(changeView)
51
52
  const textDocument = {
52
53
  uri: doc.textDocument.uri,
53
54
  version: doc.textDocument.version
@@ -69,52 +70,56 @@ export class LikeC4ModelChanges {
69
70
  }
70
71
  result = {
71
72
  uri: textDocument.uri,
72
- range: unionRangeOfAllEdits(edits.map(edit => edit.range))
73
+ range: modifiedRange
73
74
  }
74
75
  })
75
76
  return result
76
77
  }
77
78
 
78
- protected convertToTextEdit({ viewId, changes }: ChangeViewRequestParams): {
79
+ protected convertToTextEdit({ viewId, change }: ChangeViewRequestParams): {
79
80
  doc: ParsedLikeC4LangiumDocument
80
- ranges: Range[]
81
+ modifiedRange: Range
81
82
  edits: TextEdit[]
82
83
  } {
83
84
  const lookup = this.locator.locateViewAst(viewId)
84
85
  if (!lookup) {
85
86
  throw new Error(`View not found: ${viewId}`)
86
87
  }
87
- const ranges = [] as Range[]
88
- const edits = [] as TextEdit[]
89
- for (const change of changes) {
90
- switch (change.op) {
91
- case 'change-element-style': {
92
- const { edits: elementEdits, modifiedRange } = changeElementStyle(this.services, {
88
+ switch (change.op) {
89
+ case 'change-element-style': {
90
+ return {
91
+ doc: lookup.doc,
92
+ ...changeElementStyle(this.services, {
93
93
  ...lookup,
94
94
  targets: change.targets,
95
95
  style: change.style
96
96
  })
97
- ranges.push(modifiedRange)
98
- edits.push(...elementEdits)
99
- break
100
97
  }
101
- case 'change-autolayout': {
102
- const edit = changeViewLayout(this.services, {
103
- ...lookup,
104
- layout: change.layout
105
- })
106
- edits.push(edit)
107
- ranges.push(edit.range)
108
- break
98
+ }
99
+ case 'change-autolayout': {
100
+ const edit = changeViewLayout(this.services, {
101
+ ...lookup,
102
+ layout: change.layout
103
+ })
104
+ return {
105
+ doc: lookup.doc,
106
+ modifiedRange: edit.range,
107
+ edits: [edit]
109
108
  }
110
- default:
111
- nonexhaustive(change)
112
109
  }
113
- }
114
- return {
115
- doc: lookup.doc,
116
- ranges,
117
- edits
110
+ case 'save-manual-layout':
111
+ const edit = saveManualLayout(this.services, {
112
+ ...lookup,
113
+ nodes: change.nodes,
114
+ edges: change.edges
115
+ })
116
+ return {
117
+ doc: lookup.doc,
118
+ modifiedRange: edit.range,
119
+ edits: [edit]
120
+ }
121
+ default:
122
+ nonexhaustive(change)
118
123
  }
119
124
  }
120
125
  }
@@ -1,15 +1,14 @@
1
- import { type Fqn, invariant, isAncestor, type NonEmptyArray, nonNullable } from '@likec4/core'
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, isNumber, last, partition, toPairs } from 'remeda'
3
+ import { entries, filter, findLast, last } from 'remeda'
4
4
  import { type Range, TextEdit } from 'vscode-languageserver-protocol'
5
- import { ast, type ParsedAstElementView, type ParsedAstView, type ParsedLikeC4LangiumDocument } from '../ast'
5
+ import { ast, type ParsedAstView, type ParsedLikeC4LangiumDocument } from '../ast'
6
6
  import type { FqnIndex } from '../model'
7
7
  import type { LikeC4Services } from '../module'
8
- import type { ChangeView } from '../protocol'
9
8
 
10
9
  const { findNodeForKeyword, findNodeForProperty } = GrammarUtils
11
10
 
12
- const asViewStyleRule = (target: string, style: ChangeView.ChangeElementStyle['style'], indent = 0) => {
11
+ const asViewStyleRule = (target: string, style: ViewChanges.ChangeElementStyle['style'], indent = 0) => {
13
12
  const indentStr = indent > 0 ? ' '.repeat(indent) : ''
14
13
  return [
15
14
  indentStr + `style ${target} {`,
@@ -25,7 +24,7 @@ type ChangeElementStyleArg = {
25
24
  doc: ParsedLikeC4LangiumDocument
26
25
  viewAst: ast.LikeC4View
27
26
  targets: NonEmptyArray<Fqn>
28
- style: ChangeView.ChangeElementStyle['style']
27
+ style: ViewChanges.ChangeElementStyle['style']
29
28
  }
30
29
 
31
30
  /**
@@ -0,0 +1,43 @@
1
+ import { invariant, type ViewChanges } from '@likec4/core'
2
+ import indentString from 'indent-string'
3
+ import { CstUtils, GrammarUtils } from 'langium'
4
+ import { TextEdit } from 'vscode-languageserver-protocol'
5
+ import { ast, type ParsedAstView, type ParsedLikeC4LangiumDocument } from '../ast'
6
+ import type { LikeC4Services } from '../module'
7
+ import { serializeToComment } from '../view-utils/manual-layout'
8
+
9
+ const { findNodeForProperty } = GrammarUtils
10
+
11
+ export type ManualLayoutArg = {
12
+ view: ParsedAstView
13
+ doc: ParsedLikeC4LangiumDocument
14
+ viewAst: ast.LikeC4View
15
+ nodes: ViewChanges.SaveManualLayout['nodes']
16
+ edges: ViewChanges.SaveManualLayout['edges']
17
+ }
18
+
19
+ export function saveManualLayout(_services: LikeC4Services, {
20
+ viewAst,
21
+ nodes,
22
+ edges
23
+ }: ManualLayoutArg): TextEdit {
24
+ invariant(viewAst.$cstNode, 'invalid view.$cstNode')
25
+ const commentCst = CstUtils.findCommentNode(viewAst.$cstNode, ['BLOCK_COMMENT'])
26
+ let txt = serializeToComment({ nodes, edges })
27
+ if (viewAst.$cstNode.range.start.character > 0) {
28
+ txt = indentString(txt, viewAst.$cstNode.range.start.character)
29
+ // const indent = ' '.repeat(viewAst.$cstNode.range.start.character)
30
+ // txt = txt.split('\n').map(l => indent + l).join('\n')
31
+ }
32
+ if (commentCst) {
33
+ // Do not indent the first line
34
+ return TextEdit.replace(commentCst.range, txt.trimStart())
35
+ }
36
+ return TextEdit.insert(
37
+ {
38
+ line: viewAst.$cstNode.range.start.line,
39
+ character: 0
40
+ },
41
+ txt + '\n'
42
+ )
43
+ }
@@ -0,0 +1,304 @@
1
+ import {
2
+ ancestorsFqn,
3
+ commonAncestor,
4
+ type Element,
5
+ type Fqn,
6
+ InvalidModelError,
7
+ invariant,
8
+ isSameHierarchy,
9
+ isString,
10
+ parentFqn,
11
+ type Relation,
12
+ type RelationID
13
+ } from '@likec4/core'
14
+ import { filter, isIncludedIn } from 'remeda'
15
+
16
+ function intersection<T>(source: ReadonlyArray<T>, other: ReadonlyArray<T>): Array<T> {
17
+ return filter(source, isIncludedIn(other))
18
+ }
19
+
20
+ type Params = {
21
+ elements: Record<Fqn, Element>
22
+ relations: Record<RelationID, Relation>
23
+ // views: ElementView[]
24
+ }
25
+
26
+ type RelationEdge = {
27
+ source: Element
28
+ target: Element
29
+ relations: Relation[]
30
+ }
31
+
32
+ type FqnOrElement = Fqn | Element
33
+ type FqnsOrElements = Fqn[] | Element[]
34
+
35
+ export class LikeC4ModelGraph {
36
+ #elements = new Map<Fqn, Element>()
37
+ #children = new Map<Fqn, Fqn[]>()
38
+ #rootElements = new Set<Element>()
39
+
40
+ #relations = new Map<RelationID, Relation>()
41
+ // Incoming to an element or its descendants
42
+ #incoming = new Map<Fqn, RelationID[]>()
43
+ // Outgoing from an element or its descendants
44
+ #outgoing = new Map<Fqn, RelationID[]>()
45
+ // Relationships inside the element descendants
46
+ #internal = new Map<Fqn, RelationID[]>()
47
+
48
+ constructor({ elements, relations }: Params) {
49
+ for (const el of Object.values(elements)) {
50
+ this.addElement(el)
51
+ }
52
+ for (const rel of Object.values(relations)) {
53
+ this.addRelation(rel)
54
+ }
55
+ }
56
+
57
+ get rootElements() {
58
+ return [...this.#rootElements]
59
+ }
60
+
61
+ get elements() {
62
+ return [...this.#elements.values()]
63
+ }
64
+
65
+ public element(id: Fqn) {
66
+ const el = this.#elements.get(id)
67
+ invariant(el, `Element ${id} not found`)
68
+ return el
69
+ }
70
+
71
+ public incoming(id: Fqn) {
72
+ return this._incomingTo(id).flatMap(id => this.#relations.get(id) ?? [])
73
+ }
74
+
75
+ public outgoing(id: Fqn) {
76
+ return this._outgoingFrom(id).flatMap(id => this.#relations.get(id) ?? [])
77
+ }
78
+
79
+ public internal(id: Fqn) {
80
+ return this._internalOf(id).flatMap(id => this.#relations.get(id) ?? [])
81
+ }
82
+
83
+ public children(id: Fqn) {
84
+ return this._childrenOf(id).flatMap(id => this.#elements.get(id) ?? [])
85
+ }
86
+
87
+ // Get children or element itself if no children
88
+ public childrenOrElement(id: Fqn) {
89
+ const children = this.children(id)
90
+ return children.length > 0 ? children : [this.element(id)]
91
+ }
92
+
93
+ // Get all sibling (i.e. same parent)
94
+ public siblings(element: Fqn | Element) {
95
+ const id = isString(element) ? element : element.id
96
+ const parent = parentFqn(id)
97
+ const fqns = parent ? this._childrenOf(parent) : [...this.#rootElements].map(e => e.id)
98
+ return fqns.flatMap(fqn => (fqn !== id && this.#elements.get(fqn)) || [])
99
+ }
100
+
101
+ public ancestors(element: Fqn | Element) {
102
+ const id = isString(element) ? element : element.id
103
+ return ancestorsFqn(id).flatMap(id => this.#elements.get(id) ?? [])
104
+ }
105
+
106
+ /**
107
+ * Resolve siblings of the element and its ancestors
108
+ */
109
+ public ascendingSiblings(element: Fqn | Element) {
110
+ const id = isString(element) ? element : element.id
111
+ return [...this.siblings(id), ...this.ancestors(id).flatMap(a => this.siblings(a.id))]
112
+ }
113
+
114
+ /**
115
+ * Resolve all RelationEdges between element and others (any direction)
116
+ */
117
+ public anyEdgesBetween(_element: Fqn | Element, others: Fqn[] | Element[]): RelationEdge[] {
118
+ if (others.length === 0) {
119
+ return []
120
+ }
121
+ const element = isString(_element) ? this.element(_element) : _element
122
+ const in_element = this._incomingTo(element.id)
123
+ const element_out = this._outgoingFrom(element.id)
124
+ if (in_element.length === 0 && element_out.length === 0) {
125
+ return []
126
+ }
127
+
128
+ const result = [] as Array<RelationEdge>
129
+ for (const _other of others) {
130
+ const other = isString(_other) ? this.element(_other) : _other
131
+ if (isSameHierarchy(element, other)) {
132
+ continue
133
+ }
134
+
135
+ if (element_out.length > 0) {
136
+ const in_other = this._incomingTo(other.id)
137
+ const outcoming = intersection(element_out, in_other)
138
+ if (outcoming.length > 0) {
139
+ result.push({
140
+ source: element,
141
+ target: other,
142
+ relations: outcoming.flatMap(id => this.#relations.get(id) ?? [])
143
+ })
144
+ }
145
+ }
146
+
147
+ if (in_element.length > 0) {
148
+ const other_out = this._outgoingFrom(other.id)
149
+ const incoming = intersection(other_out, in_element)
150
+ if (incoming.length > 0) {
151
+ result.push({
152
+ source: other,
153
+ target: element,
154
+ relations: incoming.flatMap(id => this.#relations.get(id) ?? [])
155
+ })
156
+ }
157
+ }
158
+ }
159
+ return result
160
+ }
161
+
162
+ /**
163
+ * Resolve all RelationEdges between elements (any direction)
164
+ */
165
+ public edgesWithin<T extends Fqn[] | Element[]>(elements: T): RelationEdge[] {
166
+ if (elements.length < 2) {
167
+ return []
168
+ }
169
+ return elements.reduce((acc, el, index, array) => {
170
+ // return acc if last element
171
+ if (index === array.length - 1) {
172
+ return acc
173
+ }
174
+ acc.push(...this.anyEdgesBetween(el, array.slice(index + 1) as T))
175
+ return acc
176
+ }, [] as RelationEdge[])
177
+ }
178
+
179
+ /**
180
+ * Get directed RelationEdge between source and target if exists
181
+ */
182
+ public edgesBetween(
183
+ _sources: FqnOrElement | FqnsOrElements,
184
+ _targets: FqnOrElement | FqnsOrElements
185
+ ) {
186
+ const sources = Array.isArray(_sources) ? _sources : [_sources]
187
+ const targets = Array.isArray(_targets) ? _targets : [_targets]
188
+ if (sources.length === 0 || targets.length === 0) {
189
+ return []
190
+ }
191
+
192
+ const result = [] as Array<RelationEdge>
193
+ for (const _source of sources) {
194
+ const source = isString(_source) ? this.element(_source) : _source
195
+ const outcoming = this._outgoingFrom(source.id)
196
+ if (outcoming.length === 0) {
197
+ continue
198
+ }
199
+
200
+ for (const _target of targets) {
201
+ const target = isString(_target) ? this.element(_target) : _target
202
+ if (isSameHierarchy(source, target)) {
203
+ continue
204
+ }
205
+ const incoming = this._incomingTo(target.id)
206
+ if (incoming.length === 0) {
207
+ continue
208
+ }
209
+
210
+ const relations = intersection(outcoming, incoming).flatMap(
211
+ id => this.#relations.get(id) ?? []
212
+ )
213
+ if (relations.length > 0) {
214
+ result.push({
215
+ source,
216
+ target,
217
+ relations
218
+ })
219
+ }
220
+ }
221
+ }
222
+ return result
223
+ }
224
+
225
+ private addElement(el: Element) {
226
+ if (this.#elements.has(el.id)) {
227
+ throw new InvalidModelError(`Element ${el.id} already exists`)
228
+ }
229
+ this.#elements.set(el.id, el)
230
+ const parent = parentFqn(el.id)
231
+ if (parent) {
232
+ this._childrenOf(parent).push(el.id)
233
+ } else {
234
+ this.#rootElements.add(el)
235
+ }
236
+ }
237
+
238
+ private addRelation(rel: Relation) {
239
+ if (this.#relations.has(rel.id)) {
240
+ throw new InvalidModelError(`Relation ${rel.id} already exists`)
241
+ }
242
+ this.#relations.set(rel.id, rel)
243
+ this._incomingTo(rel.target).push(rel.id)
244
+ this._outgoingFrom(rel.source).push(rel.id)
245
+
246
+ const relParent = commonAncestor(rel.source, rel.target)
247
+ // Process internal relationships
248
+ if (relParent) {
249
+ for (const ancestor of [relParent, ...ancestorsFqn(relParent)]) {
250
+ this._internalOf(ancestor).push(rel.id)
251
+ }
252
+ }
253
+ // Process source hierarchy
254
+ for (const sourceAncestor of ancestorsFqn(rel.source)) {
255
+ if (sourceAncestor === relParent) {
256
+ break
257
+ }
258
+ this._outgoingFrom(sourceAncestor).push(rel.id)
259
+ }
260
+ // Process target hierarchy
261
+ for (const targetAncestor of ancestorsFqn(rel.target)) {
262
+ if (targetAncestor === relParent) {
263
+ break
264
+ }
265
+ this._incomingTo(targetAncestor).push(rel.id)
266
+ }
267
+ }
268
+
269
+ private _childrenOf(id: Fqn) {
270
+ let children = this.#children.get(id)
271
+ if (!children) {
272
+ children = []
273
+ this.#children.set(id, children)
274
+ }
275
+ return children
276
+ }
277
+
278
+ private _incomingTo(id: Fqn) {
279
+ let incoming = this.#incoming.get(id)
280
+ if (!incoming) {
281
+ incoming = []
282
+ this.#incoming.set(id, incoming)
283
+ }
284
+ return incoming
285
+ }
286
+
287
+ private _outgoingFrom(id: Fqn) {
288
+ let outgoing = this.#outgoing.get(id)
289
+ if (!outgoing) {
290
+ outgoing = []
291
+ this.#outgoing.set(id, outgoing)
292
+ }
293
+ return outgoing
294
+ }
295
+
296
+ private _internalOf(id: Fqn) {
297
+ let internal = this.#internal.get(id)
298
+ if (!internal) {
299
+ internal = []
300
+ this.#internal.set(id, internal)
301
+ }
302
+ return internal
303
+ }
304
+ }