--- a/app/assets/javascripts/jstoolbar/jstoolbar.js
+++ b/app/assets/javascripts/jstoolbar/jstoolbar.js
@@ -307,6 +307,23 @@
     this.encloseSelection(stag,etag);
   },
 
+  // Replaces the [start, end) range of the textarea with subst, keeping the
+  // edit on the browser's native undo stack. Assigning textarea.value (or
+  // using setRangeText) wipes the undo history and fires no input event, so
+  // Ctrl+Z after a toolbar button loses everything typed before it. Returns
+  // false when execCommand is unavailable, so callers can fall back to the
+  // value assignment.
+  replaceRange: function(start, end, subst) {
+    if (typeof(document.execCommand) != 'function') { return false; }
+    this.textarea.setSelectionRange(start, end);
+    if (subst.length > 0) {
+      return document.execCommand('insertText', false, subst);
+    } else if (start != end) {
+      return document.execCommand('delete');
+    }
+    return true;
+  },
+
   encloseLineSelection: function(prefix, suffix, fn) {
     this.textarea.focus();
 
@@ -348,14 +365,22 @@
       range.move('character', -suffix.length);
       range.select();
     } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
-      this.textarea.value = this.textarea.value.substring(0, start) + subst +
-      this.textarea.value.substring(end);
-      if (sel || (!prefix && start === end)) {
-        this.textarea.setSelectionRange(start + subst.length, start + subst.length);
+      if (this.replaceRange(start, end, subst)) {
+        // execCommand leaves the caret after the inserted text and keeps it
+        // visible by itself, so scrollTop is left alone here.
+        if (!(sel || (!prefix && start === end))) {
+          this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+        }
       } else {
-        this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+        this.textarea.value = this.textarea.value.substring(0, start) + subst +
+        this.textarea.value.substring(end);
+        if (sel || (!prefix && start === end)) {
+          this.textarea.setSelectionRange(start + subst.length, start + subst.length);
+        } else {
+          this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+        }
+        this.textarea.scrollTop = scrollPos;
       }
-      this.textarea.scrollTop = scrollPos;
     }
   },
 
@@ -401,14 +426,20 @@
       range.select();
 //      this.textarea.caretPos -= suffix.length;
     } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
-      this.textarea.value = this.textarea.value.substring(0, start) + subst +
-      this.textarea.value.substring(end);
-      if (sel) {
-        this.textarea.setSelectionRange(start + subst.length, start + subst.length);
+      if (this.replaceRange(start, end, subst)) {
+        if (!sel) {
+          this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+        }
       } else {
-        this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+        this.textarea.value = this.textarea.value.substring(0, start) + subst +
+        this.textarea.value.substring(end);
+        if (sel) {
+          this.textarea.setSelectionRange(start + subst.length, start + subst.length);
+        } else {
+          this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+        }
+        this.textarea.scrollTop = scrollPos;
       }
-      this.textarea.scrollTop = scrollPos;
     }
   },
   showPreview: function(event) {
