Skip to content

Commit c523923

Browse files
committed
ENH: Editor variants, a petri-net string diagram editor variant
1 parent 8df5025 commit c523923

15 files changed

Lines changed: 789 additions & 75 deletions

packages/frontend/src/model/document.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import type { ValidatedModel } from "./model_library";
1111
export type ModelDocument = Document & { type: "model" };
1212

1313
/** Create an empty model document. */
14-
export const newModelDocument = (theory: string): ModelDocument => ({
14+
export const newModelDocument = (theory: string, editorVariant?: string): ModelDocument => ({
1515
name: "",
1616
type: "model",
1717
theory,
18+
...(editorVariant ? { editorVariant } : {}),
1819
notebook: newNotebook<ModelJudgment>(),
1920
version: currentVersion(),
2021
});
@@ -57,37 +58,72 @@ export async function createModel(
5758
return api.createDoc(init);
5859
}
5960

60-
/** Migrate a model document from one theory to another. */
61-
export async function migrateModelDocument(
61+
/** Migrate a model document to a different theory, or switch its editor variant.
62+
*/
63+
export async function switchTheoryOrEditor(
6264
liveModel: LiveModelDoc,
63-
targetTheoryId: string,
65+
editorOrModelId: string,
6466
theories: TheoryLibrary,
6567
) {
6668
const { doc, changeDoc } = liveModel.liveDoc;
67-
const targetTheory = await theories.get(targetTheoryId);
69+
70+
let targetBaseId: string = editorOrModelId;
71+
let targetEditorVariant: string | undefined;
72+
const isEditor = theories.isEditorVariant(editorOrModelId);
73+
if (isEditor) {
74+
const base = theories.getBaseTheoryId(editorOrModelId);
75+
invariant(base !== undefined, "Editor variant must have a base theory");
76+
targetBaseId = base;
77+
targetEditorVariant = editorOrModelId;
78+
}
79+
80+
// If only the editor variant is changing (same base theory), just flip the field.
81+
if (targetBaseId === doc.theory) {
82+
changeDoc((doc) => {
83+
if (targetEditorVariant) {
84+
doc.editorVariant = targetEditorVariant;
85+
} else {
86+
delete doc.editorVariant;
87+
}
88+
});
89+
return;
90+
}
91+
92+
// Real theory migration below.
93+
const targetTheory = await theories.get(targetBaseId);
6894
const theory = liveModel.theory();
6995
let model = liveModel.elaboratedModel();
7096
invariant(theory && model); // FIXME: Should fail gracefully.
7197

7298
// Trivial migration.
73-
if (!NotebookUtils.hasFormalCells(doc.notebook) || theory.inclusions.includes(targetTheoryId)) {
99+
if (!NotebookUtils.hasFormalCells(doc.notebook) || theory.inclusions.includes(targetBaseId)) {
74100
changeDoc((doc) => {
75-
doc.theory = targetTheoryId;
101+
doc.theory = targetBaseId;
102+
if (targetEditorVariant) {
103+
doc.editorVariant = targetEditorVariant;
104+
} else {
105+
delete doc.editorVariant;
106+
}
76107
});
77108
return;
78109
}
79110

80111
// Pushforward migration.
81-
const migration = theory.pushforwards.find((m) => m.target === targetTheoryId);
112+
const migration = theory.pushforwards.find((m) => m.target === targetBaseId);
82113
if (!migration) {
83-
throw new Error(`No migration defined from ${theory.id} to ${targetTheoryId}`);
114+
throw new Error(`No migration defined from ${theory.id} to ${targetBaseId}`);
84115
}
85116
// TODO: We need a general method to propagate changes from catlog models to
86117
// notebooks. This stop-gap solution only works because pushforward
87118
// migration doesn't have to create/delete cells, only update types.
88119
model = migration.migrate(model, targetTheory.theory);
89120
changeDoc((doc) => {
90-
doc.theory = targetTheoryId;
121+
doc.theory = targetBaseId;
122+
if (targetEditorVariant) {
123+
doc.editorVariant = targetEditorVariant;
124+
} else {
125+
delete doc.editorVariant;
126+
}
91127
for (const judgment of NotebookUtils.getFormalContent(doc.notebook)) {
92128
if (judgment.tag === "object") {
93129
judgment.obType = model.obType({
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Component } from "solid-js";
2+
3+
import type { MorDecl, MorType, ObDecl, ObType } from "catlog-wasm";
4+
import type { CellActions } from "../notebook";
5+
import type { Theory } from "../theory";
6+
7+
/** Editor overrides for an editor variant of a theory.
8+
9+
Specifies which editor components should replace the defaults for particular
10+
object or morphism types.
11+
*/
12+
export type EditorVariantOverrides = {
13+
obEditors?: Array<{ obType: ObType; editor: Component<ObjectEditorProps> }>;
14+
morEditors?: Array<{ morType: MorType; editor: Component<MorphismEditorProps> }>;
15+
};
16+
17+
/** Props for an object cell editor component in a model. */
18+
export type ObjectEditorProps = {
19+
object: ObDecl;
20+
modifyObject: (f: (decl: ObDecl) => void) => void;
21+
isActive: boolean;
22+
actions: CellActions;
23+
theory: Theory;
24+
};
25+
26+
/** Props for a morphism cell editor component in a model. */
27+
export type MorphismEditorProps = {
28+
morphism: MorDecl;
29+
modifyMorphism: (f: (decl: MorDecl) => void) => void;
30+
isActive: boolean;
31+
actions: CellActions;
32+
theory: Theory;
33+
};

packages/frontend/src/model/model_editor.tsx

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { deepEqual } from "fast-equals";
12
import { Match, Switch, useContext } from "solid-js";
3+
import { Dynamic } from "solid-js/web";
24
import invariant from "tiny-invariant";
35

46
import type { InstantiatedModel, ModelJudgment, MorDecl, ObDecl } from "catlog-wasm";
@@ -8,7 +10,7 @@ import {
810
NotebookEditor,
911
newFormalCell,
1012
} from "../notebook";
11-
import type { ModelTypeMeta, Theory } from "../theory";
13+
import { TheoryLibraryContext, type ModelTypeMeta, type Theory } from "../theory";
1214
import { LiveModelContext } from "./context";
1315
import type { LiveModelDoc } from "./document";
1416
import { InstantiationCellEditor } from "./instantiation_cell_editor";
@@ -54,32 +56,60 @@ export function ModelNotebookEditor(props: { liveModel: LiveModelDoc }) {
5456
export function ModelCellEditor(props: FormalCellEditorProps<ModelJudgment>) {
5557
const liveModel = useContext(LiveModelContext);
5658
invariant(liveModel, "Live model should be provided as context");
59+
const theories = useContext(TheoryLibraryContext);
60+
61+
const editorOverrides = () => {
62+
const variantId = liveModel().liveDoc.doc.editorVariant;
63+
return variantId && theories ? theories.getEditorOverrides(variantId) : undefined;
64+
};
5765

5866
return (
5967
<Switch>
6068
<Match when={props.content.tag === "object" && liveModel().theory()}>
61-
{(theory) => (
62-
<ObjectCellEditor
63-
object={props.content as ObDecl}
64-
modifyObject={(f) => props.changeContent((content) => f(content as ObDecl))}
65-
isActive={props.isActive}
66-
actions={props.actions}
67-
theory={theory()}
68-
/>
69-
)}
69+
{(theory) => {
70+
const obDecl = () => props.content as ObDecl;
71+
const editor = () => {
72+
const override = editorOverrides()?.obEditors?.find((o) =>
73+
deepEqual(o.obType, obDecl().obType),
74+
);
75+
return override?.editor ?? ObjectCellEditor;
76+
};
77+
return (
78+
<Dynamic
79+
component={editor()}
80+
object={obDecl()}
81+
modifyObject={(f: (decl: ObDecl) => void) =>
82+
props.changeContent((content) => f(content as ObDecl))
83+
}
84+
isActive={props.isActive}
85+
actions={props.actions}
86+
theory={theory()}
87+
/>
88+
);
89+
}}
7090
</Match>
7191
<Match when={props.content.tag === "morphism" && liveModel().theory()}>
72-
{(theory) => (
73-
<MorphismCellEditor
74-
morphism={props.content as MorDecl}
75-
modifyMorphism={(f) =>
76-
props.changeContent((content) => f(content as MorDecl))
77-
}
78-
isActive={props.isActive}
79-
actions={props.actions}
80-
theory={theory()}
81-
/>
82-
)}
92+
{(theory) => {
93+
const morDecl = () => props.content as MorDecl;
94+
const editor = () => {
95+
const override = editorOverrides()?.morEditors?.find((o) =>
96+
deepEqual(o.morType, morDecl().morType),
97+
);
98+
return override?.editor ?? MorphismCellEditor;
99+
};
100+
return (
101+
<Dynamic
102+
component={editor()}
103+
morphism={morDecl()}
104+
modifyMorphism={(f: (decl: MorDecl) => void) =>
105+
props.changeContent((content) => f(content as MorDecl))
106+
}
107+
isActive={props.isActive}
108+
actions={props.actions}
109+
theory={theory()}
110+
/>
111+
);
112+
}}
83113
</Match>
84114
<Match when={props.content.tag === "instantiation"}>
85115
<InstantiationCellEditor

packages/frontend/src/model/model_info.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NotebookUtils } from "../notebook";
22
import { stdTheories } from "../stdlib";
33
import { TheorySelectorDialog } from "../theory/theory_selector";
4-
import { type LiveModelDoc, migrateModelDocument } from "./document";
4+
import { type LiveModelDoc, switchTheoryOrEditor } from "./document";
55

66
/** Widget in the top right corner of a model document pane.
77
*/
@@ -10,7 +10,13 @@ export function ModelInfo(props: { liveModel: LiveModelDoc }) {
1010

1111
const selectableTheories = () => {
1212
if (NotebookUtils.hasFormalCells(liveDoc().doc.notebook)) {
13-
return props.liveModel.theory()?.migrationTargets ?? [];
13+
const theory = props.liveModel.theory();
14+
if (!theory) {
15+
return [];
16+
}
17+
const baseId = liveDoc().doc.theory;
18+
const editorVariantIds = stdTheories.getEditorVariantIds(baseId);
19+
return [baseId, ...editorVariantIds, ...theory.migrationTargets];
1420
} else {
1521
// If the model has no formal cells, allow any theory to be selected.
1622
return undefined;
@@ -19,8 +25,10 @@ export function ModelInfo(props: { liveModel: LiveModelDoc }) {
1925

2026
return (
2127
<TheorySelectorDialog
22-
theoryMeta={stdTheories.getMetadata(liveDoc().doc.theory)}
23-
setTheory={(id) => migrateModelDocument(props.liveModel, id, stdTheories)}
28+
theoryMeta={stdTheories.getMetadata(
29+
liveDoc().doc.editorVariant ?? liveDoc().doc.theory,
30+
)}
31+
setTheory={(id) => switchTheoryOrEditor(props.liveModel, id, stdTheories)}
2432
theories={selectableTheories()}
2533
/>
2634
);

packages/frontend/src/model/morphism_cell_editor.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,16 @@ import { createMemo, createSignal, useContext } from "solid-js";
22
import invariant from "tiny-invariant";
33

44
import { NameInput } from "catcolab-ui-components";
5-
import type { MorDecl } from "catlog-wasm";
6-
import type { CellActions } from "../notebook";
7-
import type { Theory } from "../theory";
85
import { LiveModelContext } from "./context";
6+
import type { MorphismEditorProps } from "./editors";
97
import { obClasses } from "./object_cell_editor";
108
import { ObInput } from "./object_input";
119

1210
import arrowStyles from "../stdlib/arrow_styles.module.css";
1311
import "./morphism_cell_editor.css";
1412

1513
/** Editor for a morphism declaration cell in a model. */
16-
export function MorphismCellEditor(props: {
17-
morphism: MorDecl;
18-
modifyMorphism: (f: (decl: MorDecl) => void) => void;
19-
isActive: boolean;
20-
actions: CellActions;
21-
theory: Theory;
22-
}) {
14+
export function MorphismCellEditor(props: MorphismEditorProps) {
2315
const liveModel = useContext(LiveModelContext);
2416
invariant(liveModel, "Live model should be provided as context");
2517

packages/frontend/src/model/object_cell_editor.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
import { NameInput } from "catcolab-ui-components";
2-
import type { ObDecl, ObType } from "catlog-wasm";
3-
import type { CellActions } from "../notebook";
2+
import type { ObType } from "catlog-wasm";
43
import type { Theory } from "../theory";
4+
import type { ObjectEditorProps } from "./editors";
55

66
import "./object_cell_editor.css";
77

88
/** Editor for an object declaration cell in a model. */
9-
export function ObjectCellEditor(props: {
10-
object: ObDecl;
11-
modifyObject: (f: (decl: ObDecl) => void) => void;
12-
isActive: boolean;
13-
actions: CellActions;
14-
theory: Theory;
15-
}) {
9+
export function ObjectCellEditor(props: ObjectEditorProps) {
1610
const cssClasses = () => [
1711
"formal-judgment",
1812
"object-decl",

0 commit comments

Comments
 (0)