Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/frontend/src/analysis/analysis_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function AnalysisNotebookEditor(props: { liveAnalysis: LiveAnalysisDoc })
notebook={liveDoc().doc.notebook}
changeNotebook={(f) => liveDoc().changeDoc((doc) => f(doc.notebook))}
formalCellEditor={AnalysisCellEditor}
cellConstructors={cellConstructors()}
cellConstructors={cellConstructors}
noShortcuts={true}
/>
</LiveAnalysisContext.Provider>
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/diagram/diagram_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function DiagramNotebookEditor(props: { liveDiagram: LiveDiagramDoc }) {
liveDoc().changeDoc((doc) => f(doc.notebook));
}}
formalCellEditor={DiagramCellEditor}
cellConstructors={cellConstructors()}
cellConstructors={cellConstructors}
cellLabel={judgmentLabel}
duplicateCell={duplicateDiagramJudgment}
/>
Expand Down
23 changes: 17 additions & 6 deletions packages/frontend/src/model/model_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import {
export function ModelNotebookEditor(props: { liveModel: LiveModelDoc }) {
const liveDoc = () => props.liveModel.liveDoc;

const cellConstructors = () => {
const cellConstructors = (cellType?: string, cellName?: string) => {
const theory = props.liveModel.theory();
return theory ? modelCellConstructors(theory) : [];
return theory ? modelCellConstructors(theory, cellType, cellName) : [];
};

return (
Expand All @@ -41,7 +41,7 @@ export function ModelNotebookEditor(props: { liveModel: LiveModelDoc }) {
liveDoc().changeDoc((doc) => f(doc.notebook));
}}
formalCellEditor={ModelCellEditor}
cellConstructors={cellConstructors()}
cellConstructors={cellConstructors}
cellLabel={judgmentLabel}
duplicateCell={duplicateModelJudgment}
/>
Expand Down Expand Up @@ -94,9 +94,13 @@ export function ModelCellEditor(props: FormalCellEditorProps<ModelJudgment>) {
);
}

function modelCellConstructors(theory: Theory): CellConstructor<ModelJudgment>[] {
function modelCellConstructors(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to implement this feature by changing modelCellConstructors. As the name suggests, "cell constructors" are functions that construct new notebook cells. This is quite different in scoping to mutating existing cells of a given type.

theory: Theory,
cellType?: string,
cellName?: string,
): CellConstructor<ModelJudgment>[] {
const constructors: CellConstructor<ModelJudgment>[] = [];
if (theory.theory.canInstantiateModels()) {
if (theory.theory.canInstantiateModels() && !cellType && !cellName) {
constructors.push({
name: "Instantiate",
description: "Instantiate an existing model into this one",
Expand All @@ -106,8 +110,15 @@ function modelCellConstructors(theory: Theory): CellConstructor<ModelJudgment>[]
},
});
}

for (const meta of theory.modelTypes ?? []) {
constructors.push(modelCellConstructor(meta));
if (cellName && cellType) {
if (meta.name !== cellName && meta.tag === cellType) {
constructors.push(modelCellConstructor(meta));
}
} else {
constructors.push(modelCellConstructor(meta));
}
}
return constructors;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/frontend/src/notebook/notebook_cell.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,13 @@
/* border-radius: 50%; */
top: -2px;
}

button.plain {
display: flex;
align-items: center;
gap: 4px;
background: transparent;
border: none;
color: darkgray;
font-size: 11pt;
}
40 changes: 37 additions & 3 deletions packages/frontend/src/notebook/notebook_cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ArrowUp from "lucide-solid/icons/arrow-up";
import Copy from "lucide-solid/icons/copy";
import GripVertical from "lucide-solid/icons/grip-vertical";
import Plus from "lucide-solid/icons/plus";
import SquarePen from "lucide-solid/icons/square-pen";
import Trash2 from "lucide-solid/icons/trash-2";
import type { EditorView } from "prosemirror-view";
import { createEffect, createSignal, type JSX, onCleanup, Show } from "solid-js";
Expand Down Expand Up @@ -108,6 +109,8 @@ export function NotebookCell(props: {
tag?: string;
currentDropTarget: string | null;
setCurrentDropTarget: (cellId: string | null) => void;
isActive: boolean;
replaceCommands: Completion[];
}) {
let rootRef!: HTMLDivElement;
let handleRef!: HTMLButtonElement;
Expand Down Expand Up @@ -248,13 +251,44 @@ export function NotebookCell(props: {
<div class="drop-indicator-with-dots" />
</Show>
</div>
<Show when={props.tag}>
<div class="cell-tag">{props.tag}</div>
</Show>
<CellSwitcher tag={props.tag} completions={props.replaceCommands}></CellSwitcher>
</div>
);
}

export function CellSwitcher(props: { tag: string | undefined; completions: Completion[] }) {
const [isSwitchMenuOpen, setSwitchMenuOpen] = createSignal(false);
const openSwitchMenu = () => setSwitchMenuOpen(true);
const closeSwitchMenu = () => setSwitchMenuOpen(false);

return (
<Show when={props.tag}>
<Popover
open={isSwitchMenuOpen()}
onOpenChange={setSwitchMenuOpen}
floatingOptions={{
autoPlacement: {
allowedPlacements: ["left-start", "bottom-start", "top-start"],
},
}}
trapFocus={false}
>
<Popover.Anchor as="span">
<button type="button" class="plain flex items-center" onClick={openSwitchMenu}>
{props.tag}
<SquarePen size={16} />
</button>
</Popover.Anchor>
<Popover.Portal>
<Popover.Content class="popup">
<Completions completions={props.completions} onComplete={closeSwitchMenu} />
</Popover.Content>
</Popover.Portal>
</Popover>
</Show>
);
}

/** Editor for rich text cells, a simple wrapper around `RichTextEditor`.
*/
export function RichTextCellEditor(
Expand Down
97 changes: 82 additions & 15 deletions packages/frontend/src/notebook/notebook_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
keyEventHasModifier,
type ModifierKey,
} from "catcolab-ui-components";
import type { Cell, Notebook } from "catlog-wasm";
import type { Cell, MorType, Notebook, ObType } from "catlog-wasm";
import {
type CellActions,
type FormalCellEditorProps,
Expand Down Expand Up @@ -72,7 +72,7 @@ export function NotebookEditor<T>(props: {
changeNotebook: (f: (nb: Notebook<T>) => void) => void;

formalCellEditor: Component<FormalCellEditorProps<T>>;
cellConstructors?: CellConstructor<T>[];
cellConstructors: (cellType?: string, cellName?: string) => CellConstructor<T>[];
cellLabel?: (content: T) => string | undefined;

/** Called to duplicate an existing cell.
Expand Down Expand Up @@ -139,14 +139,56 @@ export function NotebookEditor<T>(props: {
});
};

const cellConstructors = (): CellConstructor<T>[] => [
{
name: "Text",
description: "Start writing text",
shortcut: ["T"],
construct: () => newRichTextCell(),
},
...(props.cellConstructors ?? []),
const isFormalCell = (cell: Cell<T>): cell is { tag: "formal"; id: string; content: T } => {
return cell.tag === "formal";
};

const isObType = (content: unknown): content is { tag: "object"; obType: ObType } => {
return (
typeof content === "object" &&
content !== null &&
"tag" in content &&
content.tag === "object"
);
};

const isMorType = (content: unknown): content is { tag: "morphism"; morType: MorType } => {
return (
typeof content === "object" &&
content !== null &&
"tag" in content &&
content.tag === "morphism"
);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A hint that this design is off is that notebook editor is not supposed to know anything about theories, models, etc. It's a completely generic component.


const retypeCellAs = (i: number, newCell: Cell<T>) => {
if (!newCell || !isFormalCell(newCell)) {
return;
}
const mutator = (cellContent: T) => {
if (isObType(cellContent) && isObType(newCell.content)) {
cellContent.obType = newCell.content.obType;
} else if (isMorType(cellContent) && isMorType(newCell.content)) {
cellContent.morType = newCell.content.morType;
}
};
props.changeNotebook((nb) => {
NotebookUtils.retypeCell(nb, i, mutator);
});
};

const cellConstructors = (cellType?: string, cellName?: string): CellConstructor<T>[] => [
...(cellType && cellName
? []
: [
{
name: "Text",
description: "Start writing text",
shortcut: ["T"],
construct: () => newRichTextCell(),
},
]),
...(props.cellConstructors(cellType, cellName) ?? []),
];

const replaceCommands = (i: number): Completion[] =>
Expand All @@ -160,6 +202,17 @@ export function NotebookEditor<T>(props: {
};
});

const retypeCommands = (i: number, cellType?: string, cellName?: string): Completion[] =>
cellConstructors(cellType, cellName).map((cc) => {
const { name, description, shortcut } = cc;
return {
name,
description,
shortcut,
onComplete: () => retypeCellAs(i, cc.construct()),
};
});

makeEventListener(window, "keydown", (evt) => {
if (props.noShortcuts) {
return;
Expand Down Expand Up @@ -289,6 +342,18 @@ export function NotebookEditor<T>(props: {
const cell = props.notebook.cellContents[cellId];
invariant(cell, `Failed to find contents for cell '${cellId}'`);

const cellName = (cell: Cell<T>) =>
(cell.tag === "formal"
? props.cellLabel?.(cell.content)
: undefined) as string;

const cellType = (cell: Cell<T>) =>
cell.tag === "formal"
? isObType(cell.content)
? "ObType"
: "MorType"
: undefined;

if (cell.tag !== "rich-text") {
cellActions.duplicate = () => {
const index = i();
Expand All @@ -309,13 +374,15 @@ export function NotebookEditor<T>(props: {
cellId={cell.id}
index={i()}
actions={cellActions}
tag={
cell.tag === "formal"
? props.cellLabel?.(cell.content)
: undefined
}
tag={cellName(cell)}
currentDropTarget={currentDropTarget()}
setCurrentDropTarget={setCurrentDropTarget}
isActive={isActive()}
replaceCommands={retypeCommands(
i(),
cellType(cell),
cellName(cell),
)}
>
<Switch>
<Match when={cell.tag === "rich-text"}>
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/notebook/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ export namespace NotebookUtils {
return notebook.cellOrder.some((cellId) => notebook.cellContents[cellId]?.tag === "formal");
}

export function retypeCell<T>(
notebook: Notebook<T>,
index: number,
mutator: (cellContent: T) => void,
) {
const cellId = getCellIdByIndex(notebook, index);
cellId && mutateCellContentById(notebook, cellId, mutator);
}

export function numCells<T>(notebook: Notebook<T>): number {
return notebook.cellOrder.length;
}
Expand Down