diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs
index 65194b38..d597fcbb 100644
--- a/helix-core/src/state.rs
+++ b/helix-core/src/state.rs
@@ -198,9 +198,6 @@ impl State {
         granularity: Granularity,
         count: usize,
     ) -> Selection {
-        // move all selections according to normal cursor move semantics by collapsing it
-        // into cursors and moving them vertically
-
         self.selection
             .transform(|range| self.move_range(range, dir, granularity, count, false))
     }
@@ -255,7 +252,6 @@ fn move_vertically(
     let pos = pos_at_coords(text, Position::new(new_line, new_col));
 
     let mut range = Range::new(if extend { range.anchor } else { pos }, pos);
-    use std::convert::TryInto;
     range.horiz = Some(horiz);
     range
 }
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index c82724a5..dfa819db 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -137,7 +137,7 @@ impl Application {
                             .tree
                             .views()
                             .map(|(view, _key)| view)
-                            .find(|view| view.doc.path == path);
+                            .find(|view| view.doc.path() == path.as_ref());
 
                         if let Some(view) = view {
                             let doc = view.doc.text().slice(..);
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 104b86b7..bf6f0c88 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -442,7 +442,7 @@ pub fn delete_selection(cx: &mut Context) {
     let doc = cx.doc();
     _delete_selection(doc);
 
-    append_changes_to_history(doc);
+    doc.append_changes_to_history();
 }
 
 pub fn change_selection(cx: &mut Context) {
@@ -492,7 +492,6 @@ pub fn append_mode(cx: &mut Context) {
     enter_insert_mode(doc);
     doc.restore_cursor = true;
 
-    // TODO: as transaction
     let text = doc.text().slice(..);
     let selection = doc.selection().transform(|range| {
         // TODO: to() + next char
@@ -510,7 +509,6 @@ pub fn command_mode(cx: &mut Context) {
     let prompt = Prompt::new(
         ":".to_owned(),
         |_input: &str| {
-            // TODO: i need this duplicate list right now to avoid borrow checker issues
             let command_list = vec![
                 "q".to_string(),
                 "o".to_string(),
@@ -650,40 +648,12 @@ pub fn open_below(cx: &mut Context) {
 
 // O inserts a new line before each line with a selection
 
-fn append_changes_to_history(doc: &mut Document) {
-    if doc.changes.is_empty() {
-        return;
-    }
-
-    // TODO: change -> change -> undo -> change -> change fails, probably old_state needs reset
-
-    let new_changeset = ChangeSet::new(doc.text());
-    let changes = std::mem::replace(&mut doc.changes, new_changeset);
-    // 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(doc.selection().clone());
-
-    // increment document version
-    // TODO: needs to happen on undo/redo too
-    doc.version += 1;
-
-    // TODO: trigger lsp/documentDidChange with changes
-
-    // HAXX: we need to reconstruct the state as it was before the changes..
-    let old_state = doc.old_state.take().expect("no old_state available");
-
-    // TODO: take transaction by value?
-    doc.history.commit_revision(&transaction, &old_state);
-
-    // TODO: notify LSP of changes
-}
-
 pub fn normal_mode(cx: &mut Context) {
     let doc = cx.doc();
 
     doc.mode = Mode::Normal;
 
-    append_changes_to_history(doc);
+    doc.append_changes_to_history();
 
     // if leaving append mode, move cursor back by 1
     if doc.restore_cursor {
@@ -848,7 +818,7 @@ pub fn paste(cx: &mut Context) {
         };
 
         doc.apply(&transaction);
-        append_changes_to_history(doc);
+        doc.append_changes_to_history();
     }
 }
 
@@ -884,7 +854,7 @@ pub fn indent(cx: &mut Context) {
         }),
     );
     doc.apply(&transaction);
-    append_changes_to_history(doc);
+    doc.append_changes_to_history();
 }
 
 pub fn unindent(cx: &mut Context) {
@@ -917,7 +887,7 @@ pub fn unindent(cx: &mut Context) {
     let transaction = Transaction::change(&doc.state, changes.into_iter());
 
     doc.apply(&transaction);
-    append_changes_to_history(doc);
+    doc.append_changes_to_history();
 }
 
 //
@@ -1010,7 +980,7 @@ pub fn completion(cx: &mut Context) {
                         let transaction =
                             util::generate_transaction_from_edits(&doc.state, vec![edit]);
                         doc.apply(&transaction);
-                        // TODO: append_changes_to_history(doc); if not in insert mode?
+                        // TODO: doc.append_changes_to_history(); if not in insert mode?
                     }
                     _ => (),
                 };
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 72a2710d..dd7b14fc 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -17,7 +17,7 @@ pub enum Mode {
 pub struct Document {
     pub state: State, // rope + selection
     /// File path on disk.
-    pub path: Option<PathBuf>,
+    path: Option<PathBuf>,
 
     /// Current editing mode.
     pub mode: Mode,
@@ -26,13 +26,16 @@ pub struct Document {
     /// Tree-sitter AST tree
     pub syntax: Option<Syntax>,
     /// Corresponding language scope name. Usually `source.<lang>`.
-    pub language: Option<String>,
+    language: Option<String>,
 
     /// Pending changes since last history commit.
-    pub changes: ChangeSet,
-    pub old_state: Option<State>,
-    pub history: History,
-    pub version: i32, // should be usize?
+    changes: ChangeSet,
+    /// State at last commit. Used for calculating reverts.
+    old_state: Option<State>,
+    /// Undo tree.
+    history: History,
+    /// Current document version, incremented at each change.
+    version: i32, // should be usize?
 
     pub diagnostics: Vec<Diagnostic>,
     pub language_server: Option<Arc<helix_lsp::Client>>,
@@ -90,23 +93,8 @@ impl Document {
 
         let mut doc = Self::new(State::new(doc));
 
-        if let Some(language_config) = LOADER.language_config_for_file_name(path.as_path()) {
-            let highlight_config = language_config.highlight_config(scopes).unwrap().unwrap();
-            // TODO: config.configure(scopes) is now delayed, is that ok?
-
-            let syntax = Syntax::new(&doc.state.doc, highlight_config.clone());
-
-            doc.syntax = Some(syntax);
-            // TODO: maybe just keep an Arc<> pointer to the language_config?
-            doc.language = Some(language_config.scope().to_string());
-
-            // TODO: this ties lsp support to tree-sitter enabled languages for now. Language
-            // config should use Option<HighlightConfig> to let us have non-tree-sitter configs.
-
-            // TODO: circular dep: view <-> lsp
-            // helix_lsp::REGISTRY;
-            // view should probably depend on lsp
-        };
+        let language_config = LOADER.language_config_for_file_name(path.as_path());
+        doc.set_language(language_config, scopes);
 
         // canonicalize path to absolute value
         doc.path = Some(std::fs::canonicalize(path)?);
@@ -140,17 +128,35 @@ impl Document {
         } // and_then notify save
     }
 
-    pub fn set_language(&mut self, scope: &str, scopes: &[String]) {
-        if let Some(language_config) = LOADER.language_config_for_scope(scope) {
+    pub fn set_language(
+        &mut self,
+        language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
+        scopes: &[String],
+    ) {
+        if let Some(language_config) = language_config {
+            // TODO: maybe just keep an Arc<> pointer to the language_config?
+            self.language = Some(language_config.scope().to_string());
+
+            // TODO: this ties lsp support to tree-sitter enabled languages for now. Language
+            // config should use Option<HighlightConfig> to let us have non-tree-sitter configs.
+
             let highlight_config = language_config.highlight_config(scopes).unwrap().unwrap();
             // TODO: config.configure(scopes) is now delayed, is that ok?
 
             let syntax = Syntax::new(&self.state.doc, highlight_config.clone());
 
             self.syntax = Some(syntax);
+        } else {
+            self.syntax = None;
+            self.language = None;
         };
     }
 
+    pub fn set_language2(&mut self, scope: &str, scopes: &[String]) {
+        let language_config = LOADER.language_config_for_scope(scope);
+        self.set_language(language_config, scopes);
+    }
+
     pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
         self.language_server = language_server;
     }
@@ -238,11 +244,44 @@ impl Document {
         false
     }
 
+    pub fn append_changes_to_history(&mut self) {
+        if self.changes.is_empty() {
+            return;
+        }
+
+        // TODO: change -> change -> undo -> change -> change fails, probably old_state needs reset
+
+        let new_changeset = ChangeSet::new(self.text());
+        let changes = std::mem::replace(&mut self.changes, new_changeset);
+        // 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(self.selection().clone());
+
+        // increment document version
+        self.version += 1;
+
+        // HAXX: we need to reconstruct the state as it was before the changes..
+        let old_state = self.old_state.take().expect("no old_state available");
+
+        self.history.commit_revision(&transaction, &old_state);
+    }
+
     #[inline]
     pub fn mode(&self) -> Mode {
         self.mode
     }
 
+    #[inline]
+    /// Corresponding language scope name. Usually `source.<lang>`.
+    pub fn language(&self) -> Option<&str> {
+        self.language.as_ref().map(String::as_str)
+    }
+
+    #[inline]
+    pub fn version(&self) -> i32 {
+        self.version
+    }
+
     #[inline]
     pub fn path(&self) -> Option<&PathBuf> {
         self.path.as_ref()
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 24cd8ef9..b70a6abd 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -37,9 +37,8 @@ impl Editor {
 
         // try to find a language server based on the language name
         let language_server = doc
-            .language
-            .as_ref()
-            .and_then(|language| self.language_servers.get(&language, &executor));
+            .language()
+            .and_then(|language| self.language_servers.get(language, &executor));
 
         if let Some(language_server) = language_server {
             // TODO: do this everywhere
@@ -47,7 +46,7 @@ impl Editor {
 
             smol::block_on(language_server.text_document_did_open(
                 doc.url().unwrap(),
-                doc.version,
+                doc.version(),
                 doc.text(),
             ))
             .unwrap();