@meltemi/diagrams-edit 1.9.0 → 1.10.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.
@@ -0,0 +1,30 @@
1
+ import type { Arg, TextEdit } from "@meltemi/diagrams-language";
2
+ import type { Element } from "@meltemi/diagrams-model";
3
+ import { type EditContext, type TextContext } from "./context.js";
4
+ import type { OperationOf } from "./plan.js";
5
+ import type { Dependent } from "./types.js";
6
+ /** The words after the text of a note's declaration: `on` and the target that follows it. */
7
+ export interface AttachmentArgs {
8
+ /** The argument before `on`: the note's text (or its id when it has none). */
9
+ readonly before: Arg;
10
+ readonly word: Arg;
11
+ readonly target: Arg;
12
+ }
13
+ /** The `on <target>` clause of a note's statement, or undefined when it has none. */
14
+ export declare function attachmentArgs(note: Element): AttachmentArgs | undefined;
15
+ /** The edit that takes ` on <target>` out of a note's statement, keeping its block and comments. */
16
+ export declare function stripAttachment(note: Element): TextEdit | undefined;
17
+ /**
18
+ * A note attached to something about to go, as a dependent of the removal and the edit that
19
+ * leaves the note without its clause: a note's text is the person's words, so it stays.
20
+ */
21
+ export declare function attachmentDependent(ctx: TextContext, note: Element): {
22
+ dependent: Dependent;
23
+ edit: TextEdit;
24
+ } | undefined;
25
+ /**
26
+ * Writes, replaces or removes the `on <element>` clause of a note, in the same declaration and
27
+ * with the smallest edit: the clause follows the note's text and the block stays where it is.
28
+ */
29
+ export declare function attachNote(ctx: EditContext, op: OperationOf<"attachNote">): TextEdit[];
30
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAoB,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAA0B,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAG1F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;CACtB;AAID,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAOxE;AAED,oGAAoG;AACpG,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAGnE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,OAAO,GACZ;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,SAAS,CAatD;AAUD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,QAAQ,EAAE,CA+BtF"}
@@ -0,0 +1,85 @@
1
+ import { reject, requireElement } from "./context.js";
2
+ import { verifyNoNewErrors } from "./insert.js";
3
+ import { messages, reasons } from "./messages/index.js";
4
+ const isOn = (arg) => arg?.kind === "QualifiedRef" && arg.text === "on";
5
+ /** The `on <target>` clause of a note's statement, or undefined when it has none. */
6
+ export function attachmentArgs(note) {
7
+ const node = note.node;
8
+ if (note.kind !== "note" || node?.kind !== "KeywordStatement")
9
+ return undefined;
10
+ const args = node.args;
11
+ const at = args.findIndex((arg, i) => i >= 2 && isOn(arg));
12
+ const target = args[at + 1];
13
+ return at < 0 || !target ? undefined : { before: args[at - 1], word: args[at], target };
14
+ }
15
+ /** The edit that takes ` on <target>` out of a note's statement, keeping its block and comments. */
16
+ export function stripAttachment(note) {
17
+ const clause = attachmentArgs(note);
18
+ return clause && { start: clause.before.end, end: clause.target.end, newText: "" };
19
+ }
20
+ /**
21
+ * A note attached to something about to go, as a dependent of the removal and the edit that
22
+ * leaves the note without its clause: a note's text is the person's words, so it stays.
23
+ */
24
+ export function attachmentDependent(ctx, note) {
25
+ const edit = stripAttachment(note);
26
+ if (!edit || !note.node)
27
+ return undefined;
28
+ const lines = ctx.compilation.parse.lines;
29
+ return {
30
+ edit,
31
+ dependent: {
32
+ kind: "attachment",
33
+ name: note.id,
34
+ description: messages().dependents.attachment(note.id, lines.location(note.node.start).line),
35
+ range: lines.range(note.node),
36
+ },
37
+ };
38
+ }
39
+ /** Whether `path` is an element a note of the diagram may be attached to. */
40
+ function attachable(ctx, note, path) {
41
+ if (path === note.id)
42
+ return false;
43
+ const element = ctx.diagram.elementsById.get(path);
44
+ // A view draws entities, and notes attach to what it draws.
45
+ return ctx.diagram.type === "c4" ? element?.entity != null : element !== undefined;
46
+ }
47
+ /**
48
+ * Writes, replaces or removes the `on <element>` clause of a note, in the same declaration and
49
+ * with the smallest edit: the clause follows the note's text and the block stays where it is.
50
+ */
51
+ export function attachNote(ctx, op) {
52
+ const note = requireElement(ctx, op.note);
53
+ const r = reasons().attach;
54
+ if (note.kind !== "note" || note.node?.kind !== "KeywordStatement") {
55
+ reject("edit.invalid-value", r.onlyNotes(note.id, note.kind));
56
+ }
57
+ const clause = attachmentArgs(note);
58
+ const node = note.node;
59
+ if (op.element === null) {
60
+ const edit = stripAttachment(note);
61
+ return edit ? [edit] : [];
62
+ }
63
+ if (op.element === note.id)
64
+ reject("edit.invalid-value", r.self(note.id));
65
+ if (!attachable(ctx, note, op.element)) {
66
+ if (ctx.diagram.type === "c4") {
67
+ reject("edit.unsafe-target", r.notDrawn(op.element, ctx.diagram.id));
68
+ }
69
+ requireElement(ctx, op.element);
70
+ }
71
+ if (clause?.target.kind === "QualifiedRef" && clause.target.text === op.element)
72
+ return [];
73
+ const edits = clause
74
+ ? [{ start: clause.target.start, end: clause.target.end, newText: op.element }]
75
+ : [
76
+ {
77
+ start: (node.args[1] ?? node.args[0]).end,
78
+ end: (node.args[1] ?? node.args[0]).end,
79
+ newText: ` on ${op.element}`,
80
+ },
81
+ ];
82
+ verifyNoNewErrors(ctx, edits);
83
+ return edits;
84
+ }
85
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}
package/dist/c4.d.ts CHANGED
@@ -2,10 +2,22 @@ import { type Statement, type TextEdit } from "@meltemi/diagrams-language";
2
2
  import { type Compilation, type EntityReference, type FlowDiagram, type ModelDefinition } from "@meltemi/diagrams-model";
