diff --git a/packages/plugin-toolbar/src/formatting.ts b/packages/plugin-toolbar/src/formatting.ts
index d261c381..5e4234e0 100644
--- a/packages/plugin-toolbar/src/formatting.ts
+++ b/packages/plugin-toolbar/src/formatting.ts
@@ -69,9 +69,8 @@ function toggleLinePrefix(editor: EditorAPI, prefix: string): boolean {
const line = doc.slice(lineStart, lineEnd);
const newLine = line.startsWith(prefix) ? line.slice(prefix.length) : prefix + line;
- const newDoc = doc.slice(0, lineStart) + newLine + doc.slice(lineEnd);
- editor.setDocument(newDoc);
- editor.setSelection(lineStart + newLine.length);
+ const selection = { anchor: lineStart + newLine.length };
+ editor.replaceRange(lineStart, lineEnd, newLine, selection);
return true;
}
@@ -135,12 +134,10 @@ export function insertCodeBlock(editor: EditorAPI): boolean {
"```\n" + (selected || "") + "\n```" +
(needsTrailingNewline ? "\n" : "");
- const newDoc = doc.slice(0, from) + block + doc.slice(to);
- editor.setDocument(newDoc);
-
// Place cursor on the language line (after ```)
const langPos = from + (needsLeadingNewline ? 1 : 0) + 3;
- editor.setSelection(langPos);
+ const selection = { anchor: langPos };
+ editor.replaceRange(from, to, block, selection);
return true;
}
@@ -153,12 +150,14 @@ export function insertImage(editor: EditorAPI): boolean {
const alt = selected || "alt text";
const md = ``;
- const newDoc = doc.slice(0, from) + md + doc.slice(to);
- editor.setDocument(newDoc);
// Select the "url" part
const urlStart = from + alt.length + 4;
- editor.setSelection(urlStart, urlStart + 3);
+ const selection = {
+ anchor: urlStart,
+ head: urlStart + 3,
+ };
+ editor.replaceRange(from, to, md, selection);
return true;
}
@@ -172,10 +171,12 @@ export function applyTextColor(editor: EditorAPI, color: string): boolean {
if (from === to) return false;
const wrapped = `${selected}`;
- const newDoc = doc.slice(0, from) + wrapped + doc.slice(to);
- editor.setDocument(newDoc);
const innerStart = from + ``.length;
- editor.setSelection(innerStart, innerStart + selected.length);
+ const selection = {
+ anchor: innerStart,
+ head: innerStart + selected.length,
+ };
+ editor.replaceRange(from, to, wrapped, selection);
return true;
}
@@ -189,10 +190,12 @@ export function applyHighlight(editor: EditorAPI, color: string): boolean {
if (from === to) return false;
const wrapped = `${selected}`;
- const newDoc = doc.slice(0, from) + wrapped + doc.slice(to);
- editor.setDocument(newDoc);
const innerStart = from + ``.length;
- editor.setSelection(innerStart, innerStart + selected.length);
+ const selection = {
+ anchor: innerStart,
+ head: innerStart + selected.length,
+ };
+ editor.replaceRange(from, to, wrapped, selection);
return true;
}
@@ -203,8 +206,7 @@ export function insertHorizontalRule(editor: EditorAPI): boolean {
const needsLeadingNewline = anchor > 0 && doc[anchor - 1] !== "\n";
const hr = (needsLeadingNewline ? "\n" : "") + "---\n";
- const newDoc = doc.slice(0, anchor) + hr + doc.slice(anchor);
- editor.setDocument(newDoc);
- editor.setSelection(anchor + hr.length);
+ const selection = { anchor: anchor + hr.length };
+ editor.replaceRange(anchor, anchor, hr, selection);
return true;
}
diff --git a/packages/plugin-toolbar/src/index.ts b/packages/plugin-toolbar/src/index.ts
index 444d71d3..338aaffc 100644
--- a/packages/plugin-toolbar/src/index.ts
+++ b/packages/plugin-toolbar/src/index.ts
@@ -26,20 +26,23 @@ export function toggleWrap(editor: EditorAPI, marker: string): boolean {
if (before === marker && after === marker) {
// Already wrapped — remove markers
- const newDoc =
- doc.slice(0, from - marker.length) +
- selected +
- doc.slice(to + marker.length);
- editor.setDocument(newDoc);
- editor.setSelection(from - marker.length, to - marker.length);
+ const replaceFrom = from - marker.length;
+ const replaceTo = to + marker.length;
+ const selection = {
+ anchor: replaceFrom,
+ head: replaceFrom + selected.length,
+ };
+ editor.replaceRange(replaceFrom, replaceTo, selected, selection);
return true;
}
// Wrap selection with markers
- const newDoc =
- doc.slice(0, from) + marker + selected + marker + doc.slice(to);
- editor.setDocument(newDoc);
- editor.setSelection(from + marker.length, to + marker.length);
+ const newContent = marker + selected + marker;
+ const selection = {
+ anchor: from + marker.length,
+ head: from + marker.length + selected.length,
+ };
+ editor.replaceRange(from, to, newContent, selection);
return true;
}
@@ -68,12 +71,14 @@ export function insertLink(editor: EditorAPI): boolean {
const linkText = selected || "link text";
const md = `[${linkText}](url)`;
- const newDoc = doc.slice(0, from) + md + doc.slice(to);
- editor.setDocument(newDoc);
// Select the "url" part for easy replacement
const urlStart = from + linkText.length + 3;
- editor.setSelection(urlStart, urlStart + 3);
+ const selection = {
+ anchor: urlStart,
+ head: urlStart + 3,
+ };
+ editor.replaceRange(from, to, md, selection);
return true;
}
@@ -100,9 +105,8 @@ export function toggleHeading(editor: EditorAPI, level: number): boolean {
newLine = prefix + line;
}
- const newDoc = doc.slice(0, lineStart) + newLine + doc.slice(end);
- editor.setDocument(newDoc);
- editor.setSelection(lineStart + newLine.length);
+ const selection = { anchor: lineStart + newLine.length };
+ editor.replaceRange(lineStart, end, newLine, selection);
return true;
}
diff --git a/packages/plugin-toolbar/test/plugin-toolbar.test.ts b/packages/plugin-toolbar/test/plugin-toolbar.test.ts
index 3fc97f53..53159546 100644
--- a/packages/plugin-toolbar/test/plugin-toolbar.test.ts
+++ b/packages/plugin-toolbar/test/plugin-toolbar.test.ts
@@ -606,3 +606,66 @@ describe("toggleUnorderedList — atomic undo", () => {
editor.destroy();
});
});
+
+describe("toggleBold — atomic undo", () => {
+ it("bold toggle → single undo fully restores document", () => {
+ const container = document.createElement("div");
+ const original = "hello world";
+ const editor = createEditor({ container, initialValue: original, plugins: [createHistoryPlugin()] });
+
+ editor.setSelection(6, 11);
+ toggleBold(editor);
+ expect(editor.getDocument()).toBe("hello **world**");
+
+ expect(editor.undo()).toBe(true);
+ expect(editor.getDocument()).toBe(original);
+ expect(editor.undo()).toBe(false);
+ editor.destroy();
+ });
+
+ it("bold toggle → undo → redo recovers bold state in one step", () => {
+ const container = document.createElement("div");
+ const editor = createEditor({ container, initialValue: "hello world", plugins: [createHistoryPlugin()] });
+
+ editor.setSelection(6, 11);
+ toggleBold(editor);
+ editor.undo();
+ expect(editor.redo()).toBe(true);
+ expect(editor.getDocument()).toBe("hello **world**");
+ editor.destroy();
+ });
+});
+
+describe("insertLink — atomic undo", () => {
+ it("insert link → single undo fully restores document", () => {
+ const container = document.createElement("div");
+ const original = "click here";
+ const editor = createEditor({ container, initialValue: original, plugins: [createHistoryPlugin()] });
+
+ editor.setSelection(6, 10);
+ insertLink(editor);
+ expect(editor.getDocument()).toBe("click [here](url)");
+
+ expect(editor.undo()).toBe(true);
+ expect(editor.getDocument()).toBe(original);
+ expect(editor.undo()).toBe(false);
+ editor.destroy();
+ });
+});
+
+describe("toggleHeading — atomic undo", () => {
+ it("heading toggle → single undo fully restores document", () => {
+ const container = document.createElement("div");
+ const original = "Title";
+ const editor = createEditor({ container, initialValue: original, plugins: [createHistoryPlugin()] });
+
+ editor.setSelection(2);
+ toggleHeading(editor, 2);
+ expect(editor.getDocument()).toBe("## Title");
+
+ expect(editor.undo()).toBe(true);
+ expect(editor.getDocument()).toBe(original);
+ expect(editor.undo()).toBe(false);
+ editor.destroy();
+ });
+});