diff --git a/src/canvas/edge.test.ts b/src/canvas/edge.test.ts new file mode 100644 index 0000000..cbe5670 --- /dev/null +++ b/src/canvas/edge.test.ts @@ -0,0 +1,16 @@ +import Edge from "./edge"; +import edge from "./edge"; + +describe("id", () => { +test("should create an id from an edge", () => { + const edge = { sourceId: "1", targetId: "2" }; + expect(Edge.id(edge)).toEqual("1-->2"); + }); +}); + +describe("parseId", () => { + test("should return an edge from an id", () => { + const id = "1-->2"; + expect(Edge.parseId(id)).toEqual({ sourceId: "1", targetId: "2" }); + }); +}); diff --git a/src/canvas/edge.ts b/src/canvas/edge.ts index 201413a..e77b827 100644 --- a/src/canvas/edge.ts +++ b/src/canvas/edge.ts @@ -3,6 +3,13 @@ interface Edge { targetId: string; } +const ArrowSymbol = "-->"; + export default { - id: (edge: Edge): string => `${edge.sourceId}-->${edge.targetId}`, + id: (edge: Edge): string => `${edge.sourceId}${ArrowSymbol}${edge.targetId}`, + + parseId: (edgeId: string): Edge => { + const [sourceId, targetId] = edgeId.split(ArrowSymbol); + return { sourceId, targetId }; + }, }; diff --git a/src/interface/control.ts b/src/interface/control.ts index 8959708..01e9bba 100644 --- a/src/interface/control.ts +++ b/src/interface/control.ts @@ -3,7 +3,7 @@ import { html } from "htm/react"; import { Store } from "redux"; import { ApplicationState } from "../store"; -import { deleteAtom, evalSelectedAtom } from "../store/defaultReducer"; +import { deleteAtom, evalSelectedAtom, deleteEdge } from "../store/defaultReducer"; import { selectors as canvasSelectors } from "../canvas"; import PlayIcon from "./play_icon"; import TrashIcon from "./trash_icon"; @@ -11,7 +11,7 @@ import CastIcon from "./cast_icon"; import BookIcon from "./book_icon"; import { actions } from "./interfaceReducer"; -const { getSelectedAtomId } = canvasSelectors; +const { getSelectedAtomId, getSelectedEdgeId } = canvasSelectors; const { toggleTranscript } = actions; interface Props { @@ -20,10 +20,12 @@ interface Props { export default function Control({ store }: Props) { const [selectedAtomId, setSelectedAtomId] = useState(null); + const [selectedEdgeId, setSelectedEdgeId] = useState(null); const [connectedToRepl, setConnectedToRepl] = useState(false); store.subscribe(() => { setSelectedAtomId(getSelectedAtomId(store.getState())); + setSelectedEdgeId(getSelectedEdgeId(store.getState())); setConnectedToRepl(store.getState().default.connectedToRepl); }); @@ -36,7 +38,8 @@ export default function Control({ store }: Props) { const onDeleteClick = (event) => { event.preventDefault(); event.stopPropagation(); - store.dispatch(deleteAtom(selectedAtomId)); + if (selectedAtomId) store.dispatch(deleteAtom(selectedAtomId)); + if (selectedEdgeId) store.dispatch(deleteEdge(selectedEdgeId)); } const onTranscriptClick = (event) => { @@ -58,7 +61,7 @@ export default function Control({ store }: Props) { <${PlayIcon} /> diff --git a/src/keyboard/index.ts b/src/keyboard/index.ts index 8d2cf4e..33fe955 100644 --- a/src/keyboard/index.ts +++ b/src/keyboard/index.ts @@ -2,7 +2,7 @@ import * as Mousetrap from "mousetrap"; import { ApplicationState } from "../store"; import { deleteAtom as deleteAtomAction, addAtom, connectAtoms, childrenSelector, - evalSelectedAtom, parentSelector, deepChildrenSelector + evalSelectedAtom, parentSelector, deepChildrenSelector, deleteEdge } from "../store/defaultReducer"; import { buildNodeGeometry } from "../canvas/node_geometry"; import { createAtom } from "../store/atom"; @@ -16,30 +16,44 @@ import { remote } from "electron"; const { dialog } = remote; const { select, unselect, changeMode } = actions; const { toggleTranscript } = interfaceActions; -const { getSelectedAtom, getMode } = selectors; +const { getSelectedAtom, getMode, getSelectedEdgeId } = selectors; export default function Keyboard(store: Store) { let state = store.getState(); let mode = getMode(store.getState()); let selectedAtom = getSelectedAtom(store.getState()); + let selectedEdgeId = getSelectedEdgeId(store.getState()); store.subscribe(() => { state = store.getState(); mode = getMode(store.getState()); selectedAtom = getSelectedAtom(store.getState()); + selectedEdgeId = getSelectedEdgeId(store.getState()); }); const standardAtomOffset = 40; const evaluateAtom = () => store.dispatch(evalSelectedAtom()); - const deleteAtom = (event) => { - if (!selectedAtom) return; - - event.preventDefault(); + const deleteSelectedAtom = () => { const parent = parentSelector(state.default, selectedAtom.id); store.dispatch(deleteAtomAction(selectedAtom.id)); if (parent) store.dispatch(select(parent.id)); }; + + const deleteSelectedEdge = () => store.dispatch(deleteEdge(selectedEdgeId)); + + const deleteSelectedElement = (event) => { + if (selectedAtom) { + event.preventDefault(); + deleteSelectedAtom(); + } + + if (selectedEdgeId) { + event.preventDefault(); + deleteSelectedEdge(); + } + }; + const createChildAtom = (event) => { if (!selectedAtom) return; @@ -163,7 +177,7 @@ export default function Keyboard(store: Store) { }; Mousetrap.bind("command+e", evaluateAtom); - Mousetrap.bind("command+backspace", deleteAtom); + Mousetrap.bind("command+backspace", deleteSelectedElement); Mousetrap.bind("tab", createChildAtom); Mousetrap.bind("command+enter", handleCmdEnter); Mousetrap.bind("esc", handleEsc); diff --git a/src/store/defaultReducer.ts b/src/store/defaultReducer.ts index 0847b41..f07b584 100644 --- a/src/store/defaultReducer.ts +++ b/src/store/defaultReducer.ts @@ -5,6 +5,7 @@ import { buildNodeGeometry } from "../canvas/node_geometry"; import { Line } from "../canvas/geometry"; import { ApplicationState } from "."; import { EvalResult } from "../repl"; +import Edge from "../canvas/edge"; export interface DefaultState { atoms: { [id: string]: Atom }; @@ -43,6 +44,10 @@ export const setAtomValue = export const evalSelectedAtom = () => ({ type: "eval-selected-atom" }); +export const deleteEdge = (edgeId: string) => ( + { type: "delete-edge", payload: Edge.parseId(edgeId) } +); + const initialState: DefaultState = { atoms: {}, edges: [], @@ -66,6 +71,12 @@ const reducer = createReducer(initialState, { return !isSource && !isTarget; }); }, + "delete-edge": (state, action) => { + const notMatchingEdge = (signature) => ({ sourceId, targetId }) => ( + !(sourceId === signature.sourceId && targetId === signature.targetId) + ); + state.edges = state.edges.filter(notMatchingEdge(action.payload)); + }, "add-atom": (state, action) => { state.atoms[action.payload.id] = action.payload; },