3
3
  import { type EditContext, type TextContext } from "./context.js";
4
4
  import { type OperationOf } from "./plan.js";
5
+ /** The operation fields that name the element it acts on. */
6
+ interface ElementTarget {
7
+ readonly kind: string;
8
+ readonly element?: string | null;
9
+ readonly target?: string;
10
+ readonly targetKind?: string;
11
+ }
5
12
  /** Refuses an operation that does not apply to a C4 view, naming the ones that do. */
6
- export declare function refuseInView(ctx: EditContext, kind: string): void;
13
+ export declare function refuseInView(ctx: EditContext, operation: ElementTarget): void;
7
14
  /** Relations of the view that are combinations written in it, not model relations. */
8
15
  export declare function isCombination(diagram: FlowDiagram, key: string): boolean;
16
+ /**
17
+ * Declares a note in a view: after its last note, else after the lists and labels and before the
18
+ * blocks, where the language keeps what a view says about what it draws.
19
+ */
20
+ export declare function insertViewNote(ctx: EditContext, text: string): TextEdit;
9
21
  export declare function includeEntity(ctx: EditContext, op: OperationOf<"includeEntity">): TextEdit[];
10
22
  export declare function excludeEntity(ctx: EditContext, op: OperationOf<"excludeEntity">): TextEdit[];
11
23
  export declare function setLocalLabel(ctx: EditContext, op: OperationOf<"setLocalLabel">): TextEdit[];
