Add strikethrough shortcut, fenced code toggle, and CodeMirror-aware undo/redo

- Bind Ctrl+Shift+S to strikethrough
- Smart code toggle: fenced block for multi-line selections, inline otherwise
- Link editing: selecting an existing link now selects its URL for editing
- Route undo/redo shortcuts through IPC so CodeMirror history is used when
  the editor has focus instead of Electron's native menu-role undo/redo
- Add Indent/Outdent toolbar buttons
This commit is contained in:
avi 2026-08-17 13:02:42 -05:00
commit 45990bbd62
9 changed files with 185 additions and 12 deletions

View file

@ -28,6 +28,7 @@ const {
toggleItalic,
toggleStrikethrough,
toggleInlineCode,
toggleCode,
toggleLink,
setHeading,
toggleBulletList,
@ -55,7 +56,12 @@ function target(doc, anchor, head = anchor) {
return true;
},
};
return { t, text: () => state.doc.toString(), sel: () => state.selection.main };
return {
t,
text: () => state.doc.toString(),
sel: () => state.selection.main,
select: (a, h = a) => (state = state.update({ selection: { anchor: a, head: h } }).state),
};
}
let failures = 0;
@ -147,6 +153,38 @@ function eq(actual, expected, msg) {
eq(text(), "a[](url)b", "link on empty cursor inserts empty label");
eq(sel().from, 2, "link cursor inside label brackets");
}
{
const { t, text, sel } = target("see [docs](old-url) here", 4, 19);
toggleLink(t);
eq(text(), "see [docs](old-url) here", "link edit does not change text");
eq(sel().from, 11, "link edit selects url (from)");
eq(sel().to, 18, "link edit selects url (to)");
}
// ---- code: inline vs fenced ----
{
const { t, text } = target("var x = 1;", 0, 9);
toggleCode(t);
eq(text(), "`var x = 1`;", "code on single line uses inline markers");
}
{
const { t, text, select } = target("line one\nline two", 0, 16);
toggleCode(t);
eq(text(), "```\nline one\nline two\n```", "code on multiple lines uses a fenced block");
select(0, 25);
toggleCode(t);
eq(text(), "line one\nline two", "fenced code toggles back off");
}
{
const { t, text } = target("before\n```\ncode a\ncode b\n```\nafter", 7, 24);
toggleCode(t);
eq(text(), "before\ncode a\ncode b\nafter", "fenced code unwraps when selection starts at the fence");
}
{
const { t, text } = target("abc", 1);
toggleCode(t);
eq(text(), "a``bc", "code on empty cursor inserts inline markers");
}
// ---- headings ----
{

View file

@ -141,12 +141,50 @@ const BOOK_DIR = __BOOKDIR_VALUE__;
await new Promise((r) => setTimeout(r, 50));
out.redoOk = cm.state.doc.toString().startsWith("## ");
cm.dispatch({ selection: { anchor: 15, head: 20 } }); // "World"
cm.contentDOM.dispatchEvent(
new KeyboardEvent("keydown", {
key: "s",
ctrlKey: true,
shiftKey: true,
bubbles: true,
cancelable: true,
})
);
await new Promise((r) => setTimeout(r, 50));
out.strikeShortcutOk = cm.state.doc.toString().includes("~~World~~");
cm.dispatch({
changes: { from: 0, to: 0, insert: "line one\nline two\n" },
selection: { anchor: 0, head: 16 },
});
document.querySelector('[data-cmd="code"]').click();
await new Promise((r) => setTimeout(r, 50));
out.fencedCodeOk = cm.state.doc.toString().startsWith("```\nline one\nline two\n```");
await new Promise((r) => setTimeout(r, 1200)); // wait for autosave debounce
out.formattingAutosaved =
(await window.folio.getChapterContent(typedId)).includes("**Hello**");
}
out.all = true;
const okFlags = [
"dndMoved",
"kbMoved",
"modalShown",
"uiCreated",
"hasEditor",
"editorAutosaved",
"toolbarBoldOk",
"focusAfterBold",
"shortcutBoldOk",
"headingOk",
"undoOk",
"redoOk",
"strikeShortcutOk",
"fencedCodeOk",
"formattingAutosaved",
];
out.all = okFlags.every((f) => out[f] === true);
} catch (e) {
out.error = String((e && e.message) || e);
}