@meltemi/diagrams-edit 1.9.0 → 1.10.17
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/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +3 -0
- package/dist/animate.js.map +1 -1
- package/dist/attachments.d.ts +30 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +85 -0
- package/dist/attachments.js.map +1 -0
- package/dist/c4.d.ts +26 -1
- package/dist/c4.d.ts.map +1 -1
- package/dist/c4.js +321 -27
- package/dist/c4.js.map +1 -1
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +15 -0
- package/dist/context.js.map +1 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +61 -15
- package/dist/create.js.map +1 -1
- package/dist/free.d.ts.map +1 -1
- package/dist/free.js +97 -9
- package/dist/free.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/insert.d.ts +39 -1
- package/dist/insert.d.ts.map +1 -1
- package/dist/insert.js +128 -2
- package/dist/insert.js.map +1 -1
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +7 -9
- package/dist/labels.js.map +1 -1
- package/dist/lines.d.ts +16 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/lines.js +123 -0
- package/dist/lines.js.map +1 -0
- package/dist/messages/en.d.ts +105 -1
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +108 -2
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/es.d.ts.map +1 -1
- package/dist/messages/es.js +69 -2
- package/dist/messages/es.js.map +1 -1
- package/dist/messages/index.d.ts +61 -1
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/pens.d.ts +16 -0
- package/dist/pens.d.ts.map +1 -0
- package/dist/pens.js +175 -0
- package/dist/pens.js.map +1 -0
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +125 -20
- package/dist/plan.js.map +1 -1
- package/dist/relations.d.ts +1 -1
- package/dist/relations.d.ts.map +1 -1
- package/dist/relations.js +35 -16
- package/dist/relations.js.map +1 -1
- package/dist/sequence.d.ts +13 -2
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +342 -31
- package/dist/sequence.js.map +1 -1
- package/dist/structure.d.ts +7 -5
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +49 -49
- package/dist/structure.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +8 -3
- package/dist/styles.js.map +1 -1
- package/dist/tags.d.ts +19 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +159 -0
- package/dist/tags.js.map +1 -0
- package/dist/text.d.ts +4 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +9 -0
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +270 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
package/dist/sequence.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isToken, } from "@meltemi/diagrams-language";
|
|
2
|
+
import { FRAGMENT_BRANCH_KEYWORDS, FRAGMENT_OPERATORS, PARTICIPANT_KINDS, relationNameAnchor, } from "@meltemi/diagrams-model";
|
|
2
3
|
import { insertStatement, reject, requireElement } from "./context.js";
|
|
3
4
|
import { declarationText, usedIds } from "./create.js";
|
|
4
5
|
import { deleteStatements } from "./delete.js";
|
|
5
|
-
import { diagramBody, groupBlock, insertAfter, insertBefore, insertDeclaration, isDeclaration, reindent, verifyNoNewErrors, } from "./insert.js";
|
|
6
|
+
import { commentsAbove, diagramBody, groupBlock, insertAfter, insertBefore, insertBeforeStatement, insertDeclaration, isDeclaration, reindent, verifyNoNewErrors, } from "./insert.js";
|
|
6
7
|
import { reasons } from "./messages/index.js";
|
|
7
8
|
import { insertPlacements, removeListItems, requireNewId } from "./plan.js";
|
|
8
9
|
import { relationDependents } from "./relations.js";
|
|
9
|
-
import { formatNumber, lineIndent, quoteString } from "./text.js";
|
|
10
|
+
import { formatNumber, isCommentTrivia, lineIndent, lineStart, quoteString } from "./text.js";
|
|
10
11
|
import { renameInStories } from "./stories.js";
|
|
11
12
|
const PARTICIPANTS = new Set(PARTICIPANT_KINDS);
|
|
12
13
|
const EVENT_KEYWORDS = new Set([
|
|
@@ -17,6 +18,7 @@ const EVENT_KEYWORDS = new Set([
|
|
|
17
18
|
"note",
|
|
18
19
|
"divider",
|
|
19
20
|
"delay",
|
|
21
|
+
"return",
|
|
20
22
|
...FRAGMENT_OPERATORS,
|
|
21
23
|
]);
|
|
22
24
|
/** Refuses an operation on a diagram that is not a sequence diagram. */
|
|
@@ -49,11 +51,23 @@ function requireFragment(ctx, key) {
|
|
|
49
51
|
reject("edit.unsafe-target", reasons().sequence.notFragment(key));
|
|
50
52
|
return event;
|
|
51
53
|
}
|
|
52
|
-
/** The block
|
|
54
|
+
/** The block of a call: the block on its relation statement. */
|
|
55
|
+
function callBlock(call) {
|
|
56
|
+
const node = call.node;
|
|
57
|
+
return node.kind === "RelationStatement" && node.block?.kind === "Block" ? node.block : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The block that holds an event: the diagram body, a fragment branch or a call's block. */
|
|
53
60
|
function containerOf(ctx, event) {
|
|
54
61
|
const place = event.path[event.path.length - 1];
|
|
55
62
|
if (!place)
|
|
56
63
|
return { block: diagramBody(ctx), owner: ctx.diagram.node.start };
|
|
64
|
+
if ("message" in place) {
|
|
65
|
+
const call = requireEvent(ctx, place.message);
|
|
66
|
+
const block = call.kind === "message" ? callBlock(call) : undefined;
|
|
67
|
+
if (!block)
|
|
68
|
+
reject("edit.unsafe-target", reasons().sequence.noCallBlock(place.message));
|
|
69
|
+
return { block, owner: call.node.start };
|
|
70
|
+
}
|
|
57
71
|
const fragment = requireFragment(ctx, place.fragment);
|
|
58
72
|
return { block: fragment.branches[place.branch].block, owner: fragment.node.start };
|
|
59
73
|
}
|
|
@@ -133,7 +147,12 @@ export function reorderParticipant(ctx, op) {
|
|
|
133
147
|
// ---------------------------------------------------------------------- events
|
|
134
148
|
export function moveEvent(ctx, op) {
|
|
135
149
|
requireSequence(ctx);
|
|
150
|
+
if ((op.direction === undefined) === (op.to === undefined)) {
|
|
151
|
+
reject("edit.invalid-value", reasons().sequence.moveForm);
|
|
152
|
+
}
|
|
136
153
|
const event = requireEvent(ctx, op.event);
|
|
154
|
+
if (op.to !== undefined)
|
|
155
|
+
return moveTo(ctx, event, op.to);
|
|
137
156
|
const statement = event.node;
|
|
138
157
|
const { block } = containerOf(ctx, event);
|
|
139
158
|
const list = siblings(block);
|
|
@@ -146,7 +165,25 @@ export function moveEvent(ctx, op) {
|
|
|
146
165
|
FRAGMENT_OPERATORS.includes(neighbour.keyword.text)
|
|
147
166
|
? [...ctx.diagram.eventsByKey.values()].find((e) => e.node === neighbour)
|
|
148
167
|
: undefined;
|
|
149
|
-
|
|
168
|
+
const call = neighbour.kind === "RelationStatement"
|
|
169
|
+
? [...ctx.diagram.eventsByKey.values()].find((e) => e.node === neighbour)
|
|
170
|
+
: undefined;
|
|
171
|
+
if (call?.kind === "message" && call.events.length > 0) {
|
|
172
|
+
// Into the adjacent call's block: before its trailing `return` going up, so that the
|
|
173
|
+
// return stays last; at its start going down.
|
|
174
|
+
const block = callBlock(call);
|
|
175
|
+
const inner = siblings(block);
|
|
176
|
+
const last = inner[inner.length - 1];
|
|
177
|
+
const returns = last?.kind === "KeywordStatement" && last.keyword.text === "return";
|
|
178
|
+
edits = up
|
|
179
|
+
? returns
|
|
180
|
+
? relocate(ctx, statement, { kind: "before", anchor: last, block })
|
|
181
|
+
: relocate(ctx, statement, { kind: "end", block, owner: call.node.start })
|
|
182
|
+
: inner[0]
|
|
183
|
+
? relocate(ctx, statement, { kind: "before", anchor: inner[0], block })
|
|
184
|
+
: relocate(ctx, statement, { kind: "start", block, owner: call.node.start });
|
|
185
|
+
}
|
|
186
|
+
else if (fragment && fragment.kind === "fragment") {
|
|
150
187
|
// Into the adjacent fragment: the end of its last branch, or the start of its first.
|
|
151
188
|
const branch = up ? fragment.branches[fragment.branches.length - 1] : fragment.branches[0];
|
|
152
189
|
edits = relocate(ctx, statement, {
|
|
@@ -160,10 +197,22 @@ export function moveEvent(ctx, op) {
|
|
|
160
197
|
}
|
|
161
198
|
}
|
|
162
199
|
else {
|
|
163
|
-
// At the edge of a branch: into the neighbouring branch, or out of the fragment
|
|
200
|
+
// At the edge of a branch: into the neighbouring branch, or out of the fragment; at the
|
|
201
|
+
// edge of a call's block, out of the call.
|
|
164
202
|
const place = event.path[event.path.length - 1];
|
|
165
203
|
if (!place)
|
|
166
204
|
return [];
|
|
205
|
+
if ("message" in place) {
|
|
206
|
+
const call = requireEvent(ctx, place.message);
|
|
207
|
+
const outer = containerOf(ctx, call);
|
|
208
|
+
const edits = relocate(ctx, statement, {
|
|
209
|
+
kind: up ? "before" : "after",
|
|
210
|
+
anchor: call.node,
|
|
211
|
+
block: outer.block,
|
|
212
|
+
});
|
|
213
|
+
verifyNoNewErrors(ctx, edits);
|
|
214
|
+
return edits;
|
|
215
|
+
}
|
|
167
216
|
const fragment = requireFragment(ctx, place.fragment);
|
|
168
217
|
const next = fragment.branches[place.branch + (up ? -1 : 1)];
|
|
169
218
|
if (next) {
|
|
@@ -185,6 +234,107 @@ export function moveEvent(ctx, op) {
|
|
|
185
234
|
verifyNoNewErrors(ctx, edits);
|
|
186
235
|
return edits;
|
|
187
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Moves an event's statement right after or right before the statement of another event, in the
|
|
239
|
+
* block that holds that one, at its indentation; checked once on the result. A statement with its
|
|
240
|
+
* lines to itself takes the comment lines right above it and the comment that ends its line.
|
|
241
|
+
*/
|
|
242
|
+
function moveTo(ctx, event, to) {
|
|
243
|
+
const written = to;
|
|
244
|
+
const after = typeof to === "object" && to !== null ? written.after : undefined;
|
|
245
|
+
const before = typeof to === "object" && to !== null ? written.before : undefined;
|
|
246
|
+
const key = after ?? before;
|
|
247
|
+
if ((after === undefined) === (before === undefined) || typeof key !== "string") {
|
|
248
|
+
reject("edit.invalid-value", reasons().sequence.destinationForm);
|
|
249
|
+
}
|
|
250
|
+
const anchor = requireEvent(ctx, key);
|
|
251
|
+
if (anchor === event) {
|
|
252
|
+
reject("edit.unsafe-target", reasons().sequence.moveBesideItself(event.key));
|
|
253
|
+
}
|
|
254
|
+
if (anchor.path.some((p) => ("fragment" in p ? p.fragment : p.message) === event.key)) {
|
|
255
|
+
reject("edit.unsafe-target", reasons().sequence.moveIntoItself(event.key, anchor.key));
|
|
256
|
+
}
|
|
257
|
+
const { block } = containerOf(ctx, anchor);
|
|
258
|
+
const statement = event.node;
|
|
259
|
+
// Already there: nothing to write.
|
|
260
|
+
const at = block.statements.indexOf(anchor.node);
|
|
261
|
+
if (block.statements[after !== undefined ? at + 1 : at - 1] === statement)
|
|
262
|
+
return [];
|
|
263
|
+
const own = ownLines(ctx, containerOf(ctx, event).block, statement);
|
|
264
|
+
const oldIndent = lineIndent(ctx.text, statement.start);
|
|
265
|
+
// The comments go along only when the statement lands on a line of its own.
|
|
266
|
+
let inline = false;
|
|
267
|
+
const render = (indent) => {
|
|
268
|
+
if (indent === null) {
|
|
269
|
+
inline = true;
|
|
270
|
+
return moved(ctx, statement, lineIndent(ctx.text, anchor.node.start));
|
|
271
|
+
}
|
|
272
|
+
if (!own)
|
|
273
|
+
return moved(ctx, statement, indent);
|
|
274
|
+
const from = own.from + lineIndent(ctx.text, own.from).length;
|
|
275
|
+
return reindent(ctx.text.slice(from, own.to), oldIndent, indent);
|
|
276
|
+
};
|
|
277
|
+
const insertion = after !== undefined
|
|
278
|
+
? insertAfter(ctx, block, anchor.node, render)
|
|
279
|
+
: insertBeforeStatement(ctx, block, anchor.node, render);
|
|
280
|
+
const edits = own && !inline
|
|
281
|
+
? [{ start: own.from, end: own.end, newText: "" }, insertion]
|
|
282
|
+
: [...deleteStatements(ctx.text, ctx.compilation.parse.root, [statement]), insertion];
|
|
283
|
+
verifyNoNewErrors(ctx, edits);
|
|
284
|
+
return edits;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The lines of a statement that has them to itself: from the comment lines right above it to the
|
|
288
|
+
* line break that ends it (`from`, `end`), the text to move ending with the comment that ends its
|
|
289
|
+
* line (`to`). Undefined when it shares a line with another statement.
|
|
290
|
+
*/
|
|
291
|
+
function ownLines(ctx, block, statement) {
|
|
292
|
+
const start = lineStart(ctx.text, statement.start);
|
|
293
|
+
if (start + lineIndent(ctx.text, start).length !== statement.start)
|
|
294
|
+
return undefined;
|
|
295
|
+
const children = block.children;
|
|
296
|
+
let terminator;
|
|
297
|
+
for (let i = children.indexOf(statement) + 1; i < children.length; i++) {
|
|
298
|
+
const child = children[i];
|
|
299
|
+
if (isToken(child) && child.kind === "Semicolon")
|
|
300
|
+
continue;
|
|
301
|
+
if (isToken(child) && child.kind === "Newline")
|
|
302
|
+
terminator = child;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (!terminator)
|
|
306
|
+
return undefined;
|
|
307
|
+
const above = commentsAbove(block, statement)[0];
|
|
308
|
+
const trailing = terminator.leadingTrivia.filter(isCommentTrivia);
|
|
309
|
+
return {
|
|
310
|
+
from: above ? lineStart(ctx.text, above.start) : start,
|
|
311
|
+
to: trailing.length > 0 ? trailing[trailing.length - 1].end : statement.end,
|
|
312
|
+
end: terminator.end,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The other end of an activation: the `deactivate` that closes an `activate` (the participant's
|
|
317
|
+
* first `deactivate` at the same depth after it, in order, fragments and calls included), or the
|
|
318
|
+
* `activate` that a `deactivate` closes; null for an activation open to the end, a `deactivate`
|
|
319
|
+
* that closes none, and every other event.
|
|
320
|
+
*/
|
|
321
|
+
export function activationPair(diagram, event) {
|
|
322
|
+
if (event.kind !== "activate" && event.kind !== "deactivate")
|
|
323
|
+
return null;
|
|
324
|
+
const order = withNested(diagram.events).filter((e) => (e.kind === "activate" || e.kind === "deactivate") && e.participant === event.participant);
|
|
325
|
+
const step = event.kind === "activate" ? 1 : -1;
|
|
326
|
+
let depth = 0;
|
|
327
|
+
for (let i = order.indexOf(event) + step; i >= 0 && i < order.length; i += step) {
|
|
328
|
+
const other = order[i];
|
|
329
|
+
if (other.kind === event.kind)
|
|
330
|
+
depth++;
|
|
331
|
+
else if (depth === 0)
|
|
332
|
+
return other;
|
|
333
|
+
else
|
|
334
|
+
depth--;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
188
338
|
/** A fresh id for a note, a fragment or a message, as `createElement` would choose ids. */
|
|
189
339
|
function freshEventId(ctx, base) {
|
|
190
340
|
const used = usedIds(ctx);
|
|
@@ -204,8 +354,7 @@ function namedEvent(ctx, event) {
|
|
|
204
354
|
if (event.kind === "message") {
|
|
205
355
|
const relation = ctx.diagram.relationsByKey.get(event.relation);
|
|
206
356
|
const id = freshEventId(ctx, `${relation.from}_${relation.to}`);
|
|
207
|
-
const
|
|
208
|
-
const anchor = statement.label && "end" in statement.label ? statement.label.end : statement.target.end;
|
|
357
|
+
const anchor = relationNameAnchor(relation);
|
|
209
358
|
return { id, edits: [{ start: anchor, end: anchor, newText: ` as ${id}` }] };
|
|
210
359
|
}
|
|
211
360
|
if (event.kind === "note" || event.kind === "fragment") {
|
|
@@ -217,14 +366,18 @@ function namedEvent(ctx, event) {
|
|
|
217
366
|
}
|
|
218
367
|
export function setSpacing(ctx, op) {
|
|
219
368
|
requireSequence(ctx);
|
|
220
|
-
const
|
|
369
|
+
const side = op.side ?? "before";
|
|
370
|
+
const amount = op.amount === null || Math.round(op.amount) === 0 ? null : Math.round(op.amount);
|
|
221
371
|
const statement = op.targetKind === "event" ? "space" : "gap";
|
|
372
|
+
const onSide = (p) => (p.statement === "space" || p.statement === "gap") && p.side === side;
|
|
222
373
|
let id;
|
|
223
374
|
const edits = [];
|
|
224
375
|
if (op.targetKind === "event") {
|
|
376
|
+
if (amount !== null && amount < 0)
|
|
377
|
+
reject("edit.invalid-value", reasons().sequence.negativeSpace);
|
|
225
378
|
const event = requireEvent(ctx, op.target);
|
|
226
379
|
const entry = event.id
|
|
227
|
-
? ctx.diagram.placement.find((p) => p.statement === "space" && p.target === event.id)
|
|
380
|
+
? ctx.diagram.placement.find((p) => p.statement === "space" && p.target === event.id && onSide(p))
|
|
228
381
|
: undefined;
|
|
229
382
|
if (!entry && amount === null)
|
|
230
383
|
return [];
|
|
@@ -237,8 +390,13 @@ export function setSpacing(ctx, op) {
|
|
|
237
390
|
if (!PARTICIPANTS.has(element.kind))
|
|
238
391
|
reject("edit.unsafe-target", reasons().sequence.notParticipant(element.id));
|
|
239
392
|
id = element.id;
|
|
393
|
+
// Before the first participant and after the last there is nothing to bring closer.
|
|
394
|
+
const order = ctx.diagram.elements.filter((e) => e.kind !== "group");
|
|
395
|
+
const edge = side === "before" ? order[0]?.id === id : order[order.length - 1]?.id === id;
|
|
396
|
+
if (amount !== null && amount < 0 && edge)
|
|
397
|
+
reject("edit.invalid-value", reasons().sequence.outerGap(id));
|
|
240
398
|
}
|
|
241
|
-
const entry = ctx.diagram.placement.find((p) => p.statement === statement && p.target === id);
|
|
399
|
+
const entry = ctx.diagram.placement.find((p) => p.statement === statement && p.target === id && onSide(p));
|
|
242
400
|
if (entry) {
|
|
243
401
|
if (amount === null) {
|
|
244
402
|
edits.push(...deleteStatements(ctx.text, ctx.compilation.parse.root, [entry.node]));
|
|
@@ -253,7 +411,9 @@ export function setSpacing(ctx, op) {
|
|
|
253
411
|
}
|
|
254
412
|
}
|
|
255
413
|
else if (amount !== null) {
|
|
256
|
-
|
|
414
|
+
// `before` is the default and never written.
|
|
415
|
+
const written = side === "after" ? " after" : "";
|
|
416
|
+
edits.push(insertPlacements(ctx, [`${statement} ${id} ${formatNumber(amount)}${written}`]));
|
|
257
417
|
}
|
|
258
418
|
verifyNoNewErrors(ctx, edits);
|
|
259
419
|
return edits;
|
|
@@ -266,6 +426,27 @@ export function insertEvent(ctx, op) {
|
|
|
266
426
|
const { block } = containerOf(ctx, event);
|
|
267
427
|
edit = insertAfter(ctx, block, event.node, (indent) => reindent(op.statement, "", indent ?? ""));
|
|
268
428
|
}
|
|
429
|
+
else if (op.inside !== undefined && "message" in op.inside) {
|
|
430
|
+
// Last in the call's block, which is created when the call has none.
|
|
431
|
+
const call = requireEvent(ctx, op.inside.message);
|
|
432
|
+
if (call.kind !== "message" || call.node.kind !== "RelationStatement") {
|
|
433
|
+
reject("edit.unsafe-target", reasons().sequence.notMessage(op.inside.message));
|
|
434
|
+
}
|
|
435
|
+
const node = call.node;
|
|
436
|
+
const block = callBlock(call);
|
|
437
|
+
if (block) {
|
|
438
|
+
edit = insertStatement(ctx, block, node.start, (indent) => reindent(op.statement, "", indent ?? ""));
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const indent = lineIndent(ctx.text, node.start);
|
|
442
|
+
const inner = indent + ctx.unit;
|
|
443
|
+
edit = {
|
|
444
|
+
start: node.end,
|
|
445
|
+
end: node.end,
|
|
446
|
+
newText: ` {${ctx.eol}${inner}${reindent(op.statement, "", inner)}${ctx.eol}${indent}}`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
269
450
|
else if (op.inside !== undefined) {
|
|
270
451
|
const fragment = requireFragment(ctx, op.inside.fragment);
|
|
271
452
|
const branch = fragment.branches[op.inside.branch];
|
|
@@ -436,6 +617,9 @@ export function participantEvents(ctx, removed) {
|
|
|
436
617
|
for (const branch of event.branches)
|
|
437
618
|
visit(branch.events);
|
|
438
619
|
}
|
|
620
|
+
else if (event.kind === "message") {
|
|
621
|
+
visit(event.events);
|
|
622
|
+
}
|
|
439
623
|
}
|
|
440
624
|
};
|
|
441
625
|
visit(ctx.diagram.events);
|
|
@@ -447,13 +631,16 @@ export function removeEvent(ctx, op) {
|
|
|
447
631
|
if (event.kind === "message") {
|
|
448
632
|
reject("edit.unsafe-target", reasons().sequence.removeMessage);
|
|
449
633
|
}
|
|
450
|
-
// A fragment goes with its events,
|
|
634
|
+
// A fragment goes with its events, an activation with its other end (unless it goes alone), and
|
|
635
|
+
// everything goes with what names them.
|
|
451
636
|
const all = withNested([event]);
|
|
637
|
+
const pair = op.alone === true ? null : activationPair(ctx.diagram, event);
|
|
452
638
|
const events = eventDependents(ctx, eventIdsOf(all));
|
|
453
639
|
const relations = relationDependents(ctx, new Set(all.flatMap((e) => (e.kind === "message" && e.id !== null ? [e.id] : []))));
|
|
454
640
|
const edits = [
|
|
455
641
|
...deleteStatements(ctx.text, ctx.compilation.parse.root, [
|
|
456
642
|
event.node,
|
|
643
|
+
...(pair ? [pair.node] : []),
|
|
457
644
|
...events.statements,
|
|
458
645
|
...relations.statements,
|
|
459
646
|
]),
|
|
@@ -463,7 +650,7 @@ export function removeEvent(ctx, op) {
|
|
|
463
650
|
verifyNoNewErrors(ctx, edits);
|
|
464
651
|
return edits;
|
|
465
652
|
}
|
|
466
|
-
/** The events with the events of the fragments among them, in order. */
|
|
653
|
+
/** The events with the events of the fragments and calls among them, in order. */
|
|
467
654
|
function withNested(events) {
|
|
468
655
|
const all = [];
|
|
469
656
|
const visit = (list) => {
|
|
@@ -472,11 +659,30 @@ function withNested(events) {
|
|
|
472
659
|
if (event.kind === "fragment")
|
|
473
660
|
for (const branch of event.branches)
|
|
474
661
|
visit(branch.events);
|
|
662
|
+
if (event.kind === "message")
|
|
663
|
+
visit(event.events);
|
|
475
664
|
}
|
|
476
665
|
};
|
|
477
666
|
visit(events);
|
|
478
667
|
return all;
|
|
479
668
|
}
|
|
669
|
+
/** The events inside the blocks of the calls among `relations` (keys), at any depth. */
|
|
670
|
+
export function nestedInCalls(ctx, relations) {
|
|
671
|
+
const found = [];
|
|
672
|
+
const visit = (events) => {
|
|
673
|
+
for (const event of events) {
|
|
674
|
+
if (event.kind === "fragment")
|
|
675
|
+
for (const branch of event.branches)
|
|
676
|
+
visit(branch.events);
|
|
677
|
+
else if (event.kind === "message" && relations.has(event.relation))
|
|
678
|
+
found.push(...withNested(event.events));
|
|
679
|
+
else if (event.kind === "message")
|
|
680
|
+
visit(event.events);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
visit(ctx.diagram.events);
|
|
684
|
+
return found;
|
|
685
|
+
}
|
|
480
686
|
/** The written ids of the notes and fragments among `events`. */
|
|
481
687
|
export function eventIdsOf(events) {
|
|
482
688
|
return new Set(events.flatMap((e) => (e.kind !== "message" && e.id !== null ? [e.id] : [])));
|
|
@@ -558,6 +764,10 @@ export function toggleActivation(ctx, op) {
|
|
|
558
764
|
if (message.kind !== "message")
|
|
559
765
|
reject("edit.unsafe-target", reasons().sequence.notMessage(op.after));
|
|
560
766
|
const relation = ctx.diagram.relationsByKey.get(message.relation);
|
|
767
|
+
// A call's block activates its receiver already.
|
|
768
|
+
if (message.events.length > 0) {
|
|
769
|
+
reject("edit.invalid-value", reasons().sequence.callActivates(message.key, relation.to));
|
|
770
|
+
}
|
|
561
771
|
const { block } = containerOf(ctx, message);
|
|
562
772
|
const events = eventsOfBlock(ctx, message);
|
|
563
773
|
const index = events.indexOf(message);
|
|
@@ -565,23 +775,7 @@ export function toggleActivation(ctx, op) {
|
|
|
565
775
|
let edits;
|
|
566
776
|
if (next?.kind === "activate" && next.participant === element.id) {
|
|
567
777
|
// The matching `deactivate`: the first one at the same depth, at any later point in order.
|
|
568
|
-
const
|
|
569
|
-
let depth = 0;
|
|
570
|
-
let match;
|
|
571
|
-
for (const event of order.slice(order.indexOf(next) + 1)) {
|
|
572
|
-
if (event.kind !== "activate" && event.kind !== "deactivate")
|
|
573
|
-
continue;
|
|
574
|
-
if (event.participant !== element.id)
|
|
575
|
-
continue;
|
|
576
|
-
if (event.kind === "activate")
|
|
577
|
-
depth++;
|
|
578
|
-
else if (depth === 0) {
|
|
579
|
-
match = event;
|
|
580
|
-
break;
|
|
581
|
-
}
|
|
582
|
-
else
|
|
583
|
-
depth--;
|
|
584
|
-
}
|
|
778
|
+
const match = activationPair(ctx.diagram, next);
|
|
585
779
|
edits = deleteStatements(ctx.text, ctx.compilation.parse.root, match ? [next.node, match.node] : [next.node]);
|
|
586
780
|
}
|
|
587
781
|
else {
|
|
@@ -612,6 +806,10 @@ function eventsOfBlock(ctx, event) {
|
|
|
612
806
|
const place = event.path[event.path.length - 1];
|
|
613
807
|
if (!place)
|
|
614
808
|
return ctx.diagram.events;
|
|
809
|
+
if ("message" in place) {
|
|
810
|
+
const call = requireEvent(ctx, place.message);
|
|
811
|
+
return call.kind === "message" ? call.events : [];
|
|
812
|
+
}
|
|
615
813
|
const fragment = requireFragment(ctx, place.fragment);
|
|
616
814
|
return fragment.branches[place.branch].events;
|
|
617
815
|
}
|
|
@@ -692,4 +890,117 @@ export function setFragmentOperator(ctx, op) {
|
|
|
692
890
|
verifyNoNewErrors(ctx, edits);
|
|
693
891
|
return edits;
|
|
694
892
|
}
|
|
893
|
+
// ---------------------------------------------------------------------- call blocks
|
|
894
|
+
/** The start of the comment lines right above a line, with no blank line between. */
|
|
895
|
+
function leadingComments(text, lineStart) {
|
|
896
|
+
let from = lineStart;
|
|
897
|
+
while (from > 0) {
|
|
898
|
+
const previous = text.lastIndexOf("\n", from - 2) + 1;
|
|
899
|
+
if (!text
|
|
900
|
+
.slice(previous, from - 1)
|
|
901
|
+
.trim()
|
|
902
|
+
.startsWith("//"))
|
|
903
|
+
break;
|
|
904
|
+
from = previous;
|
|
905
|
+
}
|
|
906
|
+
return from;
|
|
907
|
+
}
|
|
908
|
+
export function nestInCall(ctx, op) {
|
|
909
|
+
requireSequence(ctx);
|
|
910
|
+
const call = requireEvent(ctx, op.call);
|
|
911
|
+
if (call.kind !== "message" || call.node.kind !== "RelationStatement") {
|
|
912
|
+
reject("edit.unsafe-target", reasons().sequence.notMessage(op.call));
|
|
913
|
+
}
|
|
914
|
+
const node = call.node;
|
|
915
|
+
// The events must be the siblings right after the call, in order.
|
|
916
|
+
const siblingsOfCall = eventsOfBlock(ctx, call);
|
|
917
|
+
const at = siblingsOfCall.indexOf(call);
|
|
918
|
+
const chosen = op.events.map((key) => requireEvent(ctx, key));
|
|
919
|
+
const following = siblingsOfCall.slice(at + 1, at + 1 + chosen.length);
|
|
920
|
+
const ordered = [...chosen].sort((a, b) => a.node.start - b.node.start);
|
|
921
|
+
if (chosen.length === 0 || ordered.some((event, i) => event !== following[i])) {
|
|
922
|
+
reject("edit.unsafe-target", reasons().sequence.nestAfterCall);
|
|
923
|
+
}
|
|
924
|
+
if (ordered.some((event) => event.node.kind === "KeywordStatement" && event.node.keyword.text === "return")) {
|
|
925
|
+
reject("edit.unsafe-target", reasons().sequence.nestReturn);
|
|
926
|
+
}
|
|
927
|
+
const first = ordered[0].node;
|
|
928
|
+
const last = ordered[ordered.length - 1].node;
|
|
929
|
+
const indent = lineIndent(ctx.text, node.start);
|
|
930
|
+
const inner = indent + ctx.unit;
|
|
931
|
+
// The comment lines right above the first event move with it.
|
|
932
|
+
const firstLine = ctx.text.lastIndexOf("\n", first.start - 1) + 1;
|
|
933
|
+
const from = leadingComments(ctx.text, firstLine);
|
|
934
|
+
const own = lineIndent(ctx.text, first.start);
|
|
935
|
+
const text = reindent(ctx.text.slice(from + own.length, last.end), own, inner);
|
|
936
|
+
const block = callBlock(call);
|
|
937
|
+
const edits = [
|
|
938
|
+
...deleteStatements(ctx.text, ctx.compilation.parse.root, ordered.map((e) => e.node)),
|
|
939
|
+
];
|
|
940
|
+
if (from < firstLine)
|
|
941
|
+
edits.push({ start: from, end: firstLine, newText: "" });
|
|
942
|
+
edits.unshift(block
|
|
943
|
+
? insertStatement(ctx, block, node.start, (blockIndent) => reindent(text, inner, blockIndent ?? inner))
|
|
944
|
+
: {
|
|
945
|
+
start: node.end,
|
|
946
|
+
end: node.end,
|
|
947
|
+
newText: ` {${ctx.eol}${inner}${text}${ctx.eol}${indent}}`,
|
|
948
|
+
});
|
|
949
|
+
verifyNoNewErrors(ctx, edits);
|
|
950
|
+
return edits;
|
|
951
|
+
}
|
|
952
|
+
export function unnestCall(ctx, op) {
|
|
953
|
+
requireSequence(ctx);
|
|
954
|
+
const call = requireEvent(ctx, op.call);
|
|
955
|
+
if (call.kind !== "message" || call.node.kind !== "RelationStatement") {
|
|
956
|
+
reject("edit.unsafe-target", reasons().sequence.notMessage(op.call));
|
|
957
|
+
}
|
|
958
|
+
const node = call.node;
|
|
959
|
+
const block = callBlock(call);
|
|
960
|
+
const events = block ? siblings(block) : [];
|
|
961
|
+
if (!block || events.length === 0) {
|
|
962
|
+
reject("edit.unsafe-target", reasons().sequence.noCallBlock(call.key));
|
|
963
|
+
}
|
|
964
|
+
const relation = ctx.diagram.relationsByKey.get(call.relation);
|
|
965
|
+
// Every `return` answering this call, at any depth of fragments inside its block, becomes a
|
|
966
|
+
// written reply in place: `<callee> --> <caller>`, its label, id and block kept.
|
|
967
|
+
const rewrites = [];
|
|
968
|
+
for (const reply of ctx.diagram.relations) {
|
|
969
|
+
if (reply.returns !== call.key || reply.node.kind !== "KeywordStatement")
|
|
970
|
+
continue;
|
|
971
|
+
const keyword = reply.node.keyword;
|
|
972
|
+
const label = reply.node.args[0]?.kind === "StringLiteral" ? reply.node.args[0] : undefined;
|
|
973
|
+
const arrow = `${relation.to} --> ${relation.from}`;
|
|
974
|
+
rewrites.push(label
|
|
975
|
+
? { start: keyword.start, end: label.start, newText: `${arrow}: ` }
|
|
976
|
+
: { start: keyword.start, end: keyword.end, newText: arrow });
|
|
977
|
+
}
|
|
978
|
+
const first = events[0];
|
|
979
|
+
const last = events[events.length - 1];
|
|
980
|
+
const from = first.start;
|
|
981
|
+
// The moved text, with the rewrites applied inside it.
|
|
982
|
+
let slice = ctx.text.slice(from, last.end);
|
|
983
|
+
for (const rewrite of [...rewrites].sort((a, b) => b.start - a.start)) {
|
|
984
|
+
slice =
|
|
985
|
+
slice.slice(0, rewrite.start - from) + rewrite.newText + slice.slice(rewrite.end - from);
|
|
986
|
+
}
|
|
987
|
+
const indent = lineIndent(ctx.text, node.start);
|
|
988
|
+
const text = reindent(slice, lineIndent(ctx.text, first.start), indent);
|
|
989
|
+
const properties = block.statements.filter((s) => !isEventStatement(s));
|
|
990
|
+
const interleaved = properties.some((p) => p.start > first.start && p.end < last.end);
|
|
991
|
+
if (interleaved)
|
|
992
|
+
reject("edit.unsafe-target", reasons().sequence.unnestInterleaved(call.key));
|
|
993
|
+
const edits = [];
|
|
994
|
+
if (properties.length === 0) {
|
|
995
|
+
// Only events were in the block: the block goes, from the end of what precedes it.
|
|
996
|
+
const before = node.id && isToken(node.id) ? node.id.end : relationNameAnchor(relation);
|
|
997
|
+
edits.push({ start: before, end: node.end, newText: `${ctx.eol}${indent}${text}` });
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
edits.push(...deleteStatements(ctx.text, ctx.compilation.parse.root, events));
|
|
1001
|
+
edits.push({ start: node.end, end: node.end, newText: `${ctx.eol}${indent}${text}` });
|
|
1002
|
+
}
|
|
1003
|
+
verifyNoNewErrors(ctx, edits);
|
|
1004
|
+
return edits;
|
|
1005
|
+
}
|
|
695
1006
|
//# sourceMappingURL=sequence.js.map
|