@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.
Files changed (78) hide show
  1. package/dist/animate.d.ts.map +1 -1
  2. package/dist/animate.js +3 -0
  3. package/dist/animate.js.map +1 -1
  4. package/dist/attachments.d.ts +30 -0
  5. package/dist/attachments.d.ts.map +1 -0
  6. package/dist/attachments.js +85 -0
  7. package/dist/attachments.js.map +1 -0
  8. package/dist/c4.d.ts +26 -1
  9. package/dist/c4.d.ts.map +1 -1
  10. package/dist/c4.js +321 -27
  11. package/dist/c4.js.map +1 -1
  12. package/dist/context.d.ts +7 -0
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +15 -0
  15. package/dist/context.js.map +1 -1
  16. package/dist/create.d.ts.map +1 -1
  17. package/dist/create.js +61 -15
  18. package/dist/create.js.map +1 -1
  19. package/dist/free.d.ts.map +1 -1
  20. package/dist/free.js +97 -9
  21. package/dist/free.js.map +1 -1
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/insert.d.ts +39 -1
  27. package/dist/insert.d.ts.map +1 -1
  28. package/dist/insert.js +128 -2
  29. package/dist/insert.js.map +1 -1
  30. package/dist/labels.d.ts.map +1 -1
  31. package/dist/labels.js +7 -9
  32. package/dist/labels.js.map +1 -1
  33. package/dist/lines.d.ts +16 -0
  34. package/dist/lines.d.ts.map +1 -0
  35. package/dist/lines.js +123 -0
  36. package/dist/lines.js.map +1 -0
  37. package/dist/messages/en.d.ts +105 -1
  38. package/dist/messages/en.d.ts.map +1 -1
  39. package/dist/messages/en.js +108 -2
  40. package/dist/messages/en.js.map +1 -1
  41. package/dist/messages/es.d.ts.map +1 -1
  42. package/dist/messages/es.js +69 -2
  43. package/dist/messages/es.js.map +1 -1
  44. package/dist/messages/index.d.ts +61 -1
  45. package/dist/messages/index.d.ts.map +1 -1
  46. package/dist/pens.d.ts +16 -0
  47. package/dist/pens.d.ts.map +1 -0
  48. package/dist/pens.js +175 -0
  49. package/dist/pens.js.map +1 -0
  50. package/dist/plan.d.ts.map +1 -1
  51. package/dist/plan.js +125 -20
  52. package/dist/plan.js.map +1 -1
  53. package/dist/relations.d.ts +1 -1
  54. package/dist/relations.d.ts.map +1 -1
  55. package/dist/relations.js +35 -16
  56. package/dist/relations.js.map +1 -1
  57. package/dist/sequence.d.ts +13 -2
  58. package/dist/sequence.d.ts.map +1 -1
  59. package/dist/sequence.js +342 -31
  60. package/dist/sequence.js.map +1 -1
  61. package/dist/structure.d.ts +7 -5
  62. package/dist/structure.d.ts.map +1 -1
  63. package/dist/structure.js +49 -49
  64. package/dist/structure.js.map +1 -1
  65. package/dist/styles.d.ts.map +1 -1
  66. package/dist/styles.js +8 -3
  67. package/dist/styles.js.map +1 -1
  68. package/dist/tags.d.ts +19 -0
  69. package/dist/tags.d.ts.map +1 -0
  70. package/dist/tags.js +159 -0
  71. package/dist/tags.js.map +1 -0
  72. package/dist/text.d.ts +4 -0
  73. package/dist/text.d.ts.map +1 -1
  74. package/dist/text.js +9 -0
  75. package/dist/text.js.map +1 -1
  76. package/dist/types.d.ts +270 -21
  77. package/dist/types.d.ts.map +1 -1
  78. package/package.json +8 -8
package/dist/sequence.js CHANGED
@@ -1,12 +1,13 @@
1
- import { FRAGMENT_BRANCH_KEYWORDS, FRAGMENT_OPERATORS, PARTICIPANT_KINDS, } from "@meltemi/diagrams-model";
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 that holds an event: the diagram body or a fragment branch. */
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
- if (fragment && fragment.kind === "fragment") {
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 statement = relation.node;
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 amount = op.amount === null || op.amount <= 0 ? null : Math.round(op.amount);
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
- edits.push(insertPlacements(ctx, [`${statement} ${id} ${formatNumber(amount)}`]));
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, and everything goes with what names them.
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 order = withNested(ctx.diagram.events);
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