diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index 2d7a6ae8..08d10726 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -10,6 +10,7 @@ license = "MPL-2.0"
 
 [features]
 embed_runtime = ["rust-embed"]
+debug = []
 
 [dependencies]
 helix-syntax = { path = "../helix-syntax" }
@@ -17,7 +18,7 @@ helix-syntax = { path = "../helix-syntax" }
 ropey = "1.2"
 smallvec = "1.4"
 tendril = "0.4.2"
-unicode-segmentation = "1.6"
+unicode-segmentation = { git = "https://github.com/unicode-rs/unicode-segmentation.git" }
 unicode-width = "0.1"
 unicode-general-category = "0.4.0"
 # slab = "0.4.2"
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index cf58b9af..0ab7b645 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,3 +1,4 @@
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Eq, PartialEq)]
 pub enum Severity {
     Error,
@@ -6,10 +7,13 @@ pub enum Severity {
     Hint,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Range {
     pub start: usize,
     pub end: usize,
 }
+
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Diagnostic {
     pub range: Range,
     pub line: usize,
diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs
index 5ec95615..c68b0ee5 100644
--- a/helix-core/src/graphemes.rs
+++ b/helix-core/src/graphemes.rs
@@ -147,6 +147,7 @@ pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
 }
 
 /// An iterator over the graphemes of a `RopeSlice`.
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Clone)]
 pub struct RopeGraphemes<'a> {
     text: RopeSlice<'a>,
diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs
index 4348ee32..6eb72cea 100644
--- a/helix-core/src/history.rs
+++ b/helix-core/src/history.rs
@@ -2,6 +2,7 @@ use crate::{ChangeSet, Rope, State, Transaction};
 use smallvec::{smallvec, SmallVec};
 
 /// Undo-tree style history store.
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct History {
     revisions: Vec<Revision>,
     cursor: usize,
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index 9d62b16c..8a30e4c6 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -1,6 +1,7 @@
 use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
 use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum Direction {
     Forward,
diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs
index ab475cab..c8a0cf8b 100644
--- a/helix-core/src/state.rs
+++ b/helix-core/src/state.rs
@@ -1,6 +1,7 @@
 use crate::{Rope, Selection};
 
 /// A state represents the current editor state of a single buffer.
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Clone)]
 pub struct State {
     pub doc: Rope,
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index b0f82ca1..ca20ff1a 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -1,6 +1,8 @@
 use crate::{regex::Regex, Change, Rope, RopeSlice, Transaction};
 pub use helix_syntax::{get_language, get_language_name, Lang};
 
+#[cfg(feature = "debug")]
+use std::fmt;
 use std::{
     borrow::Cow,
     cell::RefCell,
@@ -12,12 +14,14 @@ use std::{
 use once_cell::sync::{Lazy, OnceCell};
 use serde::{Deserialize, Serialize};
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Serialize, Deserialize)]
 pub struct Configuration {
     pub language: Vec<LanguageConfiguration>,
 }
 
 // largely based on tree-sitter/cli/src/loader.rs
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub struct LanguageConfiguration {
@@ -46,6 +50,7 @@ pub struct LanguageConfiguration {
     pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub struct LanguageServerConfiguration {
@@ -55,6 +60,7 @@ pub struct LanguageServerConfiguration {
     pub args: Vec<String>,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub struct IndentationConfiguration {
@@ -62,6 +68,7 @@ pub struct IndentationConfiguration {
     pub unit: String,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub struct IndentQuery {
@@ -189,6 +196,7 @@ impl LanguageConfiguration {
 
 pub static LOADER: OnceCell<Loader> = OnceCell::new();
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Loader {
     // highlight_names ?
     language_configs: Vec<Arc<LanguageConfiguration>>,
@@ -256,6 +264,13 @@ pub struct TsParser {
     cursors: Vec<QueryCursor>,
 }
 
+#[cfg(feature = "debug")]
+impl fmt::Debug for TsParser {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("TsParser").finish_non_exhaustive()
+    }
+}
+
 // could also just use a pool, or a single instance?
 thread_local! {
     pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
@@ -264,6 +279,7 @@ thread_local! {
     })
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Syntax {
     config: Arc<HighlightConfiguration>,
 
@@ -444,6 +460,7 @@ impl Syntax {
     // buffer_range_for_scope_at_pos
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct LanguageLayer {
     // mode
     // grammar
@@ -752,6 +769,7 @@ pub enum HighlightEvent {
 /// Contains the data neeeded to higlight code written in a particular language.
 ///
 /// This struct is immutable and can be shared between threads.
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct HighlightConfiguration {
     pub language: Grammar,
     pub query: Query,
@@ -782,6 +800,7 @@ struct LocalScope<'a> {
     local_defs: Vec<LocalDef<'a>>,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 struct HighlightIter<'a, 'tree: 'a, F>
 where
     F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
@@ -807,6 +826,13 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
     depth: usize,
 }
 
+#[cfg(feature = "debug")]
+impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("HighlightIterLayer").finish_non_exhaustive()
+    }
+}
+
 impl HighlightConfiguration {
     /// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting
     /// queries.
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index 3a719628..24abe0ab 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -15,6 +15,7 @@ pub enum Operation {
     Insert(Tendril),
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum Assoc {
     Before,
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index 1384ce67..7fd05492 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -7,6 +7,9 @@ license = "MPL-2.0"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+[features]
+debug = ["helix-core/debug"]
+
 [dependencies]
 helix-core = { path = "../helix-core" }
 
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 9c85beef..739ff9c2 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -16,6 +16,7 @@ use tokio::{
     sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
 };
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Client {
     _process: Child,
     server_tx: UnboundedSender<Payload>,
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 5c482774..197ca27d 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -172,6 +172,7 @@ impl Notification {
     }
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Registry {
     inner: HashMap<LanguageId, Arc<Client>>,
 
diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs
index c69c1ccf..876de929 100644
--- a/helix-lsp/src/transport.rs
+++ b/helix-lsp/src/transport.rs
@@ -31,6 +31,7 @@ enum ServerMessage {
     Call(jsonrpc::Call),
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Transport {
     client_tx: UnboundedSender<jsonrpc::Call>,
     client_rx: UnboundedReceiver<Payload>,
diff --git a/helix-syntax/languages/tree-sitter-haskell b/helix-syntax/languages/tree-sitter-haskell
index 237f4eb4..004f2709 160000
--- a/helix-syntax/languages/tree-sitter-haskell
+++ b/helix-syntax/languages/tree-sitter-haskell
@@ -1 +1 @@
-Subproject commit 237f4eb4417c28f643a29d795ed227246afb66f9
+Subproject commit 004f2709c460d95fbfd1061f8efc98f36e33c03c
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 75edd723..d42db188 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -10,6 +10,7 @@ license = "MPL-2.0"
 
 [features]
 embed_runtime = ["helix-core/embed_runtime"]
+debug = ["helix-core/debug", "helix-view/debug"]
 
 [[bin]]
 name = "hx"
diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml
index d819fea1..4ead2107 100644
--- a/helix-tui/Cargo.toml
+++ b/helix-tui/Cargo.toml
@@ -10,6 +10,7 @@ license = "MPL-2.0"
 
 [features]
 default = ["crossterm"]
+debug = []
 
 [dependencies]
 bitflags = "1.0"
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index 9bac60e4..2efa53f0 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -10,6 +10,7 @@ license = "MPL-2.0"
 [features]
 term = ["tui", "crossterm"]
 default = ["term"]
+debug = ["helix-core/debug", "helix-lsp/debug"]
 
 [dependencies]
 anyhow = "1"
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 87eb34ba..dd40421f 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -13,6 +13,7 @@ use crate::{DocumentId, ViewId};
 
 use std::collections::HashMap;
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Copy, Clone, PartialEq, Eq, Hash)]
 pub enum Mode {
     Normal,
@@ -52,6 +53,31 @@ pub struct Document {
     language_server: Option<Arc<helix_lsp::Client>>,
 }
 
+#[cfg(feature = "debug")]
+use std::fmt;
+#[cfg(feature = "debug")]
+impl fmt::Debug for Document {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Document")
+            .field("id", &self.id)
+            .field("text", &self.text)
+            .field("selections", &self.selections)
+            .field("path", &self.path)
+            .field("mode", &self.mode)
+            .field("restore_cursor", &self.restore_cursor)
+            .field("syntax", &self.syntax)
+            .field("language", &self.language)
+            .field("changes", &self.changes)
+            .field("old_state", &self.old_state)
+            // .field("history", &self.history)
+            .field("last_saved_revision", &self.last_saved_revision)
+            .field("version", &self.version)
+            .field("diagnostics", &self.diagnostics)
+            // .field("language_server", &self.language_server)
+            .finish_non_exhaustive()
+    }
+}
+
 /// Like std::mem::replace() except it allows the replacement value to be mapped from the
 /// original value.
 fn take_with<T, F>(mut_ref: &mut T, closure: F)
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 3e91921b..deb7795c 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -9,6 +9,7 @@ use anyhow::Error;
 
 pub use helix_core::diagnostic::Severity;
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Editor {
     pub tree: Tree,
     pub documents: SlotMap<DocumentId, Document>,
@@ -20,6 +21,7 @@ pub struct Editor {
     pub status_msg: Option<(String, Severity)>,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(Copy, Clone)]
 pub enum Action {
     Replace,
diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs
index 5b56156f..b5eb5d87 100644
--- a/helix-view/src/tree.rs
+++ b/helix-view/src/tree.rs
@@ -4,6 +4,7 @@ use tui::layout::Rect;
 
 // the dimensions are recomputed on windo resize/tree change.
 //
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Tree {
     root: ViewId,
     // (container, index inside the container)
@@ -17,11 +18,13 @@ pub struct Tree {
     stack: Vec<(ViewId, Rect)>,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Node {
     parent: ViewId,
     content: Content,
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub enum Content {
     View(Box<View>),
     Container(Box<Container>),
@@ -45,6 +48,7 @@ impl Node {
 
 // TODO: screen coord to container + container coordinate helpers
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[derive(PartialEq, Eq)]
 pub enum Layout {
     Horizontal,
@@ -52,6 +56,7 @@ pub enum Layout {
     // could explore stacked/tabbed
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Container {
     layout: Layout,
     children: Vec<ViewId>,
@@ -432,6 +437,7 @@ impl Tree {
     }
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Traverse<'a> {
     tree: &'a Tree,
     stack: Vec<ViewId>, // TODO: reuse the one we use on update
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 7bd31305..e222c11c 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -12,6 +12,7 @@ pub const PADDING: usize = 5;
 
 type Jump = (DocumentId, Selection);
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct JumpList {
     jumps: Vec<Jump>,
     current: usize,
@@ -58,6 +59,7 @@ impl JumpList {
     }
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct View {
     pub id: ViewId,
     pub doc: DocumentId,