@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.
- package/package.json +19 -8
- package/src/ast.ts +2 -0
- package/src/generated/ast.ts +157 -123
- package/src/generated/grammar.ts +2 -2
- package/src/generated/module.ts +1 -1
- package/src/like-c4.langium +53 -34
- package/src/logger.ts +21 -7
- package/src/lsp/CompletionProvider.ts +7 -0
- package/src/lsp/SemanticTokenProvider.ts +78 -17
- package/src/lsp/index.ts +1 -0
- package/src/model/model-builder.ts +3 -39
- package/src/model/model-parser.ts +19 -4
- package/src/model-change/ModelChanges.ts +58 -53
- package/src/model-change/changeElementStyle.ts +5 -6
- package/src/model-change/saveManualLayout.ts +43 -0
- package/src/model-graph/LikeC4ModelGraph.ts +304 -0
- package/src/model-graph/compute-view/__test__/fixture.ts +438 -0
- package/src/model-graph/compute-view/compute.ts +430 -0
- package/src/model-graph/compute-view/index.ts +33 -0
- package/src/model-graph/compute-view/predicates.ts +404 -0
- package/src/model-graph/dynamic-view/__test__/fixture.ts +56 -0
- package/src/model-graph/dynamic-view/compute.ts +198 -0
- package/src/model-graph/dynamic-view/index.ts +29 -0
- package/src/model-graph/index.ts +3 -0
- package/src/model-graph/utils/applyElementCustomProperties.ts +49 -0
- package/src/model-graph/utils/applyViewRuleStyles.ts +68 -0
- package/src/model-graph/utils/buildComputeNodes.ts +61 -0
- package/src/model-graph/utils/sortNodes.ts +105 -0
- package/src/module.ts +3 -0
- package/src/protocol.ts +3 -18
- package/src/references/scope-computation.ts +29 -11
- package/src/references/scope-provider.ts +22 -16
- package/src/validation/view.ts +9 -4
- package/src/view-utils/manual-layout.ts +93 -0
- package/contrib/likec4.monarch.ts +0 -41
- 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
|
-
|
|
12
|
-
|
|
11
|
+
// function unionRangeOfAllEdits(ranges: Range[]): Range {
|
|
12
|
+
// let startLine = Number.MAX_SAFE_INTEGER
|
|
13
|
+
// let endLine = Number.MIN_SAFE_INTEGER
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
// let startCharacter = Number.MAX_SAFE_INTEGER
|
|
16
|
+
// let endCharacter = Number.MIN_SAFE_INTEGER
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
73
|
+
range: modifiedRange
|
|
73
74
|
}
|
|
74
75
|
})
|
|
75
76
|
return result
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
protected convertToTextEdit({ viewId,
|
|
79
|
+
protected convertToTextEdit({ viewId, change }: ChangeViewRequestParams): {
|
|
79
80
|
doc: ParsedLikeC4LangiumDocument
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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,
|
|
3
|
+
import { entries, filter, findLast, last } from 'remeda'
|
|
4
4
|
import { type Range, TextEdit } from 'vscode-languageserver-protocol'
|
|
5
|
-
import { ast, type
|
|
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:
|
|
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:
|
|
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
|
+
}
|