@@ -24,7 +36,20 @@ export declare function removeEntity(ctx: ModelContext, op: OperationOf<"removeE
24
36
  export declare function setEntityProperty(ctx: ModelContext, op: OperationOf<"setEntityProperty">): TextEdit[];
25
37
  export declare function createEntity(ctx: ModelContext, op: OperationOf<"createEntity">): TextEdit[];
26
38
  export declare function createModelRelation(ctx: ModelContext, op: OperationOf<"createModelRelation">): TextEdit[];
39
+ /** Moves one end of a model relation to another entity; the label, id and block stay. */
40
+ export declare function reconnectModelRelation(ctx: ModelContext, op: OperationOf<"reconnectModelRelation">): TextEdit[];
41
+ /**
42
+ * The routes of a relation in a view of a file other than the model's, once the model moved one of
43
+ * its ends (the compilation already resolves the moved model): they follow the end the view
44
+ * draws now, as `reconnectModelRelation` makes them follow in the model's own file.
45
+ */
46
+ export declare function rewriteRouteEnd(ctx: EditContext, op: OperationOf<"rewriteRouteEnd">): TextEdit[];
27
47
  export declare function removeModelRelation(ctx: ModelContext, op: OperationOf<"removeModelRelation">): TextEdit[];
48
+ /**
49
+ * Takes a model relation out of a view whose model is in another file: the statements that name
50
+ * it go (with `cascade`), or are listed as dependents, with the view as drawing it.
51
+ */
52
+ export declare function forgetModelRelation(ctx: EditContext, op: OperationOf<"forgetModelRelation">): TextEdit[];
28
53
  export declare function setModelRelationProperty(ctx: ModelContext, op: OperationOf<"setModelRelationProperty">): TextEdit[];
29
54
  export type { Statement };
30
55
  //# sourceMappingURL=c4.d.ts.map
package/dist/c4.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"c4.d.ts","sourceRoot":"","sources":["../src/c4.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACd,KAAK,QAAQ,EAEd,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,EAGrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAA2B,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAI3F,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AA0B9D,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAGjE;AAQD,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAExE;AAqCD,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,CAAC,eAAe,CAAC,GAAG,QAAQ,EAAE,CAuB5F;AAyED,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,CAAC,eAAe,CAAC,GAAG,QAAQ,EAAE,CA+B5F;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,CAAC,eAAe,CAAC,GAAG,QAAQ,EAAE,CAgC5F;AAID,mEAAmE;AACnE,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,YAAY,CAK7E;AAoBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,eAAe,GACrB,eAAe,EAAE,CAmBnB;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,QAAQ,EAAE,CAqB3F;AAOD,wBAAgB,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,QAAQ,EAAE,CAkH3F;AA0CD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,YAAY,EACjB,EAAE,EAAE,WAAW,CAAC,mBAAmB,CAAC,GACnC,QAAQ,EAAE,CAoDZ;AAUD,wBAAgB,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,QAAQ,EAAE,CAyC3F;AAOD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,YAAY,EACjB,EAAE,EAAE,WAAW,CAAC,qBAAqB,CAAC,GACrC,QAAQ,EAAE,CAuBZ;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,YAAY,EACjB,EAAE,EAAE,WAAW,CAAC,qBAAqB,CAAC,GACrC,QAAQ,EAAE,CAwBZ;AAED,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,YAAY,EACjB,EAAE,EAAE,WAAW,CAAC,0BAA0B,CAAC,GAC1C,QAAQ,EAAE,CAiEZ;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"c4.d.ts","sourceRoot":"","sources":["../src/c4.ts"],"names":[],"mappings":"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"}
package/dist/c4.js CHANGED
@@ -1,15 +1,20 @@
1
- import { isIdentifierText, isToken, } from "@meltemi/diagrams-language";
2
- import { ENTITY_CHILDREN, ENTITY_KINDS, isSafeUrl, } from "@meltemi/diagrams-model";
1
+ import { applyTextEdits, isIdentifierText, isToken, } from "@meltemi/diagrams-language";
2
+ import { ENTITY_CHILDREN, ENTITY_KINDS, defaultEntities, isSafeUrl, compile, singleIdent, } from "@meltemi/diagrams-model";
3
3
  import { insertStatement, reject } from "./context.js";
4
4
  import { deleteStatements } from "./delete.js";
5
+ import { attachmentDependent } from "./attachments.js";
5
6
  import { insertAfter, insertBefore, verifyNoNewErrors } from "./insert.js";
6
7
  import { messages, reasons } from "./messages/index.js";
7
8
  import { removeListItems } from "./plan.js";
9
+ import { endpointText, requireEndPosition, routeParts } from "./routes.js";
8
10
  import { storyDependents } from "./stories.js";
9
11
  import { quoteString } from "./text.js";
10
12
  // ---------------------------------------------------------------------- views
11
13
  /** Operations a C4 view accepts; the others write declarations the view does not have. */
12
14
  const VIEW_OPERATIONS = new Set([
15
+ // A view declares notes: `createElement` refuses every other kind there.
16
+ "createElement",
17
+ "attachNote",
13
18
  "setPosition",
14
19
  "setSize",
15
20
  "setRoute",
@@ -25,11 +30,32 @@ const VIEW_OPERATIONS = new Set([
25
30
  "includeEntity",
26
31
  "excludeEntity",
27
32
  "setLocalLabel",
33
+ "forgetModelRelation",
34
+ "rewriteRouteEnd",
35
+ "materializeRelationId",
36
+ ]);
37
+ /**
38
+ * Operations a note of a view takes as any element does, since the note is a declaration of the
39
+ * view: its text, id, classes and url, and its removal with the placement and styles it has.
40
+ */
41
+ const NOTE_OPERATIONS = new Set([
42
+ "renameElement",
43
+ "removeElement",
44
+ "setElementLabel",
45
+ "setClasses",
46
+ "setElementProperty",
28
47
  ]);
29
48
  /** Refuses an operation that does not apply to a C4 view, naming the ones that do. */
30
- export function refuseInView(ctx, kind) {
49
+ export function refuseInView(ctx, operation) {
50
+ const { kind } = operation;
31
51
  if (ctx.diagram.type !== "c4" || VIEW_OPERATIONS.has(kind))
32
52
  return;
53
+ if (NOTE_OPERATIONS.has(kind) && operation.targetKind !== "relation") {
54
+ const id = operation.element ?? operation.target;
55
+ const element = id === undefined ? undefined : ctx.diagram.elementsById.get(id);
56
+ if (element?.kind === "note" && element.entity === null)
57
+ return;
58
+ }
33
59
  reject("edit.invalid-value", reasons().c4.notInView(kind, ctx.diagram.c4?.model ?? ""));
34
60
  }
35
61
  function requireView(ctx) {
@@ -69,6 +95,13 @@ function insertViewStatement(ctx, text, after) {
69
95
  ? insertBefore(ctx, first, () => text)
70
96
  : insertStatement(ctx, body, ctx.diagram.node.start, () => text);
71
97
  }
98
+ /**
99
+ * Declares a note in a view: after its last note, else after the lists and labels and before the
100
+ * blocks, where the language keeps what a view says about what it draws.
101
+ */
102
+ export function insertViewNote(ctx, text) {
103
+ return insertViewStatement(ctx, text, ["level", "scope", "include", "exclude", "label", "note"]);
104
+ }
72
105
  function requireEntity(ctx, path) {
73
106
  const model = requireView(ctx).resolved?.definition;
74
107
  if (!model)
@@ -106,8 +139,7 @@ export function includeEntity(ctx, op) {
106
139
  return edits;
107
140
  }
108
141
  /** The view statements that only target an element, and edits that drop it from shared ones. */
109
- function viewDependents(ctx, id) {
110
- const diagram = ctx.diagram;
142
+ function viewDependents(ctx, diagram, id) {
111
143
  const lines = ctx.compilation.parse.lines;
112
144
  const statements = [];
113
145
  const edits = [];
@@ -122,6 +154,16 @@ function viewDependents(ctx, id) {
122
154
  });
123
155
  };
124
156
  const gone = new Set(diagram.relations.filter((r) => r.from === id || r.to === id).map((r) => r.key));
157
+ // A note of the view attached to the entity stays, without its `on` clause.
158
+ for (const note of diagram.elements) {
159
+ if (note.attachedTo !== id)
160
+ continue;
161
+ const attached = attachmentDependent(ctx, note);
162
+ if (!attached)
163
+ continue;
164
+ dependents.push(attached.dependent);
165
+ edits.push(attached.edit);
166
+ }
125
167
  for (const entry of diagram.placement) {
126
168
  if (entry.target === id || gone.has(entry.target))
127
169
  depend(entry.statement, entry.target, entry.node);
@@ -176,7 +218,7 @@ export function excludeEntity(ctx, op) {
176
218
  if (element.kind === "group") {
177
219
  reject("edit.unsafe-target", reasons().c4.isScope(op.entity));
178
220
  }
179
- const dependents = viewDependents(ctx, op.entity);
221
+ const dependents = viewDependents(ctx, ctx.diagram, op.entity);
180
222
  if (!op.cascade && dependents.dependents.length > 0) {
181
223
  reject("edit.has-dependents", reasons().c4.removingFromView(op.entity, dependents.dependents.length), dependents.dependents);
182
224
  }
@@ -222,7 +264,7 @@ export function setLocalLabel(ctx, op) {
222
264
  else {
223
265
  const relation = ctx.diagram.relationsByKey.get(op.target);
224
266
  if (relation && relation.id === null) {
225
- reject("edit.anonymous-relation", reasons().c4.nameRelation);
267
+ reject("edit.anonymous-relation", reasons().relations.unnamed(op.target));
226
268
  }
227
269
  edits = [
228
270
  insertViewStatement(ctx, `label ${op.target} ${quoteString(op.label)}`, [
@@ -378,6 +420,16 @@ export function removeEntity(ctx, op) {
378
420
  if (removed.has(label.target) || relationKeys.has(label.target))
379
421
  depend("label", label.target, label.node);
380
422
  }
423
+ // The notes of the view attached to a removed entity stay, without their clause.
424
+ for (const note of diagram.elements) {
425
+ if (note.attachedTo === null || !removed.has(note.attachedTo))
426
+ continue;
427
+ const attached = attachmentDependent(ctx, note);
428
+ if (!attached)
429
+ continue;
430
+ dependents.push(attached.dependent);
431
+ edits.push(attached.edit);
432
+ }
381
433
  for (const entry of diagram.placement) {
382
434
  const target = entry.target;
383
435
  const relation = diagram.relationsByKey.get(target);
@@ -578,34 +630,276 @@ export function createModelRelation(ctx, op) {
578
630
  verifyNoNewErrors(ctx, edits);
579
631
  return edits;
580
632
  }
633
+ /** Moves one end of a model relation to another entity; the label, id and block stay. */
634
+ export function reconnectModelRelation(ctx, op) {
635
+ const relation = requireModelRelation(ctx, op.relation);
636
+ const entity = requireModelEntity(ctx, op.entity);
637
+ const current = op.end === "from" ? relation.from : relation.to;
638
+ if (current === entity.path)
639
+ return [];
640
+ const ref = op.end === "from" ? relation.node.source : relation.node.target;
641
+ if (ref.kind !== "QualifiedRef") {
642
+ reject("edit.unsafe-target", reasons().c4.endUnreadable(relation.key));
643
+ }
644
+ if (op.position !== undefined)
645
+ requireEndPosition(op.port, op.position);
646
+ // The path from the root resolves wherever the statement sits: the lookup from the enclosing
647
+ // entity outwards ends at the root.
648
+ const moved = { start: ref.start, end: ref.end, newText: entity.path };
649
+ const edits = [moved];
650
+ // The routes of the file's views follow the end the views now draw.
651
+ if (relation.id !== null) {
652
+ const after = compile(applyTextEdits(ctx.text, [moved]), ctx.compilation.options);
653
+ const removed = [];
654
+ for (const diagram of ctx.compilation.document.diagrams) {
655
+ const c4 = diagram.c4;
656
+ if (!c4 || c4.resolved?.file !== null || c4.resolved.definition.id !== ctx.model.id)
657
+ continue;
658
+ const routes = routeStatements(diagram, relation.id);
659
+ if (routes.length === 0)
660
+ continue;
661
+ const now = after.document.diagrams.find((d) => d.id === diagram.id);
662
+ const drawn = now?.relations.find((r) => r.id === relation.id);
663
+ const pinned = diagram.id === op.view && op.port !== undefined
664
+ ? { port: op.port, ...(op.position !== undefined ? { position: op.position } : {}) }
665
+ : undefined;
666
+ const followed = followRoutes(ctx.text, ctx.model, routes, drawn, op.end, pinned);
667
+ edits.push(...followed.edits);
668
+ removed.push(...followed.removed);
669
+ }
670
+ edits.push(...deleteStatements(ctx.text, ctx.compilation.parse.root, removed));
671
+ }
672
+ verifyNoNewErrors(ctx, edits);
673
+ return edits;
674
+ }
675
+ /** The `route <id>` statements of a view, whether the analyser took them or not. */
676
+ function routeStatements(diagram, id) {
677
+ const found = [];
678
+ for (const owner of diagram.placementBlocks) {
679
+ for (const stmt of owner.block?.statements ?? []) {
680
+ if (stmt.kind !== "KeywordStatement" || stmt.keyword.text !== "route")
681
+ continue;
682
+ if (singleIdent(stmt.args[0])?.text === id)
683
+ found.push(stmt);
684
+ }
685
+ }
686
+ return found;
687
+ }
688
+ /** The compass sides and `auto`: the ports every entity has, which carry over to another. */
689
+ const COMPASS = new Set(["north", "south", "east", "west", "auto"]);
690
+ /** A route end as written: the entity it names, and the port after it when it has one. */
691
+ function writtenEnd(model, text) {
692
+ if (model.entitiesByPath.has(text))
693
+ return { element: text, port: null };
694
+ const dot = text.lastIndexOf(".");
695
+ if (dot > 0 && model.entitiesByPath.has(text.slice(0, dot))) {
696
+ return { element: text.slice(0, dot), port: text.slice(dot + 1) };
697
+ }
698
+ return { element: text, port: null };
699
+ }
700
+ /**
701
+ * The edits that make a view's routes of a relation end where the view now draws its moved end:
702
+ * the end rewritten with the drawn entity, a compass port carried over, a named port and a point
703
+ * dropped (or `pinned`, the port and point of a drop on this view), the other end and the
704
+ * waypoints kept; a route the view no longer draws the relation for goes; a route whose end
705
+ * already meets the drawn one stays as it is.
706
+ */
707
+ function followRoutes(text, model, routes, drawn, end, pinned) {
708
+ const edits = [];
709
+ const removed = [];
710
+ for (const route of routes) {
711
+ if (!drawn) {
712
+ removed.push(route);
713
+ continue;
714
+ }
715
+ const parts = routeParts(route);
716
+ const arg = end === "from" ? parts.fromRef : parts.toRef;
717
+ const at = end === "from" ? parts.fromAt : parts.toAt;
718
+ if (arg?.kind !== "QualifiedRef") {
719
+ reject("edit.unsafe-target", reasons().relations.routeUnreadable);
720
+ }
721
+ const target = end === "from" ? drawn.from : drawn.to;
722
+ const written = writtenEnd(model, text.slice(arg.start, arg.end));
723
+ let newText;
724
+ if (pinned)
725
+ newText = endpointText(target, pinned.port, pinned.position);
726
+ else if (written.element === target)
727
+ continue;
728
+ else {
729
+ const port = written.port !== null && COMPASS.has(written.port) ? written.port : null;
730
+ newText = endpointText(target, port);
731
+ }
732
+ const stop = (at ?? arg).end;
733
+ if (newText !== text.slice(arg.start, stop)) {
734
+ edits.push({ start: arg.start, end: stop, newText });
735
+ }
736
+ }
737
+ return { edits, removed };
738
+ }
739
+ /**
740
+ * The routes of a relation in a view of a file other than the model's, once the model moved one of
741
+ * its ends (the compilation already resolves the moved model): they follow the end the view
742
+ * draws now, as `reconnectModelRelation` makes them follow in the model's own file.
743
+ */
744
+ export function rewriteRouteEnd(ctx, op) {
745
+ const c4 = requireView(ctx);
746
+ const model = c4.resolved?.definition;
747
+ if (!model)
748
+ reject("edit.unsafe-target", reasons().c4.unresolved);
749
+ const routes = routeStatements(ctx.diagram, op.relation);
750
+ if (routes.length === 0)
751
+ return [];
752
+ const drawn = ctx.diagram.relations.find((r) => r.id === op.relation);
753
+ const followed = followRoutes(ctx.text, model, routes, drawn, op.end, undefined);
754
+ const edits = [
755
+ ...followed.edits,
756
+ ...deleteStatements(ctx.text, ctx.compilation.parse.root, followed.removed),
757
+ ];
758
+ verifyNoNewErrors(ctx, edits);
759
+ return edits;
760
+ }
761
+ /**
762
+ * Removes a model relation with the statements of the file's views that name it. Without
763
+ * `cascade`, those statements and the views that draw the relation (other than `op.view`) are
764
+ * listed as dependents.
765
+ */
766
+ /**
767
+ * What a view holds about a model relation: the statements that name it by its key, or by a
768
+ * combination standing for it alone, as statements to delete, edits that take it out of shared
769
+ * lists, and dependents; with the view itself as drawing it when `listDrawn`.
770
+ */
771
+ function relationStatements(ctx, diagram, key, listDrawn) {
772
+ const c4 = diagram.c4;
773
+ const lines = ctx.compilation.parse.lines;
774
+ const statements = [];
775
+ const edits = [];
776
+ const dependents = [];
777
+ if (!c4)
778
+ return { statements, edits, dependents };
779
+ const depend = (kind, name, node) => {
780
+ statements.push(node);
781
+ dependents.push({
782
+ kind,
783
+ name,
784
+ description: messages().dependents.statement(kind, name, lines.location(node.start).line),
785
+ range: lines.range(node),
786
+ });
787
+ };
788
+ const keys = new Set([key]);
789
+ let drawn = false;
790
+ for (const shown of diagram.relations) {
791
+ if (!shown.sources.includes(key))
792
+ continue;
793
+ drawn = true;
794
+ if (isCombination(diagram, shown.key) && shown.sources.every((k) => k === key)) {
795
+ keys.add(shown.key);
796
+ depend("relation", shown.key, shown.node);
797
+ }
798
+ }
799
+ if (drawn && listDrawn) {
800
+ dependents.push({
801
+ kind: "relation",
802
+ name: diagram.id,
803
+ description: messages().dependents.drawnIn(key, diagram.id, lines.location(diagram.node.start).line),
804
+ range: lines.range(diagram.node),
805
+ });
806
+ }
807
+ for (const entry of diagram.placement) {
808
+ if (keys.has(entry.target))
809
+ depend(entry.statement, entry.target, entry.node);
810
+ }
811
+ for (const label of c4.labels) {
812
+ if (keys.has(label.target))
813
+ depend("label", label.target, label.node);
814
+ }
815
+ // A style or an animation of several targets loses this one; of this one alone, it goes.
816
+ const partial = new Map();
817
+ for (const entry of [...diagram.styles, ...diagram.animations]) {
818
+ if (!keys.has(entry.target))
819
+ continue;
820
+ const index = entry.node.args.findIndex((a) => a.start === entry.targetRange.start.offset);
821
+ const set = partial.get(entry.node) ?? new Set();
822
+ if (index >= 0)
823
+ set.add(index);
824
+ partial.set(entry.node, set);
825
+ const kind = "properties" in entry ? "style" : "animate";
826
+ dependents.push({
827
+ kind,
828
+ name: entry.target,
829
+ description: messages().dependents.statement(kind, entry.target, lines.location(entry.node.start).line),
830
+ range: lines.range(entry.node),
831
+ });
832
+ }
833
+ for (const [node, indices] of partial) {
834
+ if (indices.size >= node.args.length)
835
+ statements.push(node);
836
+ else
837
+ edits.push(...removeListItems(node.args, indices));
838
+ }
839
+ const stories = storyDependents(ctx.compilation, diagram.id, keys);
840
+ statements.push(...stories.statements);
841
+ edits.push(...stories.edits);
842
+ dependents.push(...stories.dependents);
843
+ // An entity the view drew only for this relation leaves its default set, with what names it.
844
+ const model = c4.resolved?.definition;
845
+ if (model && c4.includes === null) {
846
+ const without = {
847
+ ...model,
848
+ relations: model.relations.filter((r) => r.key !== key),
849
+ };
850
+ const after = defaultEntities(without, c4.level, c4.scope);
851
+ for (const path of defaultEntities(model, c4.level, c4.scope)) {
852
+ if (after.has(path) || !diagram.elementsById.has(path))
853
+ continue;
854
+ dependents.push({
855
+ kind: "element",
856
+ name: path,
857
+ description: messages().dependents.leavesView(path, diagram.id),
858
+ range: lines.range(diagram.node),
859
+ });
860
+ // What the entity's own removal would take, less what the relation's already takes.
861
+ const leaving = viewDependents(ctx, diagram, path);
862
+ const taken = new Set(statements.map((s) => s.start));
863
+ const listed = new Set(dependents.map((d) => `${d.range.start.offset}:${d.description}`));
864
+ statements.push(...leaving.statements.filter((s) => !taken.has(s.start)));
865
+ edits.push(...leaving.edits);
866
+ dependents.push(...leaving.dependents.filter((d) => !listed.has(`${d.range.start.offset}:${d.description}`)));
867
+ }
868
+ }
869
+ return { statements, edits, dependents };
870
+ }
581
871
  export function removeModelRelation(ctx, op) {
582
872
  const relation = requireModelRelation(ctx, op.relation);
583
873
  const statements = [relation.node];
584
- // View statements that name the relation by its key.
874
+ const edits = [];
875
+ const dependents = [];
585
876
  for (const diagram of ctx.compilation.document.diagrams) {
586
877
  const c4 = diagram.c4;
587
878
  if (!c4 || c4.resolved?.file !== null || c4.resolved.definition.id !== ctx.model.id)
588
879
  continue;
589
- // Combinations that stand for this relation only go too, with what names them.
590
- const keys = new Set([relation.key]);
591
- for (const drawn of diagram.relations) {
592
- if (isCombination(diagram, drawn.key) && drawn.sources.every((k) => k === relation.key)) {
593
- keys.add(drawn.key);
594
- statements.push(drawn.node);
595
- }
596
- }
597
- for (const entry of diagram.placement)
598
- if (keys.has(entry.target))
599
- statements.push(entry.node);
600
- for (const label of c4.labels)
601
- if (keys.has(label.target))
602
- statements.push(label.node);
603
- for (const entry of [...diagram.styles, ...diagram.animations]) {
604
- if (keys.has(entry.target))
605
- statements.push(entry.node);
606
- }
880
+ const found = relationStatements(ctx, diagram, relation.key, diagram.id !== op.view);
881
+ statements.push(...found.statements);
882
+ edits.push(...found.edits);
883
+ dependents.push(...found.dependents);
607
884
  }
608
- const edits = deleteStatements(ctx.text, ctx.compilation.parse.root, statements);
885
+ if (!op.cascade && dependents.length > 0) {
886
+ reject("edit.has-dependents", reasons().c4.removingRelation(relation.key, dependents.length), dependents);
887
+ }
888
+ edits.push(...deleteStatements(ctx.text, ctx.compilation.parse.root, statements));
889
+ verifyNoNewErrors(ctx, edits);
890
+ return edits;
891
+ }
892
+ /**
893
+ * Takes a model relation out of a view whose model is in another file: the statements that name
894
+ * it go (with `cascade`), or are listed as dependents, with the view as drawing it.
895
+ */
896
+ export function forgetModelRelation(ctx, op) {
897
+ requireView(ctx);
898
+ const { statements, edits, dependents } = relationStatements(ctx, ctx.diagram, op.relation, op.listDrawn ?? true);
899
+ if (!op.cascade && dependents.length > 0) {
900
+ reject("edit.has-dependents", reasons().c4.removingRelation(op.relation, dependents.length), dependents);
901
+ }
902
+ edits.push(...deleteStatements(ctx.text, ctx.compilation.parse.root, statements));
609
903
  verifyNoNewErrors(ctx, edits);
610
904
  return edits;
611
905
  }