diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs
index 9d51c8c5..ed0df9cf 100644
--- a/helix-core/src/state.rs
+++ b/helix-core/src/state.rs
@@ -1,6 +1,6 @@
 use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
 use crate::syntax::LOADER;
-use crate::{Position, Range, Rope, RopeSlice, Selection, Syntax};
+use crate::{ChangeSet, Position, Range, Rope, RopeSlice, Selection, Syntax};
 use anyhow::Error;
 
 use std::path::PathBuf;
@@ -25,6 +25,8 @@ pub struct State {
 
     //
     pub syntax: Option<Syntax>,
+    pub changes: Option<ChangeSet>,
+    pub old_state: Option<(Rope, Selection)>,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq)]
@@ -50,6 +52,8 @@ impl State {
             mode: Mode::Normal,
             restore_cursor: false,
             syntax: None,
+            changes: None,
+            old_state: None,
         }
     }
 
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index d6b151ba..7871052a 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -369,6 +369,13 @@ impl Transaction {
                 return false;
             }
 
+            // Compose this transaction with the previous one
+            let old_changes = state.changes.take();
+            state.changes = Some(old_changes.map_or_else(
+                || self.changes.clone(),
+                |changes| changes.compose(self.changes.clone()).unwrap(),
+            ));
+
             if let Some(syntax) = &mut state.syntax {
                 // TODO: no unwrap
                 syntax.update(&old_doc, &state.doc, &self.changes).unwrap();
diff --git a/helix-term/src/editor.rs b/helix-term/src/editor.rs
index 0028521e..b7a385a1 100644
--- a/helix-term/src/editor.rs
+++ b/helix-term/src/editor.rs
@@ -426,31 +426,3 @@ impl Editor {
 // TODO: scope matching: biggest union match? [string] & [html, string], [string, html] & [ string, html]
 // can do this by sorting our theme matches based on array len (longest first) then stopping at the
 // first rule that matches (rule.all(|scope| scopes.contains(scope)))
-//
-// let visual_x = 0;
-// let line = ?;
-// for span in spans {
-// start(scope) => scopes.push(scope)
-//  span =>
-//      let text = rope.slice(span.start..span.end);
-//      let style = calculate_style(scopes);
-//      for each grapheme in text.graphemes() {
-//          // if newline += lines, continue
-//
-//          if state.selection.ranges().any(|range| range.contains(char_index)) {
-//              if exactly on cursor {
-//              }
-//              if on primary cursor? {
-//              }
-//              modify style temporarily
-//          }
-//
-//          // if in bounds
-//
-//          // if tab, draw tab width
-//          // draw(visual_x, line, grapheme, style)
-//          // increment visual_x by grapheme_width(grapheme)
-//          // increment char_index by grapheme.len_chars()
-//      }
-//  end => scopes.pop()
-// }
diff --git a/helix-view/src/commands.rs b/helix-view/src/commands.rs
index c92b33f5..9a6d2e5d 100644
--- a/helix-view/src/commands.rs
+++ b/helix-view/src/commands.rs
@@ -3,7 +3,7 @@ use helix_core::{
     regex::Regex,
     selection,
     state::{Direction, Granularity, Mode, State},
-    Range, Selection, Tendril, Transaction,
+    ChangeSet, Range, Selection, Tendril, Transaction,
 };
 use once_cell::sync::Lazy;
 
@@ -253,14 +253,16 @@ pub fn collapse_selection(view: &mut View, _count: usize) {
         .transform(|range| Range::new(range.head, range.head))
 }
 
-// insert mode:
-// first we calculate the correct cursors/selections
-// then we just append at each cursor
-// lastly, if it was append mode we shift cursor by 1?
+fn enter_insert_mode(view: &mut View) {
+    view.state.mode = Mode::Insert;
 
+    // HAXX
+    view.state.changes = Some(ChangeSet::new(view.state.doc()));
+    view.state.old_state = Some((view.state.doc().clone(), view.state.selection.clone()));
+}
 // inserts at the start of each selection
 pub fn insert_mode(view: &mut View, _count: usize) {
-    view.state.mode = Mode::Insert;
+    enter_insert_mode(view);
 
     view.state.selection = view
         .state
@@ -270,7 +272,7 @@ pub fn insert_mode(view: &mut View, _count: usize) {
 
 // inserts at the end of each selection
 pub fn append_mode(view: &mut View, _count: usize) {
-    view.state.mode = Mode::Insert;
+    enter_insert_mode(view);
     view.state.restore_cursor = true;
 
     // TODO: as transaction
@@ -303,21 +305,21 @@ fn selection_lines(state: &State) -> Vec<usize> {
 
 // I inserts at the start of each line with a selection
 pub fn prepend_to_line(view: &mut View, count: usize) {
-    view.state.mode = Mode::Insert;
+    enter_insert_mode(view);
 
     move_line_start(view, count);
 }
 
 // A inserts at the end of each line with a selection
 pub fn append_to_line(view: &mut View, count: usize) {
-    view.state.mode = Mode::Insert;
+    enter_insert_mode(view);
 
     move_line_end(view, count);
 }
 
 // o inserts a new line after each line with a selection
 pub fn open_below(view: &mut View, _count: usize) {
-    view.state.mode = Mode::Insert;
+    enter_insert_mode(view);
 
     let lines = selection_lines(&view.state);
 
@@ -356,6 +358,18 @@ pub fn open_below(view: &mut View, _count: usize) {
 pub fn normal_mode(view: &mut View, _count: usize) {
     view.state.mode = Mode::Normal;
 
+    if let Some(changes) = view.state.changes.take() {
+        // Instead of doing this messy merge we could always commit, and based on transaction
+        // annotations either add a new layer or compose into the previous one.
+        let transaction = Transaction::from(changes).with_selection(view.state.selection().clone());
+        let (doc, selection) = view.state.old_state.take().unwrap();
+        let mut old_state = State::new(doc);
+        old_state.selection = selection;
+
+        // TODO: take transaction by value?
+        view.history.commit_revision(&transaction, &old_state);
+    }
+
     // if leaving append mode, move cursor back by 1
     if view.state.restore_cursor {
         let text = &view.state.doc.slice(..);