diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 00000000..35049cbc
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,2 @@
+[alias]
+xtask = "run --package xtask --"
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 9b7c22e7..958407bb 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -17,7 +17,7 @@ Please search on the issue tracker before creating one. -->
 ### Environment
 
 - Platform: <!--  macOS / Windows / Linux -->
-- Helix version: <!--  'hx -v' if using a release, 'git describe' if building from master -->
+- Helix version: <!--  'hx -V' if using a release, 'git describe' if building from master -->
 
 <details><summary>~/.cache/helix/helix.log</summary>
 
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d4822f70..7f18da6a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,19 +25,19 @@ jobs:
           override: true
 
       - name: Cache cargo registry
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ~/.cargo/registry
           key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
 
       - name: Cache cargo index
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ~/.cargo/git
           key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
 
       - name: Cache cargo target dir
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: target
           key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -64,19 +64,19 @@ jobs:
           override: true
 
       - name: Cache cargo registry
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ~/.cargo/registry
           key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
 
       - name: Cache cargo index
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ~/.cargo/git
           key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
 
       - name: Cache cargo target dir
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: target
           key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -108,6 +108,52 @@ jobs:
           override: true
           components: rustfmt, clippy
 
+      - name: Cache cargo registry
+        uses: actions/cache@v2.1.7
+        with:
+          path: ~/.cargo/registry
+          key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+
+      - name: Cache cargo index
+        uses: actions/cache@v2.1.7
+        with:
+          path: ~/.cargo/git
+          key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+
+      - name: Cache cargo target dir
+        uses: actions/cache@v2.1.7
+        with:
+          path: target
+          key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
+
+      - name: Run cargo fmt
+        uses: actions-rs/cargo@v1
+        with:
+          command: fmt
+          args: --all -- --check
+
+      - name: Run cargo clippy
+        uses: actions-rs/cargo@v1
+        with:
+          command: clippy
+          args: -- -D warnings
+
+  docs:
+    name: Docs
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout sources
+        uses: actions/checkout@v2
+        with:
+          submodules: true
+
+      - name: Install stable toolchain
+        uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          override: true
+
       - name: Cache cargo registry
         uses: actions/cache@v2.1.6
         with:
@@ -126,14 +172,16 @@ jobs:
           path: target
           key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
 
-      - name: Run cargo fmt
+      - name: Generate docs
         uses: actions-rs/cargo@v1
         with:
-          command: fmt
-          args: --all -- --check
+          command: xtask
+          args: docgen
+
+      - name: Check uncommitted documentation changes
+        run: |
+          git diff
+          git diff-files --quiet \
+            || (echo "Run 'cargo xtask docgen', commit the changes and push again" \
+            && exit 1)
 
-      - name: Run cargo clippy
-        uses: actions-rs/cargo@v1
-        with:
-          command: clippy
-          args: -- -D warnings
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b16fa428..1ce3e092 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -102,7 +102,7 @@ jobs:
           fi
           cp -r runtime dist
 
-      - uses: actions/upload-artifact@v2.2.4
+      - uses: actions/upload-artifact@v2.3.0
         with:
           name: bins-${{ matrix.build }}
           path: dist
diff --git a/.gitmodules b/.gitmodules
index bf596bdc..9c10846d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -142,3 +142,15 @@
 	path = helix-syntax/languages/tree-sitter-perl
 	url = https://github.com/ganezdragon/tree-sitter-perl
 	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-wgsl"]
+	path = helix-syntax/languages/tree-sitter-wgsl
+	url = https://github.com/szebniok/tree-sitter-wgsl
+	shallow = true
+[submodule "helix-syntax/tree-sitter-llvm"]
+	path = helix-syntax/languages/tree-sitter-llvm
+	url = https://github.com/benwilliamgraham/tree-sitter-llvm
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-markdown"]
+	path = helix-syntax/languages/tree-sitter-markdown
+	url = https://github.com/MDeiml/tree-sitter-markdown
+	shallow = true
diff --git a/Cargo.lock b/Cargo.lock
index 8d2b4562..cd6c0496 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.48"
+version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
+checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
 
 [[package]]
 name = "arc-swap"
@@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.29"
+version = "0.8.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
 dependencies = [
  "cfg-if",
 ]
@@ -258,15 +258,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -275,17 +275,16 @@ dependencies = [
 
 [[package]]
 name = "futures-task"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
 
 [[package]]
 name = "futures-util"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
 dependencies = [
- "autocfg",
  "futures-core",
  "futures-task",
  "pin-project-lite",
@@ -370,6 +369,7 @@ name = "helix-core"
 version = "0.5.0"
 dependencies = [
  "arc-swap",
+ "chrono",
  "etcetera",
  "helix-syntax",
  "log",
@@ -535,9 +535,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "0.4.8"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
 
 [[package]]
 name = "jsonrpc-core"
@@ -877,18 +877,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "serde"
-version = "1.0.130"
+version = "1.0.131"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.130"
+version = "1.0.131"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -897,9 +897,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.71"
+version = "1.0.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
+checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
 dependencies = [
  "itoa",
  "ryu",
@@ -919,9 +919,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook"
-version = "0.3.10"
+version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
+checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922"
 dependencies = [
  "libc",
  "signal-hook-registry",
@@ -1259,3 +1259,12 @@ name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "xtask"
+version = "0.5.0"
+dependencies = [
+ "helix-core",
+ "helix-term",
+ "toml",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 580cccd6..8c3ee671 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
   "helix-tui",
   "helix-syntax",
   "helix-lsp",
+  "xtask",
 ]
 
 # Build helix-syntax in release mode to make the code path faster in development.
diff --git a/README.md b/README.md
index 3f4087b9..71010cc8 100644
--- a/README.md
+++ b/README.md
@@ -44,8 +44,8 @@ cargo install --path helix-term
 This will install the `hx` binary to `$HOME/.cargo/bin`.
 
 Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
-config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
-via the `HELIX_RUNTIME` environment variable.
+config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
+This location can be overriden via the `HELIX_RUNTIME` environment variable.
 
 Packages already solve this for you by wrapping the `hx` binary with a wrapper
 that sets the variable to the install dir.
@@ -65,21 +65,7 @@ brew install helix
  
 # Contributing
 
-Contributors are very welcome! **No contribution is too small and all contributions are valued.**
-
-Some suggestions to get started:
-
-- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker.
-- Help with packaging on various distributions needed!
-- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
-  * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
-  * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
-- If your preferred language is missing, integrating a tree-sitter grammar for
-    it and defining syntax highlight queries for it is straight forward and
-    doesn't require much knowledge of the internals.
-
-We provide an [architecture.md](./docs/architecture.md) that should give you
-a good overview of the internals.
+Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
 
 # Getting help
 
diff --git a/base16_theme.toml b/base16_theme.toml
new file mode 100644
index 00000000..5ec74bcc
--- /dev/null
+++ b/base16_theme.toml
@@ -0,0 +1,38 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.menu" = "black"
+"ui.menu.selected" = { modifiers = ["reversed"] }
+"ui.linenr" = { fg = "gray", bg = "black" }
+"ui.popup" = { modifiers = ["reversed"] }
+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
+"ui.selection" = { fg = "black", bg = "blue" }
+"ui.selection.primary" = { fg = "white", bg = "blue" }
+"comment" = { fg = "gray" }
+"ui.statusline" = { fg = "black", bg = "white" }
+"ui.statusline.inactive" = { fg = "gray", bg = "white" }
+"ui.help" = { modifiers = ["reversed"] }
+"ui.cursor" = { modifiers = ["reversed"] }
+"variable" = "red"
+"constant.numeric" = "yellow"
+"constant" = "yellow"
+"attributes" = "yellow"
+"type" = "yellow"
+"ui.cursor.match" = { fg = "yellow", modifiers = ["underlined"] }
+"string"  = "green"
+"variable.other.member" = "green"
+"constant.character.escape" = "cyan"
+"function" = "blue"
+"constructor" = "blue"
+"special" = "blue"
+"keyword" = "magenta"
+"label" = "magenta"
+"namespace" = "magenta"
+"ui.help" = { fg = "white", bg = "black" }
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "black" }
+"info" = "blue"
+"hint" = "gray"
+"debug" = "gray"
+"warning" = "yellow"
+"error" = "red"
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 8cadb663..a8f165c0 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -2,10 +2,12 @@
 
 - [Installation](./install.md)
 - [Usage](./usage.md)
+  - [Keymap](./keymap.md)
+  - [Commands](./commands.md)
+  - [Language Support](./lang-support.md)
 - [Migrating from Vim](./from-vim.md)
 - [Configuration](./configuration.md)
   - [Themes](./themes.md)
-  - [Keymap](./keymap.md)
   - [Key Remapping](./remapping.md)
   - [Hooks](./hooks.md)
   - [Languages](./languages.md)
diff --git a/book/src/commands.md b/book/src/commands.md
new file mode 100644
index 00000000..4c4a5c05
--- /dev/null
+++ b/book/src/commands.md
@@ -0,0 +1,5 @@
+# Commands
+
+Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
+
+{{#include ./generated/typable-cmd.md}}
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 2998bcdc..476c2b39 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -41,6 +41,7 @@ hidden = false
 | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
 | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
 | `auto-info` | Whether to display infoboxes | `true` |
+| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
 
 ### `[editor.cursor-shape]` Section
 
diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md
new file mode 100644
index 00000000..80989e63
--- /dev/null
+++ b/book/src/generated/lang-support.md
@@ -0,0 +1,42 @@
+| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
+| --- | --- | --- | --- | --- |
+| bash | ✓ |  |  | `bash-language-server` |
+| c | ✓ |  |  | `clangd` |
+| c-sharp | ✓ |  |  |  |
+| cmake | ✓ |  |  | `cmake-language-server` |
+| cpp | ✓ |  |  | `clangd` |
+| css | ✓ |  |  |  |
+| elixir | ✓ |  |  | `elixir-ls` |
+| glsl | ✓ |  | ✓ |  |
+| go | ✓ | ✓ | ✓ | `gopls` |
+| html | ✓ |  |  |  |
+| java | ✓ |  |  |  |
+| javascript | ✓ |  | ✓ |  |
+| json | ✓ |  | ✓ |  |
+| julia | ✓ |  |  | `julia` |
+| latex | ✓ |  |  |  |
+| ledger | ✓ |  |  |  |
+| llvm | ✓ |  |  |  |
+| lua | ✓ |  | ✓ |  |
+| markdown | ✓ |  |  |  |
+| mint |  |  |  | `mint` |
+| nix | ✓ |  | ✓ | `rnix-lsp` |
+| ocaml | ✓ |  | ✓ |  |
+| ocaml-interface | ✓ |  |  |  |
+| perl | ✓ | ✓ | ✓ |  |
+| php | ✓ |  | ✓ |  |
+| prolog |  |  |  | `swipl` |
+| protobuf | ✓ |  | ✓ |  |
+| python | ✓ | ✓ | ✓ | `pylsp` |
+| racket |  |  |  | `racket` |
+| ruby | ✓ |  |  | `solargraph` |
+| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
+| svelte | ✓ |  | ✓ | `svelteserver` |
+| toml | ✓ |  |  |  |
+| tsq | ✓ |  |  |  |
+| tsx | ✓ |  |  | `typescript-language-server` |
+| typescript | ✓ |  | ✓ | `typescript-language-server` |
+| vue | ✓ |  |  |  |
+| wgsl | ✓ |  |  |  |
+| yaml | ✓ |  | ✓ |  |
+| zig | ✓ |  | ✓ | `zls` |
diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md
new file mode 100644
index 00000000..bb21fd6b
--- /dev/null
+++ b/book/src/generated/typable-cmd.md
@@ -0,0 +1,43 @@
+| Name | Description |
+| --- | --- |
+| `:quit`, `:q` | Close the current view. |
+| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
+| `:open`, `:o` | Open a file from disk into the current view. |
+| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
+| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
+| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
+| `:new`, `:n` | Create a new scratch buffer. |
+| `:format`, `:fmt` | Format the file using the LSP formatter. |
+| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
+| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. |
+| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
+| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
+| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
+| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
+| `:write-all`, `:wa` | Write changes from all views to disk. |
+| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. |
+| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). |
+| `:quit-all`, `:qa` | Close all views. |
+| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
+| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
+| `:theme` | Change the editor theme. |
+| `:clipboard-yank` | Yank main selection into system clipboard. |
+| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
+| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
+| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. |
+| `:clipboard-paste-after` | Paste system clipboard after selections. |
+| `:clipboard-paste-before` | Paste system clipboard before selections. |
+| `:clipboard-paste-replace` | Replace selections with content of system clipboard. |
+| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. |
+| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. |
+| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. |
+| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
+| `:change-current-directory`, `:cd` | Change the current working directory. |
+| `:show-directory`, `:pwd` | Show the current working directory. |
+| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
+| `:reload` | Discard changes and reload from the source file. |
+| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
+| `:vsplit`, `:vs` | Open the file in a vertical split. |
+| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
+| `:tutor` | Open the tutorial. |
+| `:goto`, `:g` | Go to line number. |
diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md
index 446eb479..9ad2c285 100644
--- a/book/src/guides/adding_languages.md
+++ b/book/src/guides/adding_languages.md
@@ -2,7 +2,7 @@
 
 ## Submodules
 
-To add a new langauge, you should first add a tree-sitter submodule. To do this,
+To add a new language, you should first add a tree-sitter submodule. To do this,
 you can run the command
 ```sh
 git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
diff --git a/book/src/install.md b/book/src/install.md
index b9febbcc..1a5a9daa 100644
--- a/book/src/install.md
+++ b/book/src/install.md
@@ -25,9 +25,16 @@ shell for working on Helix.
 
 Releases are available in the `community` repository.
 
-Packages are also available on AUR:
-- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
-- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
+A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
+
+### Fedora Linux
+
+You can install the COPR package for Helix via
+
+```
+sudo dnf copr enable varlad/helix
+sudo dnf install helix
+```
 
 ## Build from source
 
diff --git a/book/src/keymap.md b/book/src/keymap.md
index fbe77267..f0a2cb30 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -34,6 +34,7 @@
 | `Ctrl-d`    | Move half page down                                | `half_page_down`            |
 | `Ctrl-i`    | Jump forward on the jumplist                       | `jump_forward`              |
 | `Ctrl-o`    | Jump backward on the jumplist                      | `jump_backward`             |
+| `Ctrl-s`    | Save the current selection to the jumplist         | `save_selection`            |
 | `v`         | Enter [select (extend) mode](#select--extend-mode) | `select_mode`               |
 | `g`         | Enter [goto mode](#goto-mode)                      | N/A                         |
 | `m`         | Enter [match mode](#match-mode)                    | N/A                         |
@@ -45,44 +46,48 @@
 
 ### Changes
 
-| Key         | Description                                     | Command               |
-| -----       | -----------                                     | -------               |
-| `r`         | Replace with a character                        | `replace`             |
-| `R`         | Replace with yanked text                        | `replace_with_yanked` |
-| `~`         | Switch case of the selected text                | `switch_case`         |
-| `` ` ``     | Set the selected text to lower case             | `switch_to_lowercase` |
-| `` Alt-` `` | Set the selected text to upper case             | `switch_to_uppercase` |
-| `i`         | Insert before selection                         | `insert_mode`         |
-| `a`         | Insert after selection (append)                 | `append_mode`         |
-| `I`         | Insert at the start of the line                 | `prepend_to_line`     |
-| `A`         | Insert at the end of the line                   | `append_to_line`      |
-| `o`         | Open new line below selection                   | `open_below`          |
-| `O`         | Open new line above selection                   | `open_above`          |
-| `.`         | Repeat last change                              | N/A                   |
-| `u`         | Undo change                                     | `undo`                |
-| `U`         | Redo change                                     | `redo`                |
-| `Alt-u`     | Move backward in history                        | `earlier`             |
-| `Alt-U`     | Move forward in history                         | `later`               |
-| `y`         | Yank selection                                  | `yank`                |
-| `p`         | Paste after selection                           | `paste_after`         |
-| `P`         | Paste before selection                          | `paste_before`        |
-| `"` `<reg>` | Select a register to yank to or paste from      | `select_register`     |
-| `>`         | Indent selection                                | `indent`              |
-| `<`         | Unindent selection                              | `unindent`            |
-| `=`         | Format selection (**LSP**)                      | `format_selections`   |
-| `d`         | Delete selection                                | `delete_selection`    |
-| `c`         | Change selection (delete and enter insert mode) | `change_selection`    |
-| `Ctrl-a`    | Increment object (number) under cursor          | `increment`           |
-| `Ctrl-x`    | Decrement object (number) under cursor          | `decrement`           |
+| Key         | Description                                                      | Command                   |
+| -----       | -----------                                                      | -------                   |
+| `r`         | Replace with a character                                         | `replace`                 |
+| `R`         | Replace with yanked text                                         | `replace_with_yanked`     |
+| `~`         | Switch case of the selected text                                 | `switch_case`             |
+| `` ` ``     | Set the selected text to lower case                              | `switch_to_lowercase`     |
+| `` Alt-` `` | Set the selected text to upper case                              | `switch_to_uppercase`     |
+| `i`         | Insert before selection                                          | `insert_mode`             |
+| `a`         | Insert after selection (append)                                  | `append_mode`             |
+| `I`         | Insert at the start of the line                                  | `prepend_to_line`         |
+| `A`         | Insert at the end of the line                                    | `append_to_line`          |
+| `o`         | Open new line below selection                                    | `open_below`              |
+| `O`         | Open new line above selection                                    | `open_above`              |
+| `.`         | Repeat last change                                               | N/A                       |
+| `u`         | Undo change                                                      | `undo`                    |
+| `U`         | Redo change                                                      | `redo`                    |
+| `Alt-u`     | Move backward in history                                         | `earlier`                 |
+| `Alt-U`     | Move forward in history                                          | `later`                   |
+| `y`         | Yank selection                                                   | `yank`                    |
+| `p`         | Paste after selection                                            | `paste_after`             |
+| `P`         | Paste before selection                                           | `paste_before`            |
+| `"` `<reg>` | Select a register to yank to or paste from                       | `select_register`         |
+| `>`         | Indent selection                                                 | `indent`                  |
+| `<`         | Unindent selection                                               | `unindent`                |
+| `=`         | Format selection (currently nonfunctional/disabled) (**LSP**)    | `format_selections`       |
+| `d`         | Delete selection                                                 | `delete_selection`        |
+| `Alt-d`     | Delete selection, without yanking                                | `delete_selection_noyank` |
+| `c`         | Change selection (delete and enter insert mode)                  | `change_selection`        |
+| `Alt-c`     | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
+| `Ctrl-a`    | Increment object (number) under cursor                           | `increment`               |
+| `Ctrl-x`    | Decrement object (number) under cursor                           | `decrement`               |
+| `q`         | Start/stop macro recording to the selected register              | `record_macro`            |
+| `Q`         | Play back a recorded macro from the selected register            | `play_macro`              |
 
 #### Shell
 
-| Key                   | Description                                                                      | Command               |
-| ------                | -----------                                                                      | -------               |
-| <code>&#124;</code>   | Pipe each selection through shell command, replacing with output                 | `shell_pipe`          |
-| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output                          | `shell_pipe_to`       |
-| `!`                   | Run shell command, inserting output before each selection                        | `shell_insert_output` |
-| `A-!`                 | Run shell command, appending output after each selection                         | `shell_append_output` |
+| Key                     | Description                                                      | Command               |
+| ------                  | -----------                                                      | -------               |
+| <code>&#124;</code>     | Pipe each selection through shell command, replacing with output | `shell_pipe`          |
+| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output          | `shell_pipe_to`       |
+| `!`                     | Run shell command, inserting output before each selection        | `shell_insert_output` |
+| `Alt-!`                 | Run shell command, appending output after each selection         | `shell_append_output` |
 
 
 ### Selection manipulation
@@ -158,17 +163,19 @@ Jumps to various locations.
 | ----- | -----------                                      | -------                    |
 | `g`   | Go to the start of the file                      | `goto_file_start`          |
 | `e`   | Go to the end of the file                        | `goto_last_line`           |
+| `f`   | Go to files in the selection                     | `goto_file`                |
 | `h`   | Go to the start of the line                      | `goto_line_start`          |
 | `l`   | Go to the end of the line                        | `goto_line_end`            |
 | `s`   | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
 | `t`   | Go to the top of the screen                      | `goto_window_top`          |
-| `m`   | Go to the middle of the screen                   | `goto_window_middle`       |
+| `c`   | Go to the middle of the screen                   | `goto_window_center`       |
 | `b`   | Go to the bottom of the screen                   | `goto_window_bottom`       |
 | `d`   | Go to definition (**LSP**)                       | `goto_definition`          |
 | `y`   | Go to type definition (**LSP**)                  | `goto_type_definition`     |
 | `r`   | Go to references (**LSP**)                       | `goto_reference`           |
 | `i`   | Go to implementation (**LSP**)                   | `goto_implementation`      |
 | `a`   | Go to the last accessed/alternate file           | `goto_last_accessed_file`  |
+| `m`   | Go to the last modified/alternate file           | `goto_last_modified_file`  |
 | `n`   | Go to next buffer                                | `goto_next_buffer`         |
 | `p`   | Go to previous buffer                            | `goto_previous_buffer`     |
 | `.`   | Go to last modification in current file          | `goto_last_modification`   |
@@ -200,6 +207,8 @@ This layer is similar to vim keybindings as kakoune does not support window.
 | `v`, `Ctrl-v`          | Vertical right split           | `vsplit`          |
 | `s`, `Ctrl-s`          | Horizontal bottom split        | `hsplit`          |
 | `h`, `Ctrl-h`, `left`  | Move to left split             | `jump_view_left`  |
+| `f`                    | Go to files in the selection in horizontal splits  | `goto_file`                |
+| `F`                    | Go to files in the selection in vertical splits    | `goto_file`                |
 | `j`, `Ctrl-j`, `down`  | Move to split below            | `jump_view_down`  |
 | `k`, `Ctrl-k`, `up`    | Move to split above            | `jump_view_up`    |
 | `l`, `Ctrl-l`, `right` | Move to right split            | `jump_view_right` |
@@ -315,7 +324,7 @@ Keys to use within prompt, Remapping currently not supported.
 | `Ctrl-u`                | Delete to start of line                                                 |
 | `Ctrl-k`                | Delete to end of line                                                   |
 | `backspace`, `Ctrl-h`   | Delete previous char                                                    |
-| `delete`, `Ctrl-d`      | Delete previous char                                                    |
+| `delete`, `Ctrl-d`      | Delete next char                                                        |
 | `Ctrl-s`                | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later   |
 | `Ctrl-p`, `Up`          | Select previous history                                                 |
 | `Ctrl-n`, `Down`        | Select next history                                                     |
diff --git a/book/src/lang-support.md b/book/src/lang-support.md
new file mode 100644
index 00000000..3920f342
--- /dev/null
+++ b/book/src/lang-support.md
@@ -0,0 +1,10 @@
+# Language Support
+
+For more information like arguments passed to default LSP server,
+extensions assosciated with a filetype, custom LSP settings, filetype
+specific indent settings, etc see the default
+[`languages.toml`][languages.toml] file.
+
+{{#include ./generated/lang-support.md}}
+
+[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
diff --git a/book/src/remapping.md b/book/src/remapping.md
index fffd189b..1cdf9b1f 100644
--- a/book/src/remapping.md
+++ b/book/src/remapping.md
@@ -11,6 +11,8 @@ this:
 ```toml
 # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
 [keys.normal]
+C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file)
+C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file
 a = "move_char_left" # Maps the 'a' key to the move_char_left command
 w = "move_line_up" # Maps the 'w' key move_line_up
 "C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
@@ -21,6 +23,7 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions
 "A-x" = "normal_mode" # Maps Alt-X to enter normal mode
 j = { k = "normal_mode" } # Maps `jk` to exit insert mode
 ```
+> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command.
 
 Control, Shift and Alt modifiers are encoded respectively with the prefixes
 `C-`, `S-` and `A-`. Special keys are encoded as follows:
@@ -42,10 +45,9 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
 | Down         | `"down"`       |
 | Home         | `"home"`       |
 | End          | `"end"`        |
-| Page         | `"pageup"`     |
-| Page         | `"pagedown"`   |
+| Page Up      | `"pageup"`     |
+| Page Down    | `"pagedown"`   |
 | Tab          | `"tab"`        |
-| Back         | `"backtab"`    |
 | Delete       | `"del"`        |
 | Insert       | `"ins"`        |
 | Null         | `"null"`       |
@@ -54,4 +56,4 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
 Keys can be disabled by binding them to the `no_op` command.
 
 Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
-> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro.
+> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`.
diff --git a/book/src/themes.md b/book/src/themes.md
index 6b38fb43..40c14781 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -145,11 +145,12 @@ We use a similar set of scopes as
     - `conditional` - `if`, `else`
     - `repeat` - `for`, `while`, `loop`
     - `import` - `import`, `export`
-    - (TODO: return?)
+    - `return`
+  - `operator` - `or`, `in`
   - `directive` - Preprocessor directives (`#if` in C) 
   - `function` - `fn`, `func`
 
-- `operator` - `||`, `+=`, `>`, `or`
+- `operator` - `||`, `+=`, `>`
 
 - `function`
   - `builtin`
@@ -161,6 +162,20 @@ We use a similar set of scopes as
 
 - `namespace`
 
+- `markup`
+  - `heading`
+  - `list`
+    - `unnumbered`
+    - `numbered`
+  - `bold`
+  - `italic`
+  - `underline`
+    - `link`
+  - `quote`
+  - `raw`
+    - `inline`
+    - `block`
+
 #### Interface
 
 These scopes are used for theming the editor interface.
diff --git a/book/src/usage.md b/book/src/usage.md
index 6b7cbc41..cf7d9d48 100644
--- a/book/src/usage.md
+++ b/book/src/usage.md
@@ -23,8 +23,10 @@ If there is a selected register before invoking a change or delete command, the
 | `/`                | Last search           |
 | `:`                | Last executed command |
 | `"`                | Last yanked text      |
+| `_`                | Black hole            |
 
 > There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
+> The black hole register works as a no-op register, meaning no data will be written to / read from it.
 
 ## Surround
 
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 00000000..bdd771aa
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,37 @@
+# Contributing
+
+Contributors are very welcome! **No contribution is too small and all contributions are valued.**
+
+Some suggestions to get started:
+
+- You can look at the [good first issue][good-first-issue] label on the issue tracker.
+- Help with packaging on various distributions needed!
+- To use print debugging to the [Helix log file][log-file], you must:
+  * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
+  * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
+- If your preferred language is missing, integrating a tree-sitter grammar for
+    it and defining syntax highlight queries for it is straight forward and
+    doesn't require much knowledge of the internals.
+
+We provide an [architecture.md][architecture.md] that should give you
+a good overview of the internals.
+
+# Auto generated documentation
+
+Some parts of [the book][docs] are autogenerated from the code itself,
+like the list of `:commands` and supported languages. To generate these
+files, run
+
+```shell
+cargo xtask docgen
+```
+
+inside the project. We use [xtask][xtask] as an ad-hoc task runner and
+thus do not require any dependencies other than `cargo` (You don't have
+to `cargo install` anything either).
+
+[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy
+[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file
+[architecture.md]: ./architecture.md
+[docs]: https://docs.helix-editor.com/
+[xtask]: https://github.com/matklad/cargo-xtask
diff --git a/flake.lock b/flake.lock
index 2029d580..2c59f993 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "devshell": {
       "locked": {
-        "lastModified": 1632436039,
-        "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
+        "lastModified": 1637575296,
+        "narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=",
         "owner": "numtide",
         "repo": "devshell",
-        "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
+        "rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618",
         "type": "github"
       },
       "original": {
@@ -17,11 +17,11 @@
     },
     "flake-utils": {
       "locked": {
-        "lastModified": 1623875721,
-        "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
+        "lastModified": 1637014545,
+        "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
+        "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
         "type": "github"
       },
       "original": {
@@ -30,22 +30,6 @@
         "type": "github"
       }
     },
-    "flakeCompat": {
-      "flake": false,
-      "locked": {
-        "lastModified": 1627913399,
-        "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
-        "type": "github"
-      },
-      "original": {
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "type": "github"
-      }
-    },
     "nixCargoIntegration": {
       "inputs": {
         "devshell": "devshell",
@@ -57,11 +41,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1634796585,
-        "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
+        "lastModified": 1638425401,
+        "narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=",
         "owner": "yusdacra",
         "repo": "nix-cargo-integration",
-        "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
+        "rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37",
         "type": "github"
       },
       "original": {
@@ -72,11 +56,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1634782485,
-        "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
+        "lastModified": 1638376152,
+        "narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
+        "rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f",
         "type": "github"
       },
       "original": {
@@ -88,22 +72,22 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1628186154,
-        "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
+        "lastModified": 1637453606,
+        "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "06552b72346632b6943c8032e57e702ea12413bf",
+        "rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
         "repo": "nixpkgs",
         "type": "github"
       }
     },
     "root": {
       "inputs": {
-        "flakeCompat": "flakeCompat",
         "nixCargoIntegration": "nixCargoIntegration",
         "nixpkgs": "nixpkgs",
         "rust-overlay": "rust-overlay"
@@ -115,11 +99,11 @@
         "nixpkgs": "nixpkgs_2"
       },
       "locked": {
-        "lastModified": 1634869268,
-        "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
+        "lastModified": 1638497756,
+        "narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "c02c2d86354327317546501af001886fbb53d374",
+        "rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index 296a68d5..cbf10c97 100644
--- a/flake.nix
+++ b/flake.nix
@@ -9,10 +9,6 @@
       inputs.nixpkgs.follows = "nixpkgs";
       inputs.rustOverlay.follows = "rust-overlay";
     };
-    flakeCompat = {
-      url = "github:edolstra/flake-compat";
-      flake = false;
-    };
   };
 
   outputs = inputs@{ self, nixCargoIntegration, ... }:
@@ -63,7 +59,7 @@
             '';
           };
         shell = common: prev: {
-          packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
+          packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]);
           env = prev.env ++ [
             { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
             { name = "RUST_BACKTRACE"; value = "1"; }
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index ea695d34..0a2a56d9 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -36,5 +36,7 @@ similar = "2.1"
 
 etcetera = "0.3"
 
+chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
+
 [dev-dependencies]
 quickcheck = { version = "1", default-features = false }
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index cc966852..c037afef 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -2,6 +2,7 @@
 //! this module provides the functionality to insert the paired closing character.
 
 use crate::{Range, Rope, Selection, Tendril, Transaction};
+use log::debug;
 use smallvec::SmallVec;
 
 // Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
     ('`', '`'),
 ];
 
-const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
+// [TODO] build this dynamically in language config. see #992
+const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
 
 // insert hook:
 // Fn(doc, selection, char) => Option<Transaction>
@@ -25,40 +28,44 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202
 //
 // to simplify, maybe return Option<Transaction> and just reimplement the default
 
-// TODO: delete implementation where it erases the whole bracket (|) -> |
+// [TODO]
+// * delete implementation where it erases the whole bracket (|) -> |
+// * do not reduce to cursors; use whole selections, and surround with pair
+// * change to multi character pairs to handle cases like placing the cursor in the
+//   middle of triple quotes, and more exotic pairs like Jinja's {% %}
 
 #[must_use]
 pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+    debug!("autopairs hook selection: {:#?}", selection);
+
+    let cursors = selection.clone().cursors(doc.slice(..));
+
     for &(open, close) in PAIRS {
         if open == ch {
             if open == close {
-                return handle_same(doc, selection, open);
+                return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
             } else {
-                return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
+                return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
             }
         }
 
         if close == ch {
             // && char_at pos == close
-            return Some(handle_close(doc, selection, open, close));
+            return Some(handle_close(doc, &cursors, open, close));
         }
     }
 
     None
 }
 
-// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
-// for example "&'a mut", or "fn<'a>"
-
-fn next_char(doc: &Rope, pos: usize) -> Option<char> {
-    if pos >= doc.len_chars() {
+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
+    if pos == 0 {
         return None;
     }
-    Some(doc.char(pos))
-}
-// TODO: selections should be extended if range, moved if point.
 
-// TODO: if not cursor but selection, wrap on both sides of selection (surround)
+    doc.get_char(pos - 1)
+}
+
 fn handle_open(
     doc: &Rope,
     selection: &Selection,
@@ -66,98 +73,362 @@ fn handle_open(
     close: char,
     close_before: &str,
 ) -> Transaction {
-    let mut ranges = SmallVec::with_capacity(selection.len());
+    let mut end_ranges = SmallVec::with_capacity(selection.len());
 
     let mut offs = 0;
 
-    let transaction = Transaction::change_by_selection(doc, selection, |range| {
-        let pos = range.head;
-        let next = next_char(doc, pos);
+    let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+        let start_head = start_range.head;
 
-        let head = pos + offs + open.len_utf8();
-        // if selection, retain anchor, if cursor, move over
-        ranges.push(Range::new(
-            if range.is_empty() {
-                head
-            } else {
-                range.anchor + offs
-            },
-            head,
-        ));
+        let next = doc.get_char(start_head);
+        let end_head = start_head + offs + open.len_utf8();
+
+        let end_anchor = if start_range.is_empty() {
+            end_head
+        } else {
+            start_range.anchor + offs
+        };
+
+        end_ranges.push(Range::new(end_anchor, end_head));
 
         match next {
             Some(ch) if !close_before.contains(ch) => {
-                offs += 1;
-                // TODO: else return (use default handler that inserts open)
-                (pos, pos, Some(Tendril::from_char(open)))
+                offs += open.len_utf8();
+                (start_head, start_head, Some(Tendril::from_char(open)))
             }
             // None | Some(ch) if close_before.contains(ch) => {}
             _ => {
                 // insert open & close
-                let mut pair = Tendril::with_capacity(2);
-                pair.push_char(open);
-                pair.push_char(close);
-
-                offs += 2;
-
-                (pos, pos, Some(pair))
+                let pair = Tendril::from_iter([open, close]);
+                offs += open.len_utf8() + close.len_utf8();
+                (start_head, start_head, Some(pair))
             }
         }
     });
 
-    transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+    let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+    debug!("auto pair transaction: {:#?}", t);
+    t
 }
 
 fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
-    let mut ranges = SmallVec::with_capacity(selection.len());
+    let mut end_ranges = SmallVec::with_capacity(selection.len());
 
     let mut offs = 0;
 
-    let transaction = Transaction::change_by_selection(doc, selection, |range| {
-        let pos = range.head;
-        let next = next_char(doc, pos);
+    let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+        let start_head = start_range.head;
+        let next = doc.get_char(start_head);
+        let end_head = start_head + offs + close.len_utf8();
 
-        let head = pos + offs + close.len_utf8();
-        // if selection, retain anchor, if cursor, move over
-        ranges.push(Range::new(
-            if range.is_empty() {
-                head
-            } else {
-                range.anchor + offs
-            },
-            head,
-        ));
+        let end_anchor = if start_range.is_empty() {
+            end_head
+        } else {
+            start_range.anchor + offs
+        };
+
+        end_ranges.push(Range::new(end_anchor, end_head));
 
         if next == Some(close) {
-            //  return transaction that moves past close
-            (pos, pos, None) // no-op
+            // return transaction that moves past close
+            (start_head, start_head, None) // no-op
         } else {
             offs += close.len_utf8();
-
-            // TODO: else return (use default handler that inserts close)
-            (pos, pos, Some(Tendril::from_char(close)))
+            (start_head, start_head, Some(Tendril::from_char(close)))
         }
     });
 
-    transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+    transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
 }
 
-// handle cases where open and close is the same, or in triples ("""docstring""")
-fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
-    // if not cursor but selection, wrap
-    // let next = next char
+/// handle cases where open and close is the same, or in triples ("""docstring""")
+fn handle_same(
+    doc: &Rope,
+    selection: &Selection,
+    token: char,
+    close_before: &str,
+    open_before: &str,
+) -> Transaction {
+    let mut end_ranges = SmallVec::with_capacity(selection.len());
 
-    // if next == bracket {
-    //   // if start of syntax node, insert token twice (new pair because node is complete)
-    //   // elseif colsedBracketAt
-    //      // is_triple == allow triple && next 3 is equal
-    //      // cursor jump over
-    // }
-    //} else if allow_triple && followed by triple {
-    //}
-    //} else if next != word char && prev != bracket && prev != word char {
-    // // condition checks for cases like I' where you don't want I'' (or I'm)
-    //  insert pair ("")
-    //}
-    None
+    let mut offs = 0;
+
+    let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+        let start_head = start_range.head;
+        let end_head = start_head + offs + token.len_utf8();
+
+        // if selection, retain anchor, if cursor, move over
+        let end_anchor = if start_range.is_empty() {
+            end_head
+        } else {
+            start_range.anchor + offs
+        };
+
+        end_ranges.push(Range::new(end_anchor, end_head));
+
+        let next = doc.get_char(start_head);
+        let prev = prev_char(doc, start_head);
+
+        if next == Some(token) {
+            //  return transaction that moves past close
+            (start_head, start_head, None) // no-op
+        } else {
+            let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
+            pair.push_char(token);
+
+            // for equal pairs, don't insert both open and close if either
+            // side has a non-pair char
+            if (next.is_none() || close_before.contains(next.unwrap()))
+                && (prev.is_none() || open_before.contains(prev.unwrap()))
+            {
+                pair.push_char(token);
+            }
+
+            offs += pair.len();
+            (start_head, start_head, Some(pair))
+        }
+    });
+
+    transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use smallvec::smallvec;
+
+    fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
+        PAIRS.iter().filter(|(open, close)| open != close)
+    }
+
+    fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
+        PAIRS.iter().filter(|(open, close)| open == close)
+    }
+
+    fn test_hooks(
+        in_doc: &Rope,
+        in_sel: &Selection,
+        ch: char,
+        expected_doc: &Rope,
+        expected_sel: &Selection,
+    ) {
+        let trans = hook(&in_doc, &in_sel, ch).unwrap();
+        let mut actual_doc = in_doc.clone();
+        assert!(trans.apply(&mut actual_doc));
+        assert_eq!(expected_doc, &actual_doc);
+        assert_eq!(expected_sel, trans.selection().unwrap());
+    }
+
+    fn test_hooks_with_pairs<I, F, R>(
+        in_doc: &Rope,
+        in_sel: &Selection,
+        pairs: I,
+        get_expected_doc: F,
+        actual_sel: &Selection,
+    ) where
+        I: IntoIterator<Item = &'static (char, char)>,
+        F: Fn(char, char) -> R,
+        R: Into<Rope>,
+        Rope: From<R>,
+    {
+        pairs.into_iter().for_each(|(open, close)| {
+            test_hooks(
+                in_doc,
+                in_sel,
+                *open,
+                &Rope::from(get_expected_doc(*open, *close)),
+                actual_sel,
+            )
+        });
+    }
+
+    // [] indicates range
+
+    /// [] -> insert ( -> ([])
+    #[test]
+    fn test_insert_blank() {
+        test_hooks_with_pairs(
+            &Rope::new(),
+            &Selection::single(1, 0),
+            PAIRS,
+            |open, close| format!("{}{}", open, close),
+            &Selection::single(1, 1),
+        );
+    }
+
+    /// []              ([])
+    /// [] -> insert -> ([])
+    /// []              ([])
+    #[test]
+    fn test_insert_blank_multi_cursor() {
+        test_hooks_with_pairs(
+            &Rope::from("\n\n\n"),
+            &Selection::new(
+                smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
+                0,
+            ),
+            PAIRS,
+            |open, close| {
+                format!(
+                    "{open}{close}\n{open}{close}\n{open}{close}\n",
+                    open = open,
+                    close = close
+                )
+            },
+            &Selection::new(
+                smallvec!(Range::point(1), Range::point(4), Range::point(7),),
+                0,
+            ),
+        );
+    }
+
+    // [TODO] broken until it works with selections
+    /// fo[o] -> append ( -> fo[o(])
+    #[ignore]
+    #[test]
+    fn test_append() {
+        test_hooks_with_pairs(
+            &Rope::from("foo"),
+            &Selection::single(2, 4),
+            PAIRS,
+            |open, close| format!("foo{}{}", open, close),
+            &Selection::single(2, 5),
+        );
+    }
+
+    /// ([]) -> insert ) -> ()[]
+    #[test]
+    fn test_insert_close_inside_pair() {
+        for (open, close) in PAIRS {
+            let doc = Rope::from(format!("{}{}", open, close));
+
+            test_hooks(
+                &doc,
+                &Selection::single(2, 1),
+                *close,
+                &doc,
+                &Selection::point(2),
+            );
+        }
+    }
+
+    /// ([])                ()[]
+    /// ([]) -> insert ) -> ()[]
+    /// ([])                ()[]
+    #[test]
+    fn test_insert_close_inside_pair_multi_cursor() {
+        let sel = Selection::new(
+            smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+            0,
+        );
+
+        let expected_sel = Selection::new(
+            // smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
+            smallvec!(Range::point(2), Range::point(5), Range::point(8),),
+            0,
+        );
+
+        for (open, close) in PAIRS {
+            let doc = Rope::from(format!(
+                "{open}{close}\n{open}{close}\n{open}{close}\n",
+                open = open,
+                close = close
+            ));
+
+            test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+        }
+    }
+
+    /// ([]) -> insert ( -> (([]))
+    #[test]
+    fn test_insert_open_inside_pair() {
+        let sel = Selection::single(2, 1);
+        let expected_sel = Selection::point(2);
+
+        for (open, close) in differing_pairs() {
+            let doc = Rope::from(format!("{}{}", open, close));
+            let expected_doc = Rope::from(format!(
+                "{open}{open}{close}{close}",
+                open = open,
+                close = close
+            ));
+
+            test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+        }
+    }
+
+    /// ([]) -> insert " -> ("[]")
+    #[test]
+    fn test_insert_nested_open_inside_pair() {
+        let sel = Selection::single(2, 1);
+        let expected_sel = Selection::point(2);
+
+        for (outer_open, outer_close) in differing_pairs() {
+            let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
+
+            for (inner_open, inner_close) in matching_pairs() {
+                let expected_doc = Rope::from(format!(
+                    "{}{}{}{}",
+                    outer_open, inner_open, inner_close, outer_close
+                ));
+
+                test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+            }
+        }
+    }
+
+    /// []word -> insert ( -> ([]word
+    #[test]
+    fn test_insert_open_before_non_pair() {
+        test_hooks_with_pairs(
+            &Rope::from("word"),
+            &Selection::single(1, 0),
+            PAIRS,
+            |open, _| format!("{}word", open),
+            &Selection::point(1),
+        )
+    }
+
+    // [TODO] broken until it works with selections
+    /// [wor]d -> insert ( -> ([wor]d
+    #[test]
+    #[ignore]
+    fn test_insert_open_with_selection() {
+        test_hooks_with_pairs(
+            &Rope::from("word"),
+            &Selection::single(0, 4),
+            PAIRS,
+            |open, _| format!("{}word", open),
+            &Selection::single(1, 5),
+        )
+    }
+
+    /// we want pairs that are *not* the same char to be inserted after
+    /// a non-pair char, for cases like functions, but for pairs that are
+    /// the same char, we want to *not* insert a pair to handle cases like "I'm"
+    ///
+    /// word[]  -> insert ( -> word([])
+    /// word[]  -> insert ' -> word'[]
+    #[test]
+    fn test_insert_open_after_non_pair() {
+        let doc = Rope::from("word");
+        let sel = Selection::single(5, 4);
+        let expected_sel = Selection::point(5);
+
+        test_hooks_with_pairs(
+            &doc,
+            &sel,
+            differing_pairs(),
+            |open, close| format!("word{}{}", open, close),
+            &expected_sel,
+        );
+
+        test_hooks_with_pairs(
+            &doc,
+            &sel,
+            matching_pairs(),
+            |open, _| format!("word{}", open),
+            &expected_sel,
+        );
+    }
 }
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index ad1ba16a..4fcf51c9 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,7 +1,7 @@
 //! LSP diagnostic utility types.
 
 /// Describes the severity level of a [`Diagnostic`].
-#[derive(Debug, Eq, PartialEq)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
 pub enum Severity {
     Error,
     Warning,
@@ -17,7 +17,7 @@ pub struct Range {
 }
 
 /// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct Diagnostic {
     pub range: Range,
     pub line: usize,
diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs
new file mode 100644
index 00000000..e3cfe107
--- /dev/null
+++ b/helix-core/src/increment/date_time.rs
@@ -0,0 +1,490 @@
+use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
+use once_cell::sync::Lazy;
+use regex::Regex;
+use ropey::RopeSlice;
+
+use std::borrow::Cow;
+use std::cmp;
+
+use super::Increment;
+use crate::{Range, Tendril};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct DateTimeIncrementor {
+    date_time: NaiveDateTime,
+    range: Range,
+    fmt: &'static str,
+    field: DateField,
+}
+
+impl DateTimeIncrementor {
+    pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> {
+        let range = if range.is_empty() {
+            if range.anchor < text.len_chars() {
+                // Treat empty range as a cursor range.
+                range.put_cursor(text, range.anchor + 1, true)
+            } else {
+                // The range is empty and at the end of the text.
+                return None;
+            }
+        } else {
+            range
+        };
+
+        FORMATS.iter().find_map(|format| {
+            let from = range.from().saturating_sub(format.max_len);
+            let to = (range.from() + format.max_len).min(text.len_chars());
+
+            let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
+            let text: Cow<str> = text.slice(from..to).into();
+
+            let captures = format.regex.captures(&text)?;
+            if captures.len() - 1 != format.fields.len() {
+                return None;
+            }
+
+            let date_time = captures.get(0)?;
+            let offset = range.from() - from_in_text;
+            let range = Range::new(date_time.start() + offset, date_time.end() + offset);
+
+            let field = captures
+                .iter()
+                .skip(1)
+                .enumerate()
+                .find_map(|(i, capture)| {
+                    let capture = capture?;
+                    let capture_range = capture.range();
+
+                    if capture_range.contains(&from_in_text)
+                        && capture_range.contains(&(to_in_text - 1))
+                    {
+                        Some(format.fields[i])
+                    } else {
+                        None
+                    }
+                })?;
+
+            let has_date = format.fields.iter().any(|f| f.unit.is_date());
+            let has_time = format.fields.iter().any(|f| f.unit.is_time());
+
+            let date_time = &text[date_time.start()..date_time.end()];
+            let date_time = match (has_date, has_time) {
+                (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?,
+                (true, false) => {
+                    let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
+
+                    date.and_hms(0, 0, 0)
+                }
+                (false, true) => {
+                    let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
+
+                    NaiveDate::from_ymd(0, 1, 1).and_time(time)
+                }
+                (false, false) => return None,
+            };
+
+            Some(DateTimeIncrementor {
+                date_time,
+                range,
+                fmt: format.fmt,
+                field,
+            })
+        })
+    }
+}
+
+impl Increment for DateTimeIncrementor {
+    fn increment(&self, amount: i64) -> (Range, Tendril) {
+        let date_time = match self.field.unit {
+            DateUnit::Years => add_years(self.date_time, amount),
+            DateUnit::Months => add_months(self.date_time, amount),
+            DateUnit::Days => add_duration(self.date_time, Duration::days(amount)),
+            DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)),
+            DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)),
+            DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)),
+            DateUnit::AmPm => toggle_am_pm(self.date_time),
+        }
+        .unwrap_or(self.date_time);
+
+        (self.range, date_time.format(self.fmt).to_string().into())
+    }
+}
+
+static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| {
+    vec![
+        Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23
+        Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23
+        Format::new("%Y-%m-%d %H:%M"),    // 2021-11-24 07:12
+        Format::new("%Y/%m/%d %H:%M"),    // 2021/11/24 07:12
+        Format::new("%Y-%m-%d"),          // 2021-11-24
+        Format::new("%Y/%m/%d"),          // 2021/11/24
+        Format::new("%a %b %d %Y"),       // Wed Nov 24 2021
+        Format::new("%d-%b-%Y"),          // 24-Nov-2021
+        Format::new("%Y %b %d"),          // 2021 Nov 24
+        Format::new("%b %d, %Y"),         // Nov 24, 2021
+        Format::new("%-I:%M:%S %P"),      // 7:21:53 am
+        Format::new("%-I:%M %P"),         // 7:21 am
+        Format::new("%-I:%M:%S %p"),      // 7:21:53 AM
+        Format::new("%-I:%M %p"),         // 7:21 AM
+        Format::new("%H:%M:%S"),          // 23:24:23
+        Format::new("%H:%M"),             // 23:24
+    ]
+});
+
+#[derive(Debug)]
+struct Format {
+    fmt: &'static str,
+    fields: Vec<DateField>,
+    regex: Regex,
+    max_len: usize,
+}
+
+impl Format {
+    fn new(fmt: &'static str) -> Self {
+        let mut remaining = fmt;
+        let mut fields = Vec::new();
+        let mut regex = String::new();
+        let mut max_len = 0;
+
+        while let Some(i) = remaining.find('%') {
+            let after = &remaining[i + 1..];
+            let mut chars = after.chars();
+            let c = chars.next().unwrap();
+
+            let spec_len = if c == '-' {
+                1 + chars.next().unwrap().len_utf8()
+            } else {
+                c.len_utf8()
+            };
+
+            let specifier = &after[..spec_len];
+            let field = DateField::from_specifier(specifier).unwrap();
+            fields.push(field);
+            max_len += field.max_len + remaining[..i].len();
+            regex += &remaining[..i];
+            regex += &format!("({})", field.regex);
+            remaining = &after[spec_len..];
+        }
+
+        let regex = Regex::new(&regex).unwrap();
+
+        Self {
+            fmt,
+            fields,
+            regex,
+            max_len,
+        }
+    }
+}
+
+impl PartialEq for Format {
+    fn eq(&self, other: &Self) -> bool {
+        self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len
+    }
+}
+
+impl Eq for Format {}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+struct DateField {
+    regex: &'static str,
+    unit: DateUnit,
+    max_len: usize,
+}
+
+impl DateField {
+    fn from_specifier(specifier: &str) -> Option<Self> {
+        match specifier {
+            "Y" => Some(DateField {
+                regex: r"\d{4}",
+                unit: DateUnit::Years,
+                max_len: 5,
+            }),
+            "y" => Some(DateField {
+                regex: r"\d\d",
+                unit: DateUnit::Years,
+                max_len: 2,
+            }),
+            "m" => Some(DateField {
+                regex: r"[0-1]\d",
+                unit: DateUnit::Months,
+                max_len: 2,
+            }),
+            "d" => Some(DateField {
+                regex: r"[0-3]\d",
+                unit: DateUnit::Days,
+                max_len: 2,
+            }),
+            "-d" => Some(DateField {
+                regex: r"[1-3]?\d",
+                unit: DateUnit::Days,
+                max_len: 2,
+            }),
+            "a" => Some(DateField {
+                regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
+                unit: DateUnit::Days,
+                max_len: 3,
+            }),
+            "A" => Some(DateField {
+                regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
+                unit: DateUnit::Days,
+                max_len: 9,
+            }),
+            "b" | "h" => Some(DateField {
+                regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
+                unit: DateUnit::Months,
+                max_len: 3,
+            }),
+            "B" => Some(DateField {
+                regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
+                unit: DateUnit::Months,
+                max_len: 9,
+            }),
+            "H" => Some(DateField {
+                regex: r"[0-2]\d",
+                unit: DateUnit::Hours,
+                max_len: 2,
+            }),
+            "M" => Some(DateField {
+                regex: r"[0-5]\d",
+                unit: DateUnit::Minutes,
+                max_len: 2,
+            }),
+            "S" => Some(DateField {
+                regex: r"[0-5]\d",
+                unit: DateUnit::Seconds,
+                max_len: 2,
+            }),
+            "I" => Some(DateField {
+                regex: r"[0-1]\d",
+                unit: DateUnit::Hours,
+                max_len: 2,
+            }),
+            "-I" => Some(DateField {
+                regex: r"1?\d",
+                unit: DateUnit::Hours,
+                max_len: 2,
+            }),
+            "P" => Some(DateField {
+                regex: r"am|pm",
+                unit: DateUnit::AmPm,
+                max_len: 2,
+            }),
+            "p" => Some(DateField {
+                regex: r"AM|PM",
+                unit: DateUnit::AmPm,
+                max_len: 2,
+            }),
+            _ => None,
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum DateUnit {
+    Years,
+    Months,
+    Days,
+    Hours,
+    Minutes,
+    Seconds,
+    AmPm,
+}
+
+impl DateUnit {
+    fn is_date(self) -> bool {
+        matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days)
+    }
+
+    fn is_time(self) -> bool {
+        matches!(
+            self,
+            DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds
+        )
+    }
+}
+
+fn ndays_in_month(year: i32, month: u32) -> u32 {
+    // The first day of the next month...
+    let (y, m) = if month == 12 {
+        (year + 1, 1)
+    } else {
+        (year, month + 1)
+    };
+    let d = NaiveDate::from_ymd(y, m, 1);
+
+    // ...is preceded by the last day of the original month.
+    d.pred().day()
+}
+
+fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+    let month = (date_time.month0() as i64).checked_add(amount)?;
+    let year = date_time.year() + i32::try_from(month / 12).ok()?;
+    let year = if month.is_negative() { year - 1 } else { year };
+
+    // Normalize month
+    let month = month % 12;
+    let month = if month.is_negative() {
+        month + 12
+    } else {
+        month
+    } as u32
+        + 1;
+
+    let day = cmp::min(date_time.day(), ndays_in_month(year, month));
+
+    Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
+}
+
+fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+    let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?;
+    let ndays = ndays_in_month(year, date_time.month());
+
+    if date_time.day() > ndays {
+        let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
+        Some(d.succ().and_time(date_time.time()))
+    } else {
+        date_time.with_year(year)
+    }
+}
+
+fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> {
+    date_time.checked_add_signed(duration)
+}
+
+fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> {
+    if date_time.hour() < 12 {
+        add_duration(date_time, Duration::hours(12))
+    } else {
+        add_duration(date_time, Duration::hours(-12))
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::Rope;
+
+    #[test]
+    fn test_increment_date_times() {
+        let tests = [
+            // (original, cursor, amount, expected)
+            ("2020-02-28", 0, 1, "2021-02-28"),
+            ("2020-02-29", 0, 1, "2021-03-01"),
+            ("2020-01-31", 5, 1, "2020-02-29"),
+            ("2020-01-20", 5, 1, "2020-02-20"),
+            ("2021-01-01", 5, -1, "2020-12-01"),
+            ("2021-01-31", 5, -2, "2020-11-30"),
+            ("2020-02-28", 8, 1, "2020-02-29"),
+            ("2021-02-28", 8, 1, "2021-03-01"),
+            ("2021-02-28", 0, -1, "2020-02-28"),
+            ("2021-03-01", 0, -1, "2020-03-01"),
+            ("2020-02-29", 5, -1, "2020-01-29"),
+            ("2020-02-20", 5, -1, "2020-01-20"),
+            ("2020-02-29", 8, -1, "2020-02-28"),
+            ("2021-03-01", 8, -1, "2021-02-28"),
+            ("1980/12/21", 8, 100, "1981/03/31"),
+            ("1980/12/21", 8, -100, "1980/09/12"),
+            ("1980/12/21", 8, 1000, "1983/09/17"),
+            ("1980/12/21", 8, -1000, "1978/03/27"),
+            ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"),
+            ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"),
+            ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"),
+            ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"),
+            ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"),
+            ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"),
+            ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"),
+            ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"),
+            ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"),
+            ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"),
+            ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"),
+            ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"),
+            ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"),
+            ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"),
+            ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"),
+            ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"),
+            ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"),
+            ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"),
+            ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"),
+            ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"),
+            ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"),
+            ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"),
+            ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"),
+            ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"),
+            ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"),
+            ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"),
+            ("24-Nov-2021", 0, 1, "25-Nov-2021"),
+            ("24-Nov-2021", 3, 1, "24-Dec-2021"),
+            ("24-Nov-2021", 7, 1, "24-Nov-2022"),
+            ("2021 Nov 24", 0, 1, "2022 Nov 24"),
+            ("2021 Nov 24", 5, 1, "2021 Dec 24"),
+            ("2021 Nov 24", 9, 1, "2021 Nov 25"),
+            ("Nov 24, 2021", 0, 1, "Dec 24, 2021"),
+            ("Nov 24, 2021", 4, 1, "Nov 25, 2021"),
+            ("Nov 24, 2021", 8, 1, "Nov 24, 2022"),
+            ("7:21:53 am", 0, 1, "8:21:53 am"),
+            ("7:21:53 am", 3, 1, "7:22:53 am"),
+            ("7:21:53 am", 5, 1, "7:21:54 am"),
+            ("7:21:53 am", 8, 1, "7:21:53 pm"),
+            ("7:21:53 AM", 0, 1, "8:21:53 AM"),
+            ("7:21:53 AM", 3, 1, "7:22:53 AM"),
+            ("7:21:53 AM", 5, 1, "7:21:54 AM"),
+            ("7:21:53 AM", 8, 1, "7:21:53 PM"),
+            ("7:21 am", 0, 1, "8:21 am"),
+            ("7:21 am", 3, 1, "7:22 am"),
+            ("7:21 am", 5, 1, "7:21 pm"),
+            ("7:21 AM", 0, 1, "8:21 AM"),
+            ("7:21 AM", 3, 1, "7:22 AM"),
+            ("7:21 AM", 5, 1, "7:21 PM"),
+            ("23:24:23", 1, 1, "00:24:23"),
+            ("23:24:23", 3, 1, "23:25:23"),
+            ("23:24:23", 6, 1, "23:24:24"),
+            ("23:24", 1, 1, "00:24"),
+            ("23:24", 3, 1, "23:25"),
+        ];
+
+        for (original, cursor, amount, expected) in tests {
+            let rope = Rope::from_str(original);
+            let range = Range::new(cursor, cursor + 1);
+            assert_eq!(
+                DateTimeIncrementor::from_range(rope.slice(..), range)
+                    .unwrap()
+                    .increment(amount)
+                    .1,
+                expected.into()
+            );
+        }
+    }
+
+    #[test]
+    fn test_invalid_date_times() {
+        let tests = [
+            "0000-00-00",
+            "1980-2-21",
+            "1980-12-1",
+            "12345",
+            "2020-02-30",
+            "1999-12-32",
+            "19-12-32",
+            "1-2-3",
+            "0000/00/00",
+            "1980/2/21",
+            "1980/12/1",
+            "12345",
+            "2020/02/30",
+            "1999/12/32",
+            "19/12/32",
+            "1/2/3",
+            "123:456:789",
+            "11:61",
+            "2021-55-12 08:12:54",
+        ];
+
+        for invalid in tests {
+            let rope = Rope::from_str(invalid);
+            let range = Range::new(0, 1);
+
+            assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None)
+        }
+    }
+}
diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs
new file mode 100644
index 00000000..f5945774
--- /dev/null
+++ b/helix-core/src/increment/mod.rs
@@ -0,0 +1,8 @@
+pub mod date_time;
+pub mod number;
+
+use crate::{Range, Tendril};
+
+pub trait Increment {
+    fn increment(&self, amount: i64) -> (Range, Tendril);
+}
diff --git a/helix-core/src/numbers.rs b/helix-core/src/increment/number.rs
similarity index 96%
rename from helix-core/src/numbers.rs
rename to helix-core/src/increment/number.rs
index e9f3c898..a19b7e75 100644
--- a/helix-core/src/numbers.rs
+++ b/helix-core/src/increment/number.rs
@@ -2,6 +2,8 @@ use std::borrow::Cow;
 
 use ropey::RopeSlice;
 
+use super::Increment;
+
 use crate::{
     textobject::{textobject_word, TextObject},
     Range, Tendril,
@@ -9,9 +11,9 @@ use crate::{
 
 #[derive(Debug, PartialEq, Eq)]
 pub struct NumberIncrementor<'a> {
-    pub range: Range,
-    pub value: i64,
-    pub radix: u32,
+    value: i64,
+    radix: u32,
+    range: Range,
 
     text: RopeSlice<'a>,
 }
@@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> {
             text,
         })
     }
+}
 
-    /// Add `amount` to the number and return the formatted text.
-    pub fn incremented_text(&self, amount: i64) -> Tendril {
+impl<'a> Increment for NumberIncrementor<'a> {
+    fn increment(&self, amount: i64) -> (Range, Tendril) {
         let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
         let old_length = old_text.len();
         let new_value = self.value.wrapping_add(amount);
@@ -144,7 +147,7 @@ impl<'a> NumberIncrementor<'a> {
             }
         }
 
-        new_text.into()
+        (self.range, new_text.into())
     }
 }
 
@@ -366,7 +369,8 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
+                    .increment(amount)
+                    .1,
                 expected.into()
             );
         }
@@ -392,7 +396,8 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
+                    .increment(amount)
+                    .1,
                 expected.into()
             );
         }
@@ -419,7 +424,8 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
+                    .increment(amount)
+                    .1,
                 expected.into()
             );
         }
@@ -464,7 +470,8 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
+                    .increment(amount)
+                    .1,
                 expected.into()
             );
         }
@@ -491,7 +498,8 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
+                    .increment(amount)
+                    .1,
                 expected.into()
             );
         }
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index de7e95c1..92a59f31 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -5,18 +5,19 @@ pub mod diagnostic;
 pub mod diff;
 pub mod graphemes;
 pub mod history;
+pub mod increment;
 pub mod indent;
 pub mod line_ending;
 pub mod macros;
 pub mod match_brackets;
 pub mod movement;
-pub mod numbers;
 pub mod object;
 pub mod path;
 mod position;
 pub mod register;
 pub mod search;
 pub mod selection;
+pub mod shellwords;
 mod state;
 pub mod surround;
 pub mod syntax;
@@ -158,7 +159,7 @@ mod merge_toml_tests {
         ";
 
         let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
-            .expect("Couldn't parse built-in langauges config");
+            .expect("Couldn't parse built-in languages config");
         let user: Value = toml::from_str(USER).unwrap();
 
         let merged = merge_toml_values(base, user);
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index c5444eb7..b9eb497d 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -15,7 +15,11 @@ impl Register {
     }
 
     pub fn new_with_values(name: char, values: Vec<String>) -> Self {
-        Self { name, values }
+        if name == '_' {
+            Self::new(name)
+        } else {
+            Self { name, values }
+        }
     }
 
     pub const fn name(&self) -> char {
@@ -27,11 +31,15 @@ impl Register {
     }
 
     pub fn write(&mut self, values: Vec<String>) {
-        self.values = values;
+        if self.name != '_' {
+            self.values = values;
+        }
     }
 
     pub fn push(&mut self, value: String) {
-        self.values.push(value);
+        if self.name != '_' {
+            self.values.push(value);
+        }
     }
 }
 
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index b4d1dffa..116a1c7c 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -308,10 +308,10 @@ impl Range {
 }
 
 impl From<(usize, usize)> for Range {
-    fn from(tuple: (usize, usize)) -> Self {
+    fn from((anchor, head): (usize, usize)) -> Self {
         Self {
-            anchor: tuple.0,
-            head: tuple.1,
+            anchor,
+            head,
             horiz: None,
         }
     }
diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs
new file mode 100644
index 00000000..13f6f3e9
--- /dev/null
+++ b/helix-core/src/shellwords.rs
@@ -0,0 +1,164 @@
+use std::borrow::Cow;
+
+/// Get the vec of escaped / quoted / doublequoted filenames from the input str
+pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
+    enum State {
+        Normal,
+        NormalEscaped,
+        Quoted,
+        QuoteEscaped,
+        Dquoted,
+        DquoteEscaped,
+    }
+
+    use State::*;
+
+    let mut state = Normal;
+    let mut args: Vec<Cow<str>> = Vec::new();
+    let mut escaped = String::with_capacity(input.len());
+
+    let mut start = 0;
+    let mut end = 0;
+
+    for (i, c) in input.char_indices() {
+        state = match state {
+            Normal => match c {
+                '\\' => {
+                    escaped.push_str(&input[start..i]);
+                    start = i + 1;
+                    NormalEscaped
+                }
+                '"' => {
+                    end = i;
+                    Dquoted
+                }
+                '\'' => {
+                    end = i;
+                    Quoted
+                }
+                c if c.is_ascii_whitespace() => {
+                    end = i;
+                    Normal
+                }
+                _ => Normal,
+            },
+            NormalEscaped => Normal,
+            Quoted => match c {
+                '\\' => {
+                    escaped.push_str(&input[start..i]);
+                    start = i + 1;
+                    QuoteEscaped
+                }
+                '\'' => {
+                    end = i;
+                    Normal
+                }
+                _ => Quoted,
+            },
+            QuoteEscaped => Quoted,
+            Dquoted => match c {
+                '\\' => {
+                    escaped.push_str(&input[start..i]);
+                    start = i + 1;
+                    DquoteEscaped
+                }
+                '"' => {
+                    end = i;
+                    Normal
+                }
+                _ => Dquoted,
+            },
+            DquoteEscaped => Dquoted,
+        };
+
+        if i >= input.len() - 1 && end == 0 {
+            end = i + 1;
+        }
+
+        if end > 0 {
+            let esc_trim = escaped.trim();
+            let inp = &input[start..end];
+
+            if !(esc_trim.is_empty() && inp.trim().is_empty()) {
+                if esc_trim.is_empty() {
+                    args.push(inp.into());
+                } else {
+                    args.push([escaped, inp.into()].concat().into());
+                    escaped = "".to_string();
+                }
+            }
+            start = i + 1;
+            end = 0;
+        }
+    }
+    args
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_normal() {
+        let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
+        let result = shellwords(input);
+        let expected = vec![
+            Cow::from(":o"),
+            Cow::from("single_word"),
+            Cow::from("twó"),
+            Cow::from("wörds"),
+            Cow::from(r#"three "with escaping\"#),
+        ];
+        // TODO test is_owned and is_borrowed, once they get stabilized.
+        assert_eq!(expected, result);
+    }
+
+    #[test]
+    fn test_quoted() {
+        let quoted =
+            r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
+        let result = shellwords(quoted);
+        let expected = vec![
+            Cow::from(":o"),
+            Cow::from("single_word"),
+            Cow::from("twó wörds"),
+            Cow::from(r#"three' "with escaping\"#),
+            Cow::from("quote incomplete"),
+        ];
+        assert_eq!(expected, result);
+    }
+
+    #[test]
+    fn test_dquoted() {
+        let dquoted = r#":o "single_word" "twó wörds" "" "  ""\three\' \"with\ escaping\\" "dquote incomplete"#;
+        let result = shellwords(dquoted);
+        let expected = vec![
+            Cow::from(":o"),
+            Cow::from("single_word"),
+            Cow::from("twó wörds"),
+            Cow::from(r#"three' "with escaping\"#),
+            Cow::from("dquote incomplete"),
+        ];
+        assert_eq!(expected, result);
+    }
+
+    #[test]
+    fn test_mixed() {
+        let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
+        let result = shellwords(dquoted);
+        let expected = vec![
+            Cow::from(":o"),
+            Cow::from("single_word"),
+            Cow::from("twó wörds"),
+            Cow::from("three' \"with escaping\\"),
+            Cow::from("no space before"),
+            Cow::from("and after"),
+            Cow::from("$#%^@"),
+            Cow::from("%^&(%^"),
+            Cow::from(")(*&^%"),
+            Cow::from(r#"a\\b"#),
+            //last ' just changes to quoted but since we dont have anything after it, it should be ignored
+        ];
+        assert_eq!(expected, result);
+    }
+}
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index 32161b70..b53b0a78 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -1,4 +1,4 @@
-use crate::{search, Selection};
+use crate::{search, Range, Selection};
 use ropey::RopeSlice;
 
 pub const PAIRS: &[(char, char)] = &[
@@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
 pub fn find_nth_pairs_pos(
     text: RopeSlice,
     ch: char,
-    pos: usize,
+    range: Range,
     n: usize,
 ) -> Option<(usize, usize)> {
-    let (open, close) = get_pair(ch);
-
-    if text.len_chars() < 2 || pos >= text.len_chars() {
+    if text.len_chars() < 2 || range.to() >= text.len_chars() {
         return None;
     }
 
+    let (open, close) = get_pair(ch);
+    let pos = range.cursor(text);
+
     if open == close {
         if Some(open) == text.get_char(pos) {
-            // Special case: cursor is directly on a matching char.
-            match pos {
-                0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
-                _ if (pos + 1) == text.len_chars() => {
-                    Some((search::find_nth_prev(text, open, pos, n)?, pos))
-                }
-                // We return no match because there's no way to know which
-                // side of the char we should be searching on.
-                _ => None,
-            }
-        } else {
-            Some((
-                search::find_nth_prev(text, open, pos, n)?,
-                search::find_nth_next(text, close, pos, n)?,
-            ))
+            // Cursor is directly on match char. We return no match
+            // because there's no way to know which side of the char
+            // we should be searching on.
+            return None;
         }
+        Some((
+            search::find_nth_prev(text, open, pos, n)?,
+            search::find_nth_next(text, close, pos, n)?,
+        ))
     } else {
         Some((
             find_nth_open_pair(text, open, close, pos, n)?,
@@ -160,8 +154,8 @@ pub fn get_surround_pos(
 ) -> Option<Vec<usize>> {
     let mut change_pos = Vec::new();
 
-    for range in selection {
-        let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
+    for &range in selection {
+        let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
         if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
             return None;
         }
@@ -178,67 +172,91 @@ mod test {
     use ropey::Rope;
     use smallvec::SmallVec;
 
-    #[test]
-    fn test_find_nth_pairs_pos() {
-        let doc = Rope::from("some (text) here");
+    fn check_find_nth_pair_pos(
+        text: &str,
+        cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
+    ) {
+        let doc = Rope::from(text);
         let slice = doc.slice(..);
 
-        // cursor on [t]ext
-        assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
-        assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
-        // cursor on so[m]e
-        assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
-        // cursor on bracket itself
-        assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
+        for (cursor_pos, ch, n, expected_range) in cases {
+            let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n);
+            assert_eq!(
+                range, expected_range,
+                "Expected {:?}, got {:?}",
+                expected_range, range
+            );
+        }
+    }
+
+    #[test]
+    fn test_find_nth_pairs_pos() {
+        check_find_nth_pair_pos(
+            "some (text) here",
+            vec![
+                // cursor on [t]ext
+                (6, '(', 1, Some((5, 10))),
+                (6, ')', 1, Some((5, 10))),
+                // cursor on so[m]e
+                (2, '(', 1, None),
+                // cursor on bracket itself
+                (5, '(', 1, Some((5, 10))),
+                (10, '(', 1, Some((5, 10))),
+            ],
+        );
     }
 
     #[test]
     fn test_find_nth_pairs_pos_skip() {
-        let doc = Rope::from("(so (many (good) text) here)");
-        let slice = doc.slice(..);
-
-        // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
+        check_find_nth_pair_pos(
+            "(so (many (good) text) here)",
+            vec![
+                // cursor on go[o]d
+                (13, '(', 1, Some((10, 15))),
+                (13, '(', 2, Some((4, 21))),
+                (13, '(', 3, Some((0, 27))),
+            ],
+        );
     }
 
     #[test]
     fn test_find_nth_pairs_pos_same() {
-        let doc = Rope::from("'so 'many 'good' text' here'");
-        let slice = doc.slice(..);
-
-        // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
-        // cursor on the quotes
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
-        // this is the best we can do since opening and closing pairs are same
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
-        assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
+        check_find_nth_pair_pos(
+            "'so 'many 'good' text' here'",
+            vec![
+                // cursor on go[o]d
+                (13, '\'', 1, Some((10, 15))),
+                (13, '\'', 2, Some((4, 21))),
+                (13, '\'', 3, Some((0, 27))),
+                // cursor on the quotes
+                (10, '\'', 1, None),
+            ],
+        )
     }
 
     #[test]
     fn test_find_nth_pairs_pos_step() {
-        let doc = Rope::from("((so)((many) good (text))(here))");
-        let slice = doc.slice(..);
-
-        // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
+        check_find_nth_pair_pos(
+            "((so)((many) good (text))(here))",
+            vec![
+                // cursor on go[o]d
+                (15, '(', 1, Some((5, 24))),
+                (15, '(', 2, Some((0, 31))),
+            ],
+        )
     }
 
     #[test]
     fn test_find_nth_pairs_pos_mixed() {
-        let doc = Rope::from("(so [many {good} text] here)");
-        let slice = doc.slice(..);
-
-        // cursor on go[o]d
-        assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
-        assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
-        assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
+        check_find_nth_pair_pos(
+            "(so [many {good} text] here)",
+            vec![
+                // cursor on go[o]d
+                (13, '{', 1, Some((10, 15))),
+                (13, '[', 1, Some((4, 21))),
+                (13, '(', 1, Some((0, 27))),
+            ],
+        )
     }
 
     #[test]
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 142265a8..ef35fc75 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -50,7 +50,7 @@ pub struct Configuration {
 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
 pub struct LanguageConfiguration {
     #[serde(rename = "name")]
-    pub language_id: String,
+    pub language_id: String, // c-sharp, rust
     pub scope: String,           // source.rust
     pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
     #[serde(default)]
@@ -310,8 +310,9 @@ impl Loader {
 
     pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
         let line = Cow::from(source.line(0));
-        static SHEBANG_REGEX: Lazy<Regex> =
-            Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
+        static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
+            Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
+        });
         let configuration_id = SHEBANG_REGEX
             .captures(&line)
             .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index 24f063d4..21ceec04 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -114,7 +114,7 @@ pub fn textobject_surround(
     ch: char,
     count: usize,
 ) -> Range {
-    surround::find_nth_pairs_pos(slice, ch, range.head, count)
+    surround::find_nth_pairs_pos(slice, ch, range, count)
         .map(|(anchor, head)| match textobject {
             TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
             TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
@@ -170,7 +170,7 @@ mod test {
 
     #[test]
     fn test_textobject_word() {
-        // (text, [(cursor position, textobject, final range), ...])
+        // (text, [(char position, textobject, final range), ...])
         let tests = &[
             (
                 "cursor at beginning of doc",
@@ -269,7 +269,9 @@ mod test {
             let slice = doc.slice(..);
             for &case in scenario {
                 let (pos, objtype, expected_range) = case;
-                let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
+                // cursor is a single width selection
+                let range = Range::new(pos, pos + 1);
+                let result = textobject_word(slice, range, objtype, 1, false);
                 assert_eq!(
                     result,
                     expected_range.into(),
@@ -283,7 +285,7 @@ mod test {
 
     #[test]
     fn test_textobject_surround() {
-        // (text, [(cursor position, textobject, final range, count), ...])
+        // (text, [(cursor position, textobject, final range, surround char, count), ...])
         let tests = &[
             (
                 "simple (single) surround pairs",
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index dfc18fbe..d8d389f3 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -22,7 +22,7 @@ pub enum Assoc {
 }
 
 // ChangeSpec = Change | ChangeSet | Vec<Change>
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
 pub struct ChangeSet {
     pub(crate) changes: Vec<Operation>,
     /// The required document length. Will refuse to apply changes unless it matches.
@@ -30,16 +30,6 @@ pub struct ChangeSet {
     len_after: usize,
 }
 
-impl Default for ChangeSet {
-    fn default() -> Self {
-        Self {
-            changes: Vec::new(),
-            len: 0,
-            len_after: 0,
-        }
-    }
-}
-
 impl ChangeSet {
     pub fn with_capacity(capacity: usize) -> Self {
         Self {
@@ -330,7 +320,7 @@ impl ChangeSet {
     /// `true` when the set is empty.
     #[inline]
     pub fn is_empty(&self) -> bool {
-        self.changes.is_empty()
+        self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
     }
 
     /// Map a position through the changes.
@@ -419,7 +409,7 @@ impl ChangeSet {
 
 /// Transaction represents a single undoable unit of changes. Several changes can be grouped into
 /// a single transaction.
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
 pub struct Transaction {
     changes: ChangeSet,
     selection: Option<Selection>,
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 7fa65928..15cae582 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -337,7 +337,10 @@ impl Registry {
                         })
                         .await;
 
-                    value.expect("failed to initialize capabilities");
+                    if let Err(e) = value {
+                        log::error!("failed to initialize language server: {}", e);
+                        return;
+                    }
 
                     // next up, notify<initialized>
                     _client
diff --git a/helix-syntax/languages/tree-sitter-markdown b/helix-syntax/languages/tree-sitter-markdown
new file mode 160000
index 00000000..ad8c3291
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-markdown
@@ -0,0 +1 @@
+Subproject commit ad8c32917a16dfbb387d1da567bf0c3fb6fffde2
diff --git a/helix-syntax/languages/tree-sitter-wgsl b/helix-syntax/languages/tree-sitter-wgsl
new file mode 160000
index 00000000..f00ff522
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-wgsl
@@ -0,0 +1 @@
+Subproject commit f00ff52251edbd58f4d39c9c3204383253032c11
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index a0079feb..623c5bb9 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"]
 repository = "https://github.com/helix-editor/helix"
 homepage = "https://helix-editor.com"
 include = ["src/**/*", "README.md"]
+default-run = "hx"
 
 [package.metadata.nix]
 build = true
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index a795a56e..3e0b6d59 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -76,17 +76,27 @@ impl Application {
             None => Ok(def_lang_conf),
         };
 
-        let theme = if let Some(theme) = &config.theme {
-            match theme_loader.load(theme) {
-                Ok(theme) => theme,
-                Err(e) => {
-                    log::warn!("failed to load theme `{}` - {}", theme, e);
+        let true_color = config.editor.true_color || crate::true_color();
+        let theme = config
+            .theme
+            .as_ref()
+            .and_then(|theme| {
+                theme_loader
+                    .load(theme)
+                    .map_err(|e| {
+                        log::warn!("failed to load theme `{}` - {}", theme, e);
+                        e
+                    })
+                    .ok()
+                    .filter(|theme| (true_color || theme.is_16_color()))
+            })
+            .unwrap_or_else(|| {
+                if true_color {
                     theme_loader.default()
+                } else {
+                    theme_loader.base16_default()
                 }
-            }
-        } else {
-            theme_loader.default()
-        };
+            });
 
         let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
             .and_then(|conf| conf.try_into())
@@ -265,7 +275,7 @@ impl Application {
         use crate::commands::{insert::idle_completion, Context};
         use helix_view::document::Mode;
 
-        if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
+        if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
             return;
         }
         let editor_view = self
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index a7179c30..cd566720 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1,16 +1,16 @@
 use helix_core::{
     comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
     history::UndoKind,
+    increment::date_time::DateTimeIncrementor,
+    increment::{number::NumberIncrementor, Increment},
     indent,
     indent::IndentStyle,
     line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
     match_brackets,
     movement::{self, Direction},
-    numbers::NumberIncrementor,
     object, pos_at_coords,
     regex::{self, Regex, RegexBuilder},
-    register::Register,
-    search, selection, surround, textobject,
+    search, selection, shellwords, surround, textobject,
     unicode::width::UnicodeWidthChar,
     LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
     Transaction,
@@ -25,7 +25,7 @@ use helix_view::{
     Document, DocumentId, Editor, ViewId,
 };
 
-use anyhow::{anyhow, bail, Context as _};
+use anyhow::{anyhow, bail, ensure, Context as _};
 use helix_lsp::{
     block_on, lsp,
     util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
@@ -41,7 +41,7 @@ use crate::{
 
 use crate::job::{self, Job, Jobs};
 use futures_util::{FutureExt, StreamExt};
-use std::num::NonZeroUsize;
+use std::{collections::HashSet, num::NonZeroUsize};
 use std::{fmt, future::Future};
 
 use std::{
@@ -70,7 +70,7 @@ pub struct Context<'a> {
 impl<'a> Context<'a> {
     /// Push a new component onto the compositor.
     pub fn push_layer(&mut self, component: Box<dyn Component>) {
-        self.callback = Some(Box::new(|compositor: &mut Compositor| {
+        self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
             compositor.push(component)
         }));
     }
@@ -135,47 +135,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
     view.offset.row = line.saturating_sub(relative);
 }
 
-/// A command is composed of a static name, and a function that takes the current state plus a count,
-/// and does a side-effect on the state (usually by creating and applying a transaction).
-#[derive(Copy, Clone)]
-pub struct Command {
-    name: &'static str,
-    fun: fn(cx: &mut Context),
-    doc: &'static str,
+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
+/// Both of these types of commands can be mapped with keybindings in the config.toml.
+#[derive(Clone)]
+pub enum MappableCommand {
+    Typable {
+        name: String,
+        args: Vec<String>,
+        doc: String,
+    },
+    Static {
+        name: &'static str,
+        fun: fn(cx: &mut Context),
+        doc: &'static str,
+    },
 }
 
-macro_rules! commands {
+macro_rules! static_commands {
     ( $($name:ident, $doc:literal,)* ) => {
         $(
             #[allow(non_upper_case_globals)]
-            pub const $name: Self = Self {
+            pub const $name: Self = Self::Static {
                 name: stringify!($name),
                 fun: $name,
                 doc: $doc
             };
         )*
 
-        pub const COMMAND_LIST: &'static [Self] = &[
+        pub const STATIC_COMMAND_LIST: &'static [Self] = &[
             $( Self::$name, )*
         ];
     }
 }
 
-impl Command {
+impl MappableCommand {
     pub fn execute(&self, cx: &mut Context) {
-        (self.fun)(cx);
+        match &self {
+            MappableCommand::Typable { name, args, doc: _ } => {
+                let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
+                if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
+                    let mut cx = compositor::Context {
+                        editor: cx.editor,
+                        jobs: cx.jobs,
+                        scroll: None,
+                    };
+                    if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
+                        cx.editor.set_error(format!("{}", e));
+                    }
+                }
+            }
+            MappableCommand::Static { fun, .. } => (fun)(cx),
+        }
     }
 
-    pub fn name(&self) -> &'static str {
-        self.name
+    pub fn name(&self) -> &str {
+        match &self {
+            MappableCommand::Typable { name, .. } => name,
+            MappableCommand::Static { name, .. } => name,
+        }
     }
 
-    pub fn doc(&self) -> &'static str {
-        self.doc
+    pub fn doc(&self) -> &str {
+        match &self {
+            MappableCommand::Typable { doc, .. } => doc,
+            MappableCommand::Static { doc, .. } => doc,
+        }
     }
 
     #[rustfmt::skip]
-    commands!(
+    static_commands!(
         no_op, "Do nothing",
         move_char_left, "Move left",
         move_char_right, "Move right",
@@ -232,7 +261,9 @@ impl Command {
         extend_line, "Select current line, if already selected, extend to next line",
         extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
         delete_selection, "Delete selection",
+        delete_selection_noyank, "Delete selection, without yanking",
         change_selection, "Change selection (delete and enter insert mode)",
+        change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
         collapse_selection, "Collapse selection onto a single cursor",
         flip_selections, "Flip selection cursor and anchor",
         insert_mode, "Insert before selection",
@@ -258,11 +289,15 @@ impl Command {
         goto_implementation, "Goto implementation",
         goto_file_start, "Goto file start/line",
         goto_file_end, "Goto file end",
+        goto_file, "Goto files in selection",
+        goto_file_hsplit, "Goto files in selection (hsplit)",
+        goto_file_vsplit, "Goto files in selection (vsplit)",
         goto_reference, "Goto references",
         goto_window_top, "Goto window top",
-        goto_window_middle, "Goto window middle",
+        goto_window_center, "Goto window center",
         goto_window_bottom, "Goto window bottom",
         goto_last_accessed_file, "Goto last accessed file",
+        goto_last_modified_file, "Goto last modified file",
         goto_last_modification, "Goto last modification",
         goto_line, "Goto line",
         goto_last_line, "Goto last line",
@@ -327,6 +362,7 @@ impl Command {
         expand_selection, "Expand selection to parent syntax node",
         jump_forward, "Jump forward on jumplist",
         jump_backward, "Jump backward on jumplist",
+        save_selection, "Save the current selection to the jumplist",
         jump_view_right, "Jump to the split to the right",
         jump_view_left, "Jump to the split to the left",
         jump_view_up, "Jump to the split above",
@@ -359,36 +395,56 @@ impl Command {
         rename_symbol, "Rename symbol",
         increment, "Increment",
         decrement, "Decrement",
+        record_macro, "Record macro",
+        play_macro, "Play macro",
     );
 }
 
-impl fmt::Debug for Command {
+impl fmt::Debug for MappableCommand {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let Command { name, .. } = self;
-        f.debug_tuple("Command").field(name).finish()
+        f.debug_tuple("MappableCommand")
+            .field(&self.name())
+            .finish()
     }
 }
 
-impl fmt::Display for Command {
+impl fmt::Display for MappableCommand {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let Command { name, .. } = self;
-        f.write_str(name)
+        f.write_str(self.name())
     }
 }
 
-impl std::str::FromStr for Command {
+impl std::str::FromStr for MappableCommand {
     type Err = anyhow::Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Command::COMMAND_LIST
-            .iter()
-            .copied()
-            .find(|cmd| cmd.name == s)
-            .ok_or_else(|| anyhow!("No command named '{}'", s))
+        if let Some(suffix) = s.strip_prefix(':') {
+            let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
+            let name = typable_command
+                .next()
+                .ok_or_else(|| anyhow!("Expected typable command name"))?;
+            let args = typable_command
+                .map(|s| s.to_owned())
+                .collect::<Vec<String>>();
+            cmd::TYPABLE_COMMAND_MAP
+                .get(name)
+                .map(|cmd| MappableCommand::Typable {
+                    name: cmd.name.to_owned(),
+                    doc: format!(":{} {:?}", cmd.name, args),
+                    args,
+                })
+                .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
+        } else {
+            MappableCommand::STATIC_COMMAND_LIST
+                .iter()
+                .cloned()
+                .find(|cmd| cmd.name() == s)
+                .ok_or_else(|| anyhow!("No command named '{}'", s))
+        }
     }
 }
 
-impl<'de> Deserialize<'de> for Command {
+impl<'de> Deserialize<'de> for MappableCommand {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
         D: Deserializer<'de>,
@@ -398,9 +454,27 @@ impl<'de> Deserialize<'de> for Command {
     }
 }
 
-impl PartialEq for Command {
+impl PartialEq for MappableCommand {
     fn eq(&self, other: &Self) -> bool {
-        self.name() == other.name()
+        match (self, other) {
+            (
+                MappableCommand::Typable {
+                    name: first_name, ..
+                },
+                MappableCommand::Typable {
+                    name: second_name, ..
+                },
+            ) => first_name == second_name,
+            (
+                MappableCommand::Static {
+                    name: first_name, ..
+                },
+                MappableCommand::Static {
+                    name: second_name, ..
+                },
+            ) => first_name == second_name,
+            _ => false,
+        }
     }
 }
 
@@ -599,8 +673,15 @@ fn kill_to_line_end(cx: &mut Context) {
 
     let selection = doc.selection(view.id).clone().transform(|range| {
         let line = range.cursor_line(text);
-        let pos = line_end_char_index(&text, line);
-        range.put_cursor(text, pos, true)
+        let line_end_pos = line_end_char_index(&text, line);
+        let pos = range.cursor(text);
+
+        let mut new_range = range.put_cursor(text, line_end_pos, true);
+        // don't want to remove the line separator itself if the cursor doesn't reach the end of line.
+        if pos != line_end_pos {
+            new_range.head = line_end_pos;
+        }
+        new_range
     });
     delete_selection_insert_mode(doc, view, &selection);
 }
@@ -729,10 +810,12 @@ fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) ->
 }
 
 fn goto_window(cx: &mut Context, align: Align) {
+    let count = cx.count() - 1;
     let (view, doc) = current!(cx.editor);
 
     let height = view.inner_area().height as usize;
 
+    // respect user given count if any
     // - 1 so we have at least one gap in the middle.
     // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
     // as we type
@@ -741,10 +824,11 @@ fn goto_window(cx: &mut Context, align: Align) {
     let last_line = view.last_line(doc);
 
     let line = match align {
-        Align::Top => (view.offset.row + scrolloff),
-        Align::Center => (view.offset.row + (height / 2)),
-        Align::Bottom => last_line.saturating_sub(scrolloff),
+        Align::Top => (view.offset.row + scrolloff + count),
+        Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
+        Align::Bottom => last_line.saturating_sub(scrolloff + count),
     }
+    .max(view.offset.row + scrolloff)
     .min(last_line.saturating_sub(scrolloff));
 
     let pos = doc.text().line_to_char(line);
@@ -756,7 +840,7 @@ fn goto_window_top(cx: &mut Context) {
     goto_window(cx, Align::Top)
 }
 
-fn goto_window_middle(cx: &mut Context) {
+fn goto_window_center(cx: &mut Context) {
     goto_window(cx, Align::Center)
 }
 
@@ -834,6 +918,49 @@ fn goto_file_end(cx: &mut Context) {
     doc.set_selection(view.id, selection);
 }
 
+fn goto_file(cx: &mut Context) {
+    goto_file_impl(cx, Action::Replace);
+}
+
+fn goto_file_hsplit(cx: &mut Context) {
+    goto_file_impl(cx, Action::HorizontalSplit);
+}
+
+fn goto_file_vsplit(cx: &mut Context) {
+    goto_file_impl(cx, Action::VerticalSplit);
+}
+
+fn goto_file_impl(cx: &mut Context, action: Action) {
+    let (view, doc) = current_ref!(cx.editor);
+    let text = doc.text();
+    let selections = doc.selection(view.id);
+    let mut paths: Vec<_> = selections
+        .iter()
+        .map(|r| text.slice(r.from()..r.to()).to_string())
+        .collect();
+    let primary = selections.primary();
+    if selections.len() == 1 && primary.to() - primary.from() == 1 {
+        let current_word = movement::move_next_long_word_start(
+            text.slice(..),
+            movement::move_prev_long_word_start(text.slice(..), primary, 1),
+            1,
+        );
+        paths.clear();
+        paths.push(
+            text.slice(current_word.from()..current_word.to())
+                .to_string(),
+        );
+    }
+    for sel in paths {
+        let p = sel.trim();
+        if !p.is_empty() {
+            if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
+                cx.editor.set_error(format!("Open file failed: {:?}", e));
+            }
+        }
+    }
+}
+
 fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
 where
     F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1693,19 +1820,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
     );
 }
 
-fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
-    let text = doc.text().slice(..);
-    let selection = doc.selection(view_id);
+enum Operation {
+    Delete,
+    Change,
+}
 
-    // first yank the selection
-    let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
-    reg.write(values);
+fn delete_selection_impl(cx: &mut Context, op: Operation) {
+    let (view, doc) = current!(cx.editor);
+
+    let text = doc.text().slice(..);
+    let selection = doc.selection(view.id);
+
+    if cx.register != Some('_') {
+        // first yank the selection
+        let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
+        let reg_name = cx.register.unwrap_or('"');
+        let registers = &mut cx.editor.registers;
+        let reg = registers.get_mut(reg_name);
+        reg.write(values);
+    };
 
     // then delete
     let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
         (range.from(), range.to(), None)
     });
-    doc.apply(&transaction, view_id);
+    doc.apply(&transaction, view.id);
+
+    match op {
+        Operation::Delete => {
+            doc.append_changes_to_history(view.id);
+            // exit select mode, if currently in select mode
+            exit_select_mode(cx);
+        }
+        Operation::Change => {
+            enter_insert_mode(doc);
+        }
+    }
 }
 
 #[inline]
@@ -1720,25 +1870,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
 }
 
 fn delete_selection(cx: &mut Context) {
-    let reg_name = cx.register.unwrap_or('"');
-    let (view, doc) = current!(cx.editor);
-    let registers = &mut cx.editor.registers;
-    let reg = registers.get_mut(reg_name);
-    delete_selection_impl(reg, doc, view.id);
+    delete_selection_impl(cx, Operation::Delete);
+}
 
-    doc.append_changes_to_history(view.id);
-
-    // exit select mode, if currently in select mode
-    exit_select_mode(cx);
+fn delete_selection_noyank(cx: &mut Context) {
+    cx.register = Some('_');
+    delete_selection_impl(cx, Operation::Delete);
 }
 
 fn change_selection(cx: &mut Context) {
-    let reg_name = cx.register.unwrap_or('"');
-    let (view, doc) = current!(cx.editor);
-    let registers = &mut cx.editor.registers;
-    let reg = registers.get_mut(reg_name);
-    delete_selection_impl(reg, doc, view.id);
-    enter_insert_mode(doc);
+    delete_selection_impl(cx, Operation::Change);
+}
+
+fn change_selection_noyank(cx: &mut Context) {
+    cx.register = Some('_');
+    delete_selection_impl(cx, Operation::Change);
 }
 
 fn collapse_selection(cx: &mut Context) {
@@ -1806,7 +1952,7 @@ fn append_mode(cx: &mut Context) {
     doc.set_selection(view.id, selection);
 }
 
-mod cmd {
+pub mod cmd {
     use super::*;
     use std::collections::HashMap;
 
@@ -1819,13 +1965,13 @@ mod cmd {
         pub aliases: &'static [&'static str],
         pub doc: &'static str,
         // params, flags, helper, completer
-        pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
+        pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
         pub completer: Option<Completer>,
     }
 
     fn quit(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         // last view and we have unsaved changes
@@ -1840,7 +1986,7 @@ mod cmd {
 
     fn force_quit(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         cx.editor.close(view!(cx.editor).id);
@@ -1850,17 +1996,19 @@ mod cmd {
 
     fn open(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        let path = args.get(0).context("wrong argument count")?;
-        let _ = cx.editor.open(path.into(), Action::Replace)?;
+        ensure!(!args.is_empty(), "wrong argument count");
+        for arg in args {
+            let _ = cx.editor.open(arg.as_ref().into(), Action::Replace)?;
+        }
         Ok(())
     }
 
     fn buffer_close(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let view = view!(cx.editor);
@@ -1871,7 +2019,7 @@ mod cmd {
 
     fn force_buffer_close(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let view = view!(cx.editor);
@@ -1880,15 +2028,12 @@ mod cmd {
         Ok(())
     }
 
-    fn write_impl<P: AsRef<Path>>(
-        cx: &mut compositor::Context,
-        path: Option<P>,
-    ) -> anyhow::Result<()> {
+    fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
         let jobs = &mut cx.jobs;
         let (_, doc) = current!(cx.editor);
 
-        if let Some(path) = path {
-            doc.set_path(Some(path.as_ref()))
+        if let Some(ref path) = path {
+            doc.set_path(Some(path.as_ref().as_ref()))
                 .context("invalid filepath")?;
         }
         if doc.path().is_none() {
@@ -1907,12 +2052,17 @@ mod cmd {
         });
         let future = doc.format_and_save(fmt);
         cx.jobs.add(Job::new(future).wait_before_exiting());
+
+        if path.is_some() {
+            let id = doc.id();
+            let _ = cx.editor.refresh_language_server(id);
+        }
         Ok(())
     }
 
     fn write(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_impl(cx, args.first())
@@ -1920,7 +2070,7 @@ mod cmd {
 
     fn new_file(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         cx.editor.new_file(Action::Replace);
@@ -1930,7 +2080,7 @@ mod cmd {
 
     fn format(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let (_, doc) = current!(cx.editor);
@@ -1945,7 +2095,7 @@ mod cmd {
     }
     fn set_indent_style(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         use IndentStyle::*;
@@ -1965,7 +2115,7 @@ mod cmd {
         // Attempt to parse argument as an indent style.
         let style = match args.get(0) {
             Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
-            Some(&"0") => Some(Tabs),
+            Some(Cow::Borrowed("0")) => Some(Tabs),
             Some(arg) => arg
                 .parse::<u8>()
                 .ok()
@@ -1984,7 +2134,7 @@ mod cmd {
     /// Sets or reports the current document's line ending setting.
     fn set_line_ending(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         use LineEnding::*;
@@ -2028,7 +2178,7 @@ mod cmd {
 
     fn earlier(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2044,7 +2194,7 @@ mod cmd {
 
     fn later(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2059,7 +2209,7 @@ mod cmd {
 
     fn write_quit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_impl(cx, args.first())?;
@@ -2068,7 +2218,7 @@ mod cmd {
 
     fn force_write_quit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_impl(cx, args.first())?;
@@ -2099,7 +2249,7 @@ mod cmd {
 
     fn write_all_impl(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
         quit: bool,
         force: bool,
@@ -2135,7 +2285,7 @@ mod cmd {
 
     fn write_all(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_all_impl(cx, args, event, false, false)
@@ -2143,7 +2293,7 @@ mod cmd {
 
     fn write_all_quit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_all_impl(cx, args, event, true, false)
@@ -2151,7 +2301,7 @@ mod cmd {
 
     fn force_write_all_quit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_all_impl(cx, args, event, true, true)
@@ -2159,7 +2309,7 @@ mod cmd {
 
     fn quit_all_impl(
         editor: &mut Editor,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
         force: bool,
     ) -> anyhow::Result<()> {
@@ -2178,23 +2328,23 @@ mod cmd {
 
     fn quit_all(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
-        quit_all_impl(&mut cx.editor, args, event, false)
+        quit_all_impl(cx.editor, args, event, false)
     }
 
     fn force_quit_all(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
-        quit_all_impl(&mut cx.editor, args, event, true)
+        quit_all_impl(cx.editor, args, event, true)
     }
 
     fn cquit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let exit_code = args
@@ -2213,85 +2363,91 @@ mod cmd {
 
     fn theme(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        let theme = args.first().context("theme not provided")?;
-        cx.editor.set_theme_from_name(theme)
+        let theme = args.first().context("Theme not provided")?;
+        let theme = cx
+            .editor
+            .theme_loader
+            .load(theme)
+            .with_context(|| format!("Failed setting theme {}", theme))?;
+        let true_color = cx.editor.config.true_color || crate::true_color();
+        if !(true_color || theme.is_16_color()) {
+            bail!("Unsupported theme: theme requires true color support");
+        }
+        cx.editor.set_theme(theme);
+        Ok(())
     }
 
     fn yank_main_selection_to_clipboard(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
+        yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
     }
 
     fn yank_joined_to_clipboard(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let (_, doc) = current!(cx.editor);
-        let separator = args
-            .first()
-            .copied()
-            .unwrap_or_else(|| doc.line_ending.as_str());
-        yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
+        let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+        let separator = args.first().unwrap_or(&default_sep);
+        yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
     }
 
     fn yank_main_selection_to_primary_clipboard(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
+        yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
     }
 
     fn yank_joined_to_primary_clipboard(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let (_, doc) = current!(cx.editor);
-        let separator = args
-            .first()
-            .copied()
-            .unwrap_or_else(|| doc.line_ending.as_str());
-        yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
+        let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+        let separator = args.first().unwrap_or(&default_sep);
+        yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
     }
 
     fn paste_clipboard_after(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+        paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
     }
 
     fn paste_clipboard_before(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+        paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
     }
 
     fn paste_primary_clipboard_after(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+        paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
     }
 
     fn paste_primary_clipboard_before(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+        paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
     }
 
     fn replace_selections_with_clipboard_impl(
@@ -2318,7 +2474,7 @@ mod cmd {
 
     fn replace_selections_with_clipboard(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
@@ -2326,7 +2482,7 @@ mod cmd {
 
     fn replace_selections_with_primary_clipboard(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
@@ -2334,7 +2490,7 @@ mod cmd {
 
     fn show_clipboard_provider(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         cx.editor
@@ -2344,12 +2500,13 @@ mod cmd {
 
     fn change_current_directory(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let dir = helix_core::path::expand_tilde(
             args.first()
                 .context("target directory not provided")?
+                .as_ref()
                 .as_ref(),
         );
 
@@ -2367,7 +2524,7 @@ mod cmd {
 
     fn show_current_directory(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
@@ -2379,7 +2536,7 @@ mod cmd {
     /// Sets the [`Document`]'s encoding..
     fn set_encoding(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let (_, doc) = current!(cx.editor);
@@ -2395,7 +2552,7 @@ mod cmd {
     /// Reload the [`Document`] from its source file.
     fn reload(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let (view, doc) = current!(cx.editor);
@@ -2404,7 +2561,7 @@ mod cmd {
 
     fn tree_sitter_scopes(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let (view, doc) = current!(cx.editor);
@@ -2418,15 +2575,18 @@ mod cmd {
 
     fn vsplit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let id = view!(cx.editor).doc;
 
-        if let Some(path) = args.get(0) {
-            cx.editor.open(path.into(), Action::VerticalSplit)?;
-        } else {
+        if args.is_empty() {
             cx.editor.switch(id, Action::VerticalSplit);
+        } else {
+            for arg in args {
+                cx.editor
+                    .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
+            }
         }
 
         Ok(())
@@ -2434,15 +2594,18 @@ mod cmd {
 
     fn hsplit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let id = view!(cx.editor).doc;
 
-        if let Some(path) = args.get(0) {
-            cx.editor.open(path.into(), Action::HorizontalSplit)?;
-        } else {
+        if args.is_empty() {
             cx.editor.switch(id, Action::HorizontalSplit);
+        } else {
+            for arg in args {
+                cx.editor
+                    .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
+            }
         }
 
         Ok(())
@@ -2450,7 +2613,7 @@ mod cmd {
 
     fn tutor(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let path = helix_core::runtime_dir().join("tutor.txt");
@@ -2460,6 +2623,24 @@ mod cmd {
         Ok(())
     }
 
+    pub(super) fn goto_line_number(
+        cx: &mut compositor::Context,
+        args: &[Cow<str>],
+        _event: PromptEvent,
+    ) -> anyhow::Result<()> {
+        ensure!(!args.is_empty(), "Line number required");
+
+        let line = args[0].parse::<usize>()?;
+
+        goto_line_impl(cx.editor, NonZeroUsize::new(line));
+
+        let (view, doc) = current!(cx.editor);
+
+        view.ensure_cursor_in_view(doc, line);
+
+        Ok(())
+    }
+
     pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
         TypableCommand {
             name: "quit",
@@ -2513,7 +2694,7 @@ mod cmd {
         TypableCommand {
             name: "format",
             aliases: &["fmt"],
-            doc: "Format the file using a formatter.",
+            doc: "Format the file using the LSP formatter.",
             fun: format,
             completer: None,
         },
@@ -2604,7 +2785,7 @@ mod cmd {
         TypableCommand {
             name: "theme",
             aliases: &[],
-            doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
+            doc: "Change the editor theme.",
             fun: theme,
             completer: Some(completers::theme),
         },
@@ -2688,7 +2869,7 @@ mod cmd {
         TypableCommand {
             name: "change-current-directory",
             aliases: &["cd"],
-            doc: "Change the current working directory (:cd <dir>).",
+            doc: "Change the current working directory.",
             fun: change_current_directory,
             completer: Some(completers::directory),
         },
@@ -2741,17 +2922,25 @@ mod cmd {
             fun: tutor,
             completer: None,
         },
+        TypableCommand {
+            name: "goto",
+            aliases: &["g"],
+            doc: "Go to line number.",
+            fun: goto_line_number,
+            completer: None,
+        }
     ];
 
-    pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
-        TYPABLE_COMMAND_LIST
-            .iter()
-            .flat_map(|cmd| {
-                std::iter::once((cmd.name, cmd))
-                    .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
-            })
-            .collect()
-    });
+    pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
+        Lazy::new(|| {
+            TYPABLE_COMMAND_LIST
+                .iter()
+                .flat_map(|cmd| {
+                    std::iter::once((cmd.name, cmd))
+                        .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
+                })
+                .collect()
+        });
 }
 
 fn command_mode(cx: &mut Context) {
@@ -2777,7 +2966,7 @@ fn command_mode(cx: &mut Context) {
                 if let Some(cmd::TypableCommand {
                     completer: Some(completer),
                     ..
-                }) = cmd::COMMANDS.get(parts[0])
+                }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
                 {
                     completer(part)
                         .into_iter()
@@ -2803,8 +2992,18 @@ fn command_mode(cx: &mut Context) {
                 return;
             }
 
-            if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
-                if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
+            // If command is numeric, interpret as line number and go there.
+            if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
+                if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
+                    cx.editor.set_error(format!("{}", e));
+                }
+                return;
+            }
+
+            // Handle typable commands
+            if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
+                let args = shellwords::shellwords(input);
+                if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
                     cx.editor.set_error(format!("{}", e));
                 }
             } else {
@@ -2816,7 +3015,7 @@ fn command_mode(cx: &mut Context) {
     prompt.doc_fn = Box::new(|input: &str| {
         let part = input.split(' ').next().unwrap_or_default();
 
-        if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
+        if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
             return Some(doc);
         }
 
@@ -3254,7 +3453,7 @@ fn apply_workspace_edit(
 
 fn last_picker(cx: &mut Context) {
     // TODO: last picker does not seem to work well with buffer_picker
-    cx.callback = Some(Box::new(|compositor: &mut Compositor| {
+    cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
         if let Some(picker) = compositor.last_picker.take() {
             compositor.push(picker);
         }
@@ -3436,10 +3635,14 @@ fn push_jump(editor: &mut Editor) {
 }
 
 fn goto_line(cx: &mut Context) {
-    if let Some(count) = cx.count {
-        push_jump(cx.editor);
+    goto_line_impl(cx.editor, cx.count)
+}
 
-        let (view, doc) = current!(cx.editor);
+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
+    if let Some(count) = count {
+        push_jump(editor);
+
+        let (view, doc) = current!(editor);
         let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
             // If the last line is blank, don't jump to it.
             doc.text().len_lines().saturating_sub(2)
@@ -3498,6 +3701,20 @@ fn goto_last_modification(cx: &mut Context) {
     }
 }
 
+fn goto_last_modified_file(cx: &mut Context) {
+    let view = view!(cx.editor);
+    let alternate_file = view
+        .last_modified_docs
+        .into_iter()
+        .flatten()
+        .find(|&id| id != view.doc);
+    if let Some(alt) = alternate_file {
+        cx.editor.switch(alt, Action::Replace);
+    } else {
+        cx.editor.set_error("no last modified buffer".to_owned())
+    }
+}
+
 fn select_mode(cx: &mut Context) {
     let (view, doc) = current!(cx.editor);
     let text = doc.text().slice(..);
@@ -3982,8 +4199,9 @@ pub mod insert {
     // The default insert hook: simply insert the character
     #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
     fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+        let cursors = selection.clone().cursors(doc.slice(..));
         let t = Tendril::from_char(ch);
-        let transaction = Transaction::insert(doc, selection, t);
+        let transaction = Transaction::insert(doc, &cursors, t);
         Some(transaction)
     }
 
@@ -3998,11 +4216,11 @@ pub mod insert {
         };
 
         let text = doc.text();
-        let selection = doc.selection(view.id).clone().cursors(text.slice(..));
+        let selection = doc.selection(view.id);
 
         // run through insert hooks, stopping on the first one that returns Some(t)
         for hook in hooks {
-            if let Some(transaction) = hook(text, &selection, c) {
+            if let Some(transaction) = hook(text, selection, c) {
                 doc.apply(&transaction, view.id);
                 break;
             }
@@ -4317,11 +4535,8 @@ fn yank_joined_to_clipboard_impl(
 
 fn yank_joined_to_clipboard(cx: &mut Context) {
     let line_ending = doc!(cx.editor).line_ending;
-    let _ = yank_joined_to_clipboard_impl(
-        &mut cx.editor,
-        line_ending.as_str(),
-        ClipboardType::Clipboard,
-    );
+    let _ =
+        yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
     exit_select_mode(cx);
 }
 
@@ -4346,20 +4561,17 @@ fn yank_main_selection_to_clipboard_impl(
 }
 
 fn yank_main_selection_to_clipboard(cx: &mut Context) {
-    let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+    let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
 }
 
 fn yank_joined_to_primary_clipboard(cx: &mut Context) {
     let line_ending = doc!(cx.editor).line_ending;
-    let _ = yank_joined_to_clipboard_impl(
-        &mut cx.editor,
-        line_ending.as_str(),
-        ClipboardType::Selection,
-    );
+    let _ =
+        yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
 }
 
 fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
-    let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+    let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
     exit_select_mode(cx);
 }
 
@@ -4374,11 +4586,12 @@ fn paste_impl(
     doc: &mut Document,
     view: &View,
     action: Paste,
+    count: usize,
 ) -> Option<Transaction> {
     let repeat = std::iter::repeat(
         values
             .last()
-            .map(|value| Tendril::from_slice(value))
+            .map(|value| Tendril::from(value.repeat(count)))
             .unwrap(),
     );
 
@@ -4393,7 +4606,7 @@ fn paste_impl(
     let mut values = values
         .iter()
         .map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
-        .map(|value| Tendril::from(value.as_ref()))
+        .map(|value| Tendril::from(value.as_ref().repeat(count)))
         .chain(repeat);
 
     let text = doc.text();
@@ -4413,7 +4626,7 @@ fn paste_impl(
             // paste append
             (Paste::After, false) => range.to(),
         };
-        (pos, pos, Some(values.next().unwrap()))
+        (pos, pos, values.next())
     });
 
     Some(transaction)
@@ -4423,13 +4636,14 @@ fn paste_clipboard_impl(
     editor: &mut Editor,
     action: Paste,
     clipboard_type: ClipboardType,
+    count: usize,
 ) -> anyhow::Result<()> {
     let (view, doc) = current!(editor);
 
     match editor
         .clipboard_provider
         .get_contents(clipboard_type)
-        .map(|contents| paste_impl(&[contents], doc, view, action))
+        .map(|contents| paste_impl(&[contents], doc, view, action, count))
     {
         Ok(Some(transaction)) => {
             doc.apply(&transaction, view.id);
@@ -4442,22 +4656,43 @@ fn paste_clipboard_impl(
 }
 
 fn paste_clipboard_after(cx: &mut Context) {
-    let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
+    let _ = paste_clipboard_impl(
+        cx.editor,
+        Paste::After,
+        ClipboardType::Clipboard,
+        cx.count(),
+    );
 }
 
 fn paste_clipboard_before(cx: &mut Context) {
-    let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
+    let _ = paste_clipboard_impl(
+        cx.editor,
+        Paste::Before,
+        ClipboardType::Clipboard,
+        cx.count(),
+    );
 }
 
 fn paste_primary_clipboard_after(cx: &mut Context) {
-    let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
+    let _ = paste_clipboard_impl(
+        cx.editor,
+        Paste::After,
+        ClipboardType::Selection,
+        cx.count(),
+    );
 }
 
 fn paste_primary_clipboard_before(cx: &mut Context) {
-    let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
+    let _ = paste_clipboard_impl(
+        cx.editor,
+        Paste::Before,
+        ClipboardType::Selection,
+        cx.count(),
+    );
 }
 
 fn replace_with_yanked(cx: &mut Context) {
+    let count = cx.count();
     let reg_name = cx.register.unwrap_or('"');
     let (view, doc) = current!(cx.editor);
     let registers = &mut cx.editor.registers;
@@ -4467,12 +4702,12 @@ fn replace_with_yanked(cx: &mut Context) {
             let repeat = std::iter::repeat(
                 values
                     .last()
-                    .map(|value| Tendril::from_slice(value))
+                    .map(|value| Tendril::from_slice(&value.repeat(count)))
                     .unwrap(),
             );
             let mut values = values
                 .iter()
-                .map(|value| Tendril::from_slice(value))
+                .map(|value| Tendril::from_slice(&value.repeat(count)))
                 .chain(repeat);
             let selection = doc.selection(view.id);
             let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@@ -4492,6 +4727,7 @@ fn replace_with_yanked(cx: &mut Context) {
 fn replace_selections_with_clipboard_impl(
     editor: &mut Editor,
     clipboard_type: ClipboardType,
+    count: usize,
 ) -> anyhow::Result<()> {
     let (view, doc) = current!(editor);
 
@@ -4499,7 +4735,11 @@ fn replace_selections_with_clipboard_impl(
         Ok(contents) => {
             let selection = doc.selection(view.id);
             let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
-                (range.from(), range.to(), Some(contents.as_str().into()))
+                (
+                    range.from(),
+                    range.to(),
+                    Some(contents.repeat(count).as_str().into()),
+                )
             });
 
             doc.apply(&transaction, view.id);
@@ -4511,21 +4751,22 @@ fn replace_selections_with_clipboard_impl(
 }
 
 fn replace_selections_with_clipboard(cx: &mut Context) {
-    let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+    let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count());
 }
 
 fn replace_selections_with_primary_clipboard(cx: &mut Context) {
-    let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+    let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count());
 }
 
 fn paste_after(cx: &mut Context) {
+    let count = cx.count();
     let reg_name = cx.register.unwrap_or('"');
     let (view, doc) = current!(cx.editor);
     let registers = &mut cx.editor.registers;
 
     if let Some(transaction) = registers
         .read(reg_name)
-        .and_then(|values| paste_impl(values, doc, view, Paste::After))
+        .and_then(|values| paste_impl(values, doc, view, Paste::After, count))
     {
         doc.apply(&transaction, view.id);
         doc.append_changes_to_history(view.id);
@@ -4533,13 +4774,14 @@ fn paste_after(cx: &mut Context) {
 }
 
 fn paste_before(cx: &mut Context) {
+    let count = cx.count();
     let reg_name = cx.register.unwrap_or('"');
     let (view, doc) = current!(cx.editor);
     let registers = &mut cx.editor.registers;
 
     if let Some(transaction) = registers
         .read(reg_name)
-        .and_then(|values| paste_impl(values, doc, view, Paste::Before))
+        .and_then(|values| paste_impl(values, doc, view, Paste::Before, count))
     {
         doc.apply(&transaction, view.id);
         doc.append_changes_to_history(view.id);
@@ -4935,8 +5177,12 @@ fn hover(cx: &mut Context) {
                 // skip if contents empty
 
                 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
-                let popup = Popup::new(contents);
-                compositor.push(Box::new(popup));
+                let popup = Popup::new("documentation", contents);
+                if let Some(doc_popup) = compositor.find_id("documentation") {
+                    *doc_popup = popup;
+                } else {
+                    compositor.push(Box::new(popup));
+                }
             }
         },
     );
@@ -5030,7 +5276,7 @@ fn expand_selection(cx: &mut Context) {
             doc.set_selection(view.id, selection);
         }
     };
-    motion(&mut cx.editor);
+    motion(cx.editor);
     cx.editor.last_motion = Some(Motion(Box::new(motion)));
 }
 
@@ -5086,6 +5332,12 @@ fn jump_backward(cx: &mut Context) {
     };
 }
 
+fn save_selection(cx: &mut Context) {
+    push_jump(cx.editor);
+    cx.editor
+        .set_status("Selection saved to jumplist".to_owned());
+}
+
 fn rotate_view(cx: &mut Context) {
     cx.editor.focus_next()
 }
@@ -5262,7 +5514,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
                 });
                 doc.set_selection(view.id, selection);
             };
-            textobject(&mut cx.editor);
+            textobject(cx.editor);
             cx.editor.last_motion = Some(Motion(Box::new(textobject)));
         }
     })
@@ -5428,9 +5680,7 @@ fn shell_impl(
 ) -> anyhow::Result<(Tendril, bool)> {
     use std::io::Write;
     use std::process::{Command, Stdio};
-    if shell.is_empty() {
-        bail!("No shell set");
-    }
+    ensure!(!shell.is_empty(), "No shell set");
 
     let mut process = match Command::new(&shell[0])
         .args(&shell[1..])
@@ -5594,7 +5844,7 @@ fn rename_symbol(cx: &mut Context) {
             let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
             let edits = block_on(task).unwrap_or_default();
             log::debug!("Edits from LSP: {:?}", edits);
-            apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
+            apply_workspace_edit(cx.editor, offset_encoding, &edits);
         },
     );
     cx.push_layer(Box::new(prompt));
@@ -5614,16 +5864,45 @@ fn decrement(cx: &mut Context) {
 fn increment_impl(cx: &mut Context, amount: i64) {
     let (view, doc) = current!(cx.editor);
     let selection = doc.selection(view.id);
-    let text = doc.text();
+    let text = doc.text().slice(..);
 
-    let changes = selection.ranges().iter().filter_map(|range| {
-        let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
-        let new_text = incrementor.incremented_text(amount);
-        Some((
-            incrementor.range.from(),
-            incrementor.range.to(),
-            Some(new_text),
-        ))
+    let changes: Vec<_> = selection
+        .ranges()
+        .iter()
+        .filter_map(|range| {
+            let incrementor: Box<dyn Increment> =
+                if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) {
+                    Box::new(incrementor)
+                } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) {
+                    Box::new(incrementor)
+                } else {
+                    return None;
+                };
+
+            let (range, new_text) = incrementor.increment(amount);
+
+            Some((range.from(), range.to(), Some(new_text)))
+        })
+        .collect();
+
+    // Overlapping changes in a transaction will panic, so we need to find and remove them.
+    // For example, if there are cursors on each of the year, month, and day of `2021-11-29`,
+    // incrementing will give overlapping changes, with each change incrementing a different part of
+    // the date. Since these conflict with each other we remove these changes from the transaction
+    // so nothing happens.
+    let mut overlapping_indexes = HashSet::new();
+    for (i, changes) in changes.windows(2).enumerate() {
+        if changes[0].1 > changes[1].0 {
+            overlapping_indexes.insert(i);
+            overlapping_indexes.insert(i + 1);
+        }
+    }
+    let changes = changes.into_iter().enumerate().filter_map(|(i, change)| {
+        if overlapping_indexes.contains(&i) {
+            None
+        } else {
+            Some(change)
+        }
     });
 
     if changes.clone().count() > 0 {
@@ -5634,3 +5913,56 @@ fn increment_impl(cx: &mut Context, amount: i64) {
         doc.append_changes_to_history(view.id);
     }
 }
+
+fn record_macro(cx: &mut Context) {
+    if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
+        // Remove the keypress which ends the recording
+        keys.pop();
+        let s = keys
+            .into_iter()
+            .map(|key| format!("{}", key))
+            .collect::<Vec<_>>()
+            .join(" ");
+        cx.editor.registers.get_mut(reg).write(vec![s]);
+        cx.editor
+            .set_status(format!("Recorded to register {}", reg));
+    } else {
+        let reg = cx.register.take().unwrap_or('@');
+        cx.editor.macro_recording = Some((reg, Vec::new()));
+        cx.editor
+            .set_status(format!("Recording to register {}", reg));
+    }
+}
+
+fn play_macro(cx: &mut Context) {
+    let reg = cx.register.unwrap_or('@');
+    let keys = match cx
+        .editor
+        .registers
+        .get(reg)
+        .and_then(|reg| reg.read().get(0))
+        .context("Register empty")
+        .and_then(|s| {
+            s.split_whitespace()
+                .map(str::parse::<KeyEvent>)
+                .collect::<Result<Vec<_>, _>>()
+                .context("Failed to parse macro")
+        }) {
+        Ok(keys) => keys,
+        Err(e) => {
+            cx.editor.set_error(format!("{}", e));
+            return;
+        }
+    };
+    let count = cx.count();
+
+    cx.callback = Some(Box::new(
+        move |compositor: &mut Compositor, cx: &mut compositor::Context| {
+            for _ in 0..count {
+                for &key in keys.iter() {
+                    compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
+                }
+            }
+        },
+    ));
+}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 3a644750..321f56a5 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
 use crossterm::event::Event;
 use tui::buffer::Buffer as Surface;
 
-pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
+pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
 
 // --> EventResult should have a callback that takes a context with methods like .popup(),
 // .prompt() etc. That way we can abstract it from the renderer.
@@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent {
 
     /// May be used by the parent component to compute the child area.
     /// viewport is the maximum allowed area, and the child should stay within those bounds.
+    ///
+    /// The returned size might be larger than the viewport if the child is too big to fit.
+    /// In this case the parent can use the values to calculate scroll.
     fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
-        // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context
-        // that way render can use it
         None
     }
 
     fn type_name(&self) -> &'static str {
         std::any::type_name::<Self>()
     }
+
+    fn id(&self) -> Option<&'static str> {
+        None
+    }
 }
 
 use anyhow::Error;
@@ -126,12 +131,17 @@ impl Compositor {
     }
 
     pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
+        // If it is a key event and a macro is being recorded, push the key event to the recording.
+        if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
+            keys.push(key.into());
+        }
+
         // propagate events through the layers until we either find a layer that consumes it or we
         // run out of layers (event bubbling)
         for layer in self.layers.iter_mut().rev() {
             match layer.handle_event(event, cx) {
                 EventResult::Consumed(Some(callback)) => {
-                    callback(self);
+                    callback(self, cx);
                     return true;
                 }
                 EventResult::Consumed(None) => return true,
@@ -184,6 +194,14 @@ impl Compositor {
             .find(|component| component.type_name() == type_name)
             .and_then(|component| component.as_any_mut().downcast_mut())
     }
+
+    pub fn find_id<T: 'static>(&mut self, id: &'static str) -> Option<&mut T> {
+        let type_name = std::any::type_name::<T>();
+        self.layers
+            .iter_mut()
+            .find(|component| component.type_name() == type_name && component.id() == Some(id))
+            .and_then(|component| component.as_any_mut().downcast_mut())
+    }
 }
 
 // View casting, taken straight from Cursive
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 7f6d0c6b..257d5f29 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,4 +1,4 @@
-pub use crate::commands::Command;
+pub use crate::commands::MappableCommand;
 use crate::config::Config;
 use helix_core::hashmap;
 use helix_view::{document::Mode, info::Info, input::KeyEvent};
@@ -92,7 +92,7 @@ macro_rules! alt {
 #[macro_export]
 macro_rules! keymap {
     (@trie $cmd:ident) => {
-        $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
+        $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
     };
 
     (@trie
@@ -120,7 +120,7 @@ macro_rules! keymap {
                         _key,
                         keymap!(@trie $value)
                     );
-                    debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
+                    assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
                     _order.push(_key);
                 )+
             )*
@@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
 #[derive(Debug, Clone, PartialEq, Deserialize)]
 #[serde(untagged)]
 pub enum KeyTrie {
-    Leaf(Command),
-    Sequence(Vec<Command>),
+    Leaf(MappableCommand),
+    Sequence(Vec<MappableCommand>),
     Node(KeyTrieNode),
 }
 
@@ -304,9 +304,9 @@ impl KeyTrie {
 pub enum KeymapResultKind {
     /// Needs more keys to execute a command. Contains valid keys for next keystroke.
     Pending(KeyTrieNode),
-    Matched(Command),
+    Matched(MappableCommand),
     /// Matched a sequence of commands to execute.
-    MatchedSequence(Vec<Command>),
+    MatchedSequence(Vec<MappableCommand>),
     /// Key was not found in the root keymap
     NotFound,
     /// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -386,10 +386,10 @@ impl Keymap {
         };
 
         let trie = match trie_node.search(&[*first]) {
-            Some(&KeyTrie::Leaf(cmd)) => {
-                return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
+            Some(KeyTrie::Leaf(ref cmd)) => {
+                return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
             }
-            Some(&KeyTrie::Sequence(ref cmds)) => {
+            Some(KeyTrie::Sequence(ref cmds)) => {
                 return KeymapResult::new(
                     KeymapResultKind::MatchedSequence(cmds.clone()),
                     self.sticky(),
@@ -408,9 +408,9 @@ impl Keymap {
                 }
                 KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
             }
-            Some(&KeyTrie::Leaf(cmd)) => {
+            Some(&KeyTrie::Leaf(ref cmd)) => {
                 self.state.clear();
-                return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
+                return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
             }
             Some(&KeyTrie::Sequence(ref cmds)) => {
                 self.state.clear();
@@ -512,6 +512,7 @@ impl Default for Keymaps {
             "g" => { "Goto"
                 "g" => goto_file_start,
                 "e" => goto_last_line,
+                "f" => goto_file,
                 "h" => goto_line_start,
                 "l" => goto_line_end,
                 "s" => goto_first_nonwhitespace,
@@ -520,9 +521,10 @@ impl Default for Keymaps {
                 "r" => goto_reference,
                 "i" => goto_implementation,
                 "t" => goto_window_top,
-                "m" => goto_window_middle,
+                "c" => goto_window_center,
                 "b" => goto_window_bottom,
                 "a" => goto_last_accessed_file,
+                "m" => goto_last_modified_file,
                 "n" => goto_next_buffer,
                 "p" => goto_previous_buffer,
                 "." => goto_last_modification,
@@ -537,9 +539,9 @@ impl Default for Keymaps {
             "O" => open_above,
 
             "d" => delete_selection,
-            // TODO: also delete without yanking
+            "A-d" => delete_selection_noyank,
             "c" => change_selection,
-            // TODO: also change delete without yanking
+            "A-c" => change_selection_noyank,
 
             "C" => copy_selection_on_next_line,
             "A-C" => copy_selection_on_prev_line,
@@ -591,6 +593,9 @@ impl Default for Keymaps {
             // paste_all
             "P" => paste_before,
 
+            "q" => record_macro,
+            "Q" => play_macro,
+
             ">" => indent,
             "<" => unindent,
             "=" => format_selections,
@@ -622,6 +627,8 @@ impl Default for Keymaps {
                 "C-w" | "w" => rotate_view,
                 "C-s" | "s" => hsplit,
                 "C-v" | "v" => vsplit,
+                "f" => goto_file_hsplit,
+                "F" => goto_file_vsplit,
                 "C-q" | "q" => wclose,
                 "C-o" | "o" => wonly,
                 "C-h" | "h" | "left" => jump_view_left,
@@ -637,7 +644,7 @@ impl Default for Keymaps {
 
             "tab" => jump_forward, // tab == <C-i>
             "C-o" => jump_backward,
-            // "C-s" => save_selection,
+            "C-s" => save_selection,
 
             "space" => { "Space"
                 "f" => file_picker,
@@ -650,6 +657,8 @@ impl Default for Keymaps {
                     "C-w" | "w" => rotate_view,
                     "C-s" | "s" => hsplit,
                     "C-v" | "v" => vsplit,
+                    "f" => goto_file_hsplit,
+                    "F" => goto_file_vsplit,
                     "C-q" | "q" => wclose,
                     "C-o" | "o" => wonly,
                     "C-h" | "h" | "left" => jump_view_left,
@@ -827,36 +836,36 @@ mod tests {
         let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
         assert_eq!(
             keymap.get(key!('i')).kind,
-            KeymapResultKind::Matched(Command::normal_mode),
+            KeymapResultKind::Matched(MappableCommand::normal_mode),
             "Leaf should replace leaf"
         );
         assert_eq!(
             keymap.get(key!('无')).kind,
-            KeymapResultKind::Matched(Command::insert_mode),
+            KeymapResultKind::Matched(MappableCommand::insert_mode),
             "New leaf should be present in merged keymap"
         );
         // Assumes that z is a node in the default keymap
         assert_eq!(
             keymap.get(key!('z')).kind,
-            KeymapResultKind::Matched(Command::jump_backward),
+            KeymapResultKind::Matched(MappableCommand::jump_backward),
             "Leaf should replace node"
         );
         // Assumes that `g` is a node in default keymap
         assert_eq!(
             keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
-            &KeyTrie::Leaf(Command::goto_line_end),
+            &KeyTrie::Leaf(MappableCommand::goto_line_end),
             "Leaf should be present in merged subnode"
         );
         // Assumes that `gg` is in default keymap
         assert_eq!(
             keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
-            &KeyTrie::Leaf(Command::delete_char_forward),
+            &KeyTrie::Leaf(MappableCommand::delete_char_forward),
             "Leaf should replace old leaf in merged subnode"
         );
         // Assumes that `ge` is in default keymap
         assert_eq!(
             keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
-            &KeyTrie::Leaf(Command::goto_last_line),
+            &KeyTrie::Leaf(MappableCommand::goto_last_line),
             "Old leaves in subnode should be present in merged node"
         );
 
@@ -890,7 +899,7 @@ mod tests {
                 .root()
                 .search(&[key!(' '), key!('s'), key!('v')])
                 .unwrap(),
-            &KeyTrie::Leaf(Command::vsplit),
+            &KeyTrie::Leaf(MappableCommand::vsplit),
             "Leaf should be present in merged subnode"
         );
         // Make sure an order was set during merge
diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs
index f5e3a8cd..58cb139c 100644
--- a/helix-term/src/lib.rs
+++ b/helix-term/src/lib.rs
@@ -9,3 +9,14 @@ pub mod config;
 pub mod job;
 pub mod keymap;
 pub mod ui;
+
+#[cfg(not(windows))]
+fn true_color() -> bool {
+    std::env::var("COLORTERM")
+        .map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
+        .unwrap_or(false)
+}
+#[cfg(windows)]
+fn true_color() -> bool {
+    true
+}
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index dd782d29..a55201ff 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -168,7 +168,7 @@ impl Completion {
                 }
             };
         });
-        let popup = Popup::new(menu);
+        let popup = Popup::new("completion", menu);
         let mut completion = Self {
             popup,
             start_offset,
@@ -328,8 +328,8 @@ impl Component for Completion {
                 let y = popup_y;
 
                 if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
-                    width = rel_width;
-                    height = rel_height;
+                    width = rel_width.min(width);
+                    height = rel_height.min(height);
                 }
                 Rect::new(x, y, width, height)
             } else {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index e8f8fd9b..7d57e581 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -17,7 +17,6 @@ use helix_core::{
 };
 use helix_view::{
     document::{Mode, SCRATCH_BUFFER_NAME},
-    editor::LineNumber,
     graphics::{CursorKind, Modifier, Rect, Style},
     info::Info,
     input::KeyEvent,
@@ -32,7 +31,7 @@ use tui::buffer::Buffer as Surface;
 pub struct EditorView {
     keymaps: Keymaps,
     on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
-    last_insert: (commands::Command, Vec<KeyEvent>),
+    last_insert: (commands::MappableCommand, Vec<KeyEvent>),
     pub(crate) completion: Option<Completion>,
     spinners: ProgressSpinners,
     autoinfo: Option<Info>,
@@ -49,7 +48,7 @@ impl EditorView {
         Self {
             keymaps,
             on_next_key: None,
-            last_insert: (commands::Command::normal_mode, Vec::new()),
+            last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
             completion: None,
             spinners: ProgressSpinners::default(),
             autoinfo: None,
@@ -310,17 +309,16 @@ impl EditorView {
 
                     use helix_core::graphemes::{grapheme_width, RopeGraphemes};
 
-                    let style = spans.iter().fold(text_style, |acc, span| {
-                        let style = theme.get(theme.scopes()[span.0].as_str());
-                        acc.patch(style)
-                    });
-
                     for grapheme in RopeGraphemes::new(text) {
                         let out_of_bounds = visual_x < offset.col as u16
                             || visual_x >= viewport.width + offset.col as u16;
 
                         if LineEnding::from_rope_slice(&grapheme).is_some() {
                             if !out_of_bounds {
+                                let style = spans.iter().fold(text_style, |acc, span| {
+                                    acc.patch(theme.highlight(span.0))
+                                });
+
                                 // we still want to render an empty cell with the style
                                 surface.set_string(
                                     viewport.x + visual_x - offset.col as u16,
@@ -351,6 +349,10 @@ impl EditorView {
                             };
 
                             if !out_of_bounds {
+                                let style = spans.iter().fold(text_style, |acc, span| {
+                                    acc.patch(theme.highlight(span.0))
+                                });
+
                                 // if we're offscreen just keep going until we hit a new line
                                 surface.set_string(
                                     viewport.x + visual_x - offset.col as u16,
@@ -417,22 +419,6 @@ impl EditorView {
         let text = doc.text().slice(..);
         let last_line = view.last_line(doc);
 
-        let linenr = theme.get("ui.linenr");
-        let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
-
-        let warning = theme.get("warning");
-        let error = theme.get("error");
-        let info = theme.get("info");
-        let hint = theme.get("hint");
-
-        // Whether to draw the line number for the last line of the
-        // document or not.  We only draw it if it's not an empty line.
-        let draw_last = text.line_to_byte(last_line) < text.len_bytes();
-
-        let current_line = doc
-            .text()
-            .char_to_line(doc.selection(view.id).primary().cursor(text));
-
         // it's used inside an iterator so the collect isn't needless:
         // https://github.com/rust-lang/rust-clippy/issues/6164
         #[allow(clippy::needless_collect)]
@@ -442,51 +428,31 @@ impl EditorView {
             .map(|range| range.cursor_line(text))
             .collect();
 
-        for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
-            use helix_core::diagnostic::Severity;
-            if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
-                surface.set_stringn(
-                    viewport.x,
-                    viewport.y + i as u16,
-                    "●",
-                    1,
-                    match diagnostic.severity {
-                        Some(Severity::Error) => error,
-                        Some(Severity::Warning) | None => warning,
-                        Some(Severity::Info) => info,
-                        Some(Severity::Hint) => hint,
-                    },
-                );
+        let mut offset = 0;
+
+        let gutter_style = theme.get("ui.gutter");
+
+        // avoid lots of small allocations by reusing a text buffer for each line
+        let mut text = String::with_capacity(8);
+
+        for (constructor, width) in view.gutters() {
+            let gutter = constructor(doc, view, theme, config, is_focused, *width);
+            text.reserve(*width); // ensure there's enough space for the gutter
+            for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
+                let selected = cursors.contains(&line);
+
+                if let Some(style) = gutter(line, selected, &mut text) {
+                    surface.set_stringn(
+                        viewport.x + offset,
+                        viewport.y + i as u16,
+                        &text,
+                        *width,
+                        gutter_style.patch(style),
+                    );
+                }
+                text.clear();
             }
-
-            let selected = cursors.contains(&line);
-
-            let text = if line == last_line && !draw_last {
-                "    ~".into()
-            } else {
-                let line = match config.line_number {
-                    LineNumber::Absolute => line + 1,
-                    LineNumber::Relative => {
-                        if current_line == line {
-                            line + 1
-                        } else {
-                            abs_diff(current_line, line)
-                        }
-                    }
-                };
-                format!("{:>5}", line)
-            };
-            surface.set_stringn(
-                viewport.x + 1,
-                viewport.y + i as u16,
-                text,
-                5,
-                if selected && is_focused {
-                    linenr_select
-                } else {
-                    linenr
-                },
-            );
+            offset += *width as u16;
         }
     }
 
@@ -916,7 +882,7 @@ impl EditorView {
                     return EventResult::Ignored;
                 }
 
-                commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
+                commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
 
                 EventResult::Consumed(None)
             }
@@ -934,7 +900,8 @@ impl EditorView {
                 }
 
                 if modifiers == crossterm::event::KeyModifiers::ALT {
-                    commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
+                    commands::MappableCommand::replace_selections_with_primary_clipboard
+                        .execute(cxt);
 
                     return EventResult::Consumed(None);
                 }
@@ -948,7 +915,7 @@ impl EditorView {
                     let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
                     doc.set_selection(view_id, Selection::point(pos));
                     editor.tree.focus = view_id;
-                    commands::Command::paste_primary_clipboard_before.execute(cxt);
+                    commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
                     return EventResult::Consumed(None);
                 }
 
@@ -963,7 +930,7 @@ impl EditorView {
 impl Component for EditorView {
     fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
         let mut cxt = commands::Context {
-            editor: &mut cx.editor,
+            editor: cx.editor,
             count: None,
             register: None,
             callback: None,
@@ -1140,13 +1107,31 @@ impl Component for EditorView {
                     disp.push_str(&s);
                 }
             }
+            let style = cx.editor.theme.get("ui.text");
+            let macro_width = if cx.editor.macro_recording.is_some() {
+                3
+            } else {
+                0
+            };
             surface.set_string(
-                area.x + area.width.saturating_sub(key_width),
+                area.x + area.width.saturating_sub(key_width + macro_width),
                 area.y + area.height.saturating_sub(1),
                 disp.get(disp.len().saturating_sub(key_width as usize)..)
                     .unwrap_or(&disp),
-                cx.editor.theme.get("ui.text"),
+                style,
             );
+            if let Some((reg, _)) = cx.editor.macro_recording {
+                let disp = format!("[{}]", reg);
+                let style = style
+                    .fg(helix_view::graphics::Color::Yellow)
+                    .add_modifier(Modifier::BOLD);
+                surface.set_string(
+                    area.x + area.width.saturating_sub(3),
+                    area.y + area.height.saturating_sub(1),
+                    &disp,
+                    style,
+                );
+            }
         }
 
         if let Some(completion) = self.completion.as_mut() {
@@ -1172,12 +1157,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
         key.modifiers.remove(KeyModifiers::SHIFT)
     }
 }
-
-#[inline]
-const fn abs_diff(a: usize, b: usize) -> usize {
-    if a > b {
-        a - b
-    } else {
-        b - a
-    }
-}
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 649703b5..46657fb9 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -228,6 +228,7 @@ impl Component for Markdown {
             return None;
         }
         let contents = parse(&self.contents, None, &self.config_loader);
+        // TODO: account for tab width
         let max_text_width = (viewport.0 - padding).min(120);
         let mut text_width = 0;
         let mut height = padding;
@@ -240,11 +241,6 @@ impl Component for Markdown {
             } else if content_width > text_width {
                 text_width = content_width;
             }
-
-            if height >= viewport.1 {
-                height = viewport.1;
-                break;
-            }
         }
 
         Some((text_width + padding, height))
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index e891c149..69053db3 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
             _ => return EventResult::Ignored,
         };
 
-        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
             // remove the layer
             compositor.pop();
         })));
@@ -202,7 +202,7 @@ impl<T: Item + 'static> Component for Menu<T> {
                 return close_fn;
             }
             // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
-            shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+            shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
                 self.move_up();
                 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
                 return EventResult::Consumed(None);
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index cdf42311..f57e2e2b 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -186,6 +186,7 @@ pub mod completers {
             &helix_core::config_dir().join("themes"),
         ));
         names.push("default".into());
+        names.push("base16_default".into());
 
         let mut names: Vec<_> = names
             .into_iter()
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 6b1c5832..1ef94df0 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -46,7 +46,7 @@ pub struct FilePicker<T> {
 }
 
 pub enum CachedPreview {
-    Document(Document),
+    Document(Box<Document>),
     Binary,
     LargeFile,
     NotFound,
@@ -140,7 +140,7 @@ impl<T> FilePicker<T> {
                     _ => {
                         // TODO: enable syntax highlighting; blocked by async rendering
                         Document::open(path, None, Some(&editor.theme), None)
-                            .map(CachedPreview::Document)
+                            .map(|doc| CachedPreview::Document(Box::new(doc)))
                             .unwrap_or(CachedPreview::NotFound)
                     }
                 },
@@ -404,13 +404,13 @@ impl<T: 'static> Component for Picker<T> {
             _ => return EventResult::Ignored,
         };
 
-        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
             // remove the layer
             compositor.last_picker = compositor.pop();
         })));
 
         match key_event.into() {
-            shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+            shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
                 self.move_up();
             }
             key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
@@ -421,19 +421,19 @@ impl<T: 'static> Component for Picker<T> {
             }
             key!(Enter) => {
                 if let Some(option) = self.selection() {
-                    (self.callback_fn)(&mut cx.editor, option, Action::Replace);
+                    (self.callback_fn)(cx.editor, option, Action::Replace);
                 }
                 return close_fn;
             }
             ctrl!('s') => {
                 if let Some(option) = self.selection() {
-                    (self.callback_fn)(&mut cx.editor, option, Action::HorizontalSplit);
+                    (self.callback_fn)(cx.editor, option, Action::HorizontalSplit);
                 }
                 return close_fn;
             }
             ctrl!('v') => {
                 if let Some(option) = self.selection() {
-                    (self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit);
+                    (self.callback_fn)(cx.editor, option, Action::VerticalSplit);
                 }
                 return close_fn;
             }
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 8f7921a1..bf7510a2 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -15,16 +15,20 @@ pub struct Popup<T: Component> {
     contents: T,
     position: Option<Position>,
     size: (u16, u16),
+    child_size: (u16, u16),
     scroll: usize,
+    id: &'static str,
 }
 
 impl<T: Component> Popup<T> {
-    pub fn new(contents: T) -> Self {
+    pub fn new(id: &'static str, contents: T) -> Self {
         Self {
             contents,
             position: None,
             size: (0, 0),
+            child_size: (0, 0),
             scroll: 0,
+            id,
         }
     }
 
@@ -68,6 +72,9 @@ impl<T: Component> Popup<T> {
     pub fn scroll(&mut self, offset: usize, direction: bool) {
         if direction {
             self.scroll += offset;
+
+            let max_offset = self.child_size.1.saturating_sub(self.size.1);
+            self.scroll = (self.scroll + offset).min(max_offset as usize);
         } else {
             self.scroll = self.scroll.saturating_sub(offset);
         }
@@ -93,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
             _ => return EventResult::Ignored,
         };
 
-        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
             // remove the layer
             compositor.pop();
         })));
@@ -115,13 +122,21 @@ impl<T: Component> Component for Popup<T> {
         // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
     }
 
-    fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
+    fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+        let max_width = 120.min(viewport.0);
+        let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
         let (width, height) = self
             .contents
-            .required_size((120, 26)) // max width, max height
+            .required_size((max_width, max_height))
             .expect("Component needs required_size implemented in order to be embedded in a popup");
 
-        self.size = (width, height);
+        self.child_size = (width, height);
+        self.size = (width.min(max_width), height.min(max_height));
+
+        // re-clamp scroll offset
+        let max_offset = self.child_size.1.saturating_sub(self.size.1);
+        self.scroll = self.scroll.min(max_offset as usize);
 
         Some(self.size)
     }
@@ -143,4 +158,8 @@ impl<T: Component> Component for Popup<T> {
 
         self.contents.render(area, surface, cx);
     }
+
+    fn id(&self) -> Option<&'static str> {
+        Some(self.id)
+    }
 }
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index e90b0772..07e1b33c 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -426,7 +426,7 @@ impl Component for Prompt {
             _ => return EventResult::Ignored,
         };
 
-        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+        let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
             // remove the layer
             compositor.pop();
         })));
@@ -505,7 +505,7 @@ impl Component for Prompt {
                 self.change_completion_selection(CompletionDirection::Forward);
                 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
             }
-            shift!(BackTab) => {
+            shift!(Tab) => {
                 self.change_completion_selection(CompletionDirection::Backward);
                 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
             }
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index f480bc2f..c49a0200 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -102,7 +102,7 @@ impl Default for Cell {
 /// buf.get_mut(5, 0).set_char('x');
 /// assert_eq!(buf.get(5, 0).symbol, "x");
 /// ```
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
 pub struct Buffer {
     /// The area represented by this buffer
     pub area: Rect,
@@ -111,15 +111,6 @@ pub struct Buffer {
     pub content: Vec<Cell>,
 }
 
-impl Default for Buffer {
-    fn default() -> Buffer {
-        Buffer {
-            area: Default::default(),
-            content: Vec::new(),
-        }
-    }
-}
-
 impl Buffer {
     /// Returns a Buffer with all cells set to the default one
     pub fn empty(area: Rect) -> Buffer {
diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs
index b8e52479..8a974ddb 100644
--- a/helix-tui/src/text.rs
+++ b/helix-tui/src/text.rs
@@ -195,15 +195,9 @@ impl<'a> From<&'a str> for Span<'a> {
 }
 
 /// A string composed of clusters of graphemes, each with their own style.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
 pub struct Spans<'a>(pub Vec<Span<'a>>);
 
-impl<'a> Default for Spans<'a> {
-    fn default() -> Spans<'a> {
-        Spans(Vec::new())
-    }
-}
-
 impl<'a> Spans<'a> {
     /// Returns the width of the underlying string.
     ///
@@ -280,17 +274,11 @@ impl<'a> From<Spans<'a>> for String {
 /// text.extend(Text::styled("Some more lines\nnow with more style!", style));
 /// assert_eq!(6, text.height());
 /// ```
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
 pub struct Text<'a> {
     pub lines: Vec<Spans<'a>>,
 }
 
-impl<'a> Default for Text<'a> {
-    fn default() -> Text<'a> {
-        Text { lines: Vec::new() }
-    }
-}
-
 impl<'a> Text<'a> {
     /// Create some text (potentially multiple lines) with no style.
     ///
diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs
index d7caa0b0..6aee5988 100644
--- a/helix-tui/src/widgets/table.rs
+++ b/helix-tui/src/widgets/table.rs
@@ -363,21 +363,12 @@ impl<'a> Table<'a> {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Default, Clone)]
 pub struct TableState {
     pub offset: usize,
     pub selected: Option<usize>,
 }
 
-impl Default for TableState {
-    fn default() -> TableState {
-        TableState {
-            offset: 0,
-            selected: None,
-        }
-    }
-}
-
 impl TableState {
     pub fn selected(&self) -> Option<usize> {
         self.selected
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 01975452..a0315bed 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -104,6 +104,7 @@ pub struct Document {
 
     last_saved_revision: usize,
     version: i32, // should be usize?
+    pub(crate) modified_since_accessed: bool,
 
     diagnostics: Vec<Diagnostic>,
     language_server: Option<Arc<helix_lsp::Client>>,
@@ -127,6 +128,7 @@ impl fmt::Debug for Document {
             // .field("history", &self.history)
             .field("last_saved_revision", &self.last_saved_revision)
             .field("version", &self.version)
+            .field("modified_since_accessed", &self.modified_since_accessed)
             .field("diagnostics", &self.diagnostics)
             // .field("language_server", &self.language_server)
             .finish()
@@ -344,6 +346,7 @@ impl Document {
             history: Cell::new(History::default()),
             savepoint: None,
             last_saved_revision: 0,
+            modified_since_accessed: false,
             language_server: None,
         }
     }
@@ -639,6 +642,9 @@ impl Document {
                     selection.clone().ensure_invariants(self.text.slice(..)),
                 );
             }
+
+            // set modified since accessed
+            self.modified_since_accessed = true;
         }
 
         if !transaction.changes().is_empty() {
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index a121a836..fff4792d 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -2,6 +2,7 @@ use crate::{
     clipboard::{get_clipboard_provider, ClipboardProvider},
     document::{Mode, SCRATCH_BUFFER_NAME},
     graphics::{CursorKind, Rect},
+    input::KeyEvent,
     theme::{self, Theme},
     tree::{self, Tree},
     Document, DocumentId, View, ViewId,
@@ -11,6 +12,7 @@ use futures_util::future;
 use std::{
     collections::{BTreeMap, HashMap},
     io::stdin,
+    num::NonZeroUsize,
     path::{Path, PathBuf},
     pin::Pin,
     sync::Arc,
@@ -18,7 +20,7 @@ use std::{
 
 use tokio::time::{sleep, Duration, Instant, Sleep};
 
-use anyhow::Error;
+use anyhow::{bail, Error};
 
 pub use helix_core::diagnostic::Severity;
 pub use helix_core::register::Registers;
@@ -105,6 +107,8 @@ pub struct Config {
     pub file_picker: FilePickerConfig,
     /// Shape for cursor in each mode
     pub cursor_shape: CursorShapeConfig,
+    /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
+    pub true_color: bool,
 }
 
 // Cursor shape is read and used on every rendered frame and so needs
@@ -141,7 +145,7 @@ impl Default for CursorShapeConfig {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub enum LineNumber {
     /// Show absolute line number
@@ -171,6 +175,7 @@ impl Default for Config {
             auto_info: true,
             file_picker: FilePickerConfig::default(),
             cursor_shape: CursorShapeConfig::default(),
+            true_color: false,
         }
     }
 }
@@ -190,11 +195,12 @@ impl std::fmt::Debug for Motion {
 #[derive(Debug)]
 pub struct Editor {
     pub tree: Tree,
-    pub next_document_id: usize,
+    pub next_document_id: DocumentId,
     pub documents: BTreeMap<DocumentId, Document>,
     pub count: Option<std::num::NonZeroUsize>,
     pub selected_register: Option<char>,
     pub registers: Registers,
+    pub macro_recording: Option<(char, Vec<KeyEvent>)>,
     pub theme: Theme,
     pub language_servers: helix_lsp::Registry,
     pub clipboard_provider: Box<dyn ClipboardProvider>,
@@ -223,8 +229,8 @@ pub enum Action {
 impl Editor {
     pub fn new(
         mut area: Rect,
-        themes: Arc<theme::Loader>,
-        config_loader: Arc<syntax::Loader>,
+        theme_loader: Arc<theme::Loader>,
+        syn_loader: Arc<syntax::Loader>,
         config: Config,
     ) -> Self {
         let language_servers = helix_lsp::Registry::new();
@@ -234,14 +240,15 @@ impl Editor {
 
         Self {
             tree: Tree::new(area),
-            next_document_id: 0,
+            next_document_id: DocumentId::default(),
             documents: BTreeMap::new(),
             count: None,
             selected_register: None,
-            theme: themes.default(),
+            macro_recording: None,
+            theme: theme_loader.default(),
             language_servers,
-            syn_loader: config_loader,
-            theme_loader: themes,
+            syn_loader,
+            theme_loader,
             registers: Registers::default(),
             clipboard_provider: get_clipboard_provider(),
             status_msg: None,
@@ -297,14 +304,51 @@ impl Editor {
         self._refresh();
     }
 
-    pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
-        use anyhow::Context;
-        let theme = self
-            .theme_loader
-            .load(theme.as_ref())
-            .with_context(|| format!("failed setting theme `{}`", theme))?;
-        self.set_theme(theme);
-        Ok(())
+    /// Refreshes the language server for a given document
+    pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
+        let doc = self.documents.get_mut(&doc_id)?;
+        doc.detect_language(Some(&self.theme), &self.syn_loader);
+        Self::launch_language_server(&mut self.language_servers, doc)
+    }
+
+    /// Launch a language server for a given document
+    fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
+        // try to find a language server based on the language name
+        let language_server = doc.language.as_ref().and_then(|language| {
+            ls.get(language)
+                .map_err(|e| {
+                    log::error!(
+                        "Failed to initialize the LSP for `{}` {{ {} }}",
+                        language.scope(),
+                        e
+                    )
+                })
+                .ok()
+        });
+        if let Some(language_server) = language_server {
+            // only spawn a new lang server if the servers aren't the same
+            if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
+                if let Some(language_server) = doc.language_server() {
+                    tokio::spawn(language_server.text_document_did_close(doc.identifier()));
+                }
+                let language_id = doc
+                    .language()
+                    .and_then(|s| s.split('.').last()) // source.rust
+                    .map(ToOwned::to_owned)
+                    .unwrap_or_default();
+
+                // TODO: this now races with on_init code if the init happens too quickly
+                tokio::spawn(language_server.text_document_did_open(
+                    doc.url().unwrap(),
+                    doc.version(),
+                    doc.text(),
+                    language_id,
+                ));
+
+                doc.set_language_server(Some(language_server));
+            }
+        }
+        Some(())
     }
 
     fn _refresh(&mut self) {
@@ -358,7 +402,8 @@ impl Editor {
                         .tree
                         .traverse()
                         .any(|(_, v)| v.doc == doc.id && v.id != view.id);
-                let view = view_mut!(self);
+
+                let (view, doc) = current!(self);
                 if remove_empty_scratch {
                     // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
                     // borrow, invalidating direct access to `doc.id`.
@@ -367,7 +412,16 @@ impl Editor {
                 } else {
                     let jump = (view.doc, doc.selection(view.id).clone());
                     view.jumps.push(jump);
-                    view.last_accessed_doc = Some(view.doc);
+                    // Set last accessed doc if it is a different document
+                    if doc.id != id {
+                        view.last_accessed_doc = Some(view.doc);
+                        // Set last modified doc if modified and last modified doc is different
+                        if std::mem::take(&mut doc.modified_since_accessed)
+                            && view.last_modified_docs[0] != Some(id)
+                        {
+                            view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
+                        }
+                    }
                 }
 
                 let view_id = view.id;
@@ -377,23 +431,22 @@ impl Editor {
             }
             Action::Load => {
                 let view_id = view!(self).id;
-                if let Some(doc) = self.document_mut(id) {
-                    if doc.selections().is_empty() {
-                        doc.selections.insert(view_id, Selection::point(0));
-                    }
+                let doc = self.documents.get_mut(&id).unwrap();
+                if doc.selections().is_empty() {
+                    doc.selections.insert(view_id, Selection::point(0));
                 }
                 return;
             }
-            Action::HorizontalSplit => {
+            Action::HorizontalSplit | Action::VerticalSplit => {
                 let view = View::new(id);
-                let view_id = self.tree.split(view, Layout::Horizontal);
-                // initialize selection for view
-                let doc = self.documents.get_mut(&id).unwrap();
-                doc.selections.insert(view_id, Selection::point(0));
-            }
-            Action::VerticalSplit => {
-                let view = View::new(id);
-                let view_id = self.tree.split(view, Layout::Vertical);
+                let view_id = self.tree.split(
+                    view,
+                    match action {
+                        Action::HorizontalSplit => Layout::Horizontal,
+                        Action::VerticalSplit => Layout::Vertical,
+                        _ => unreachable!(),
+                    },
+                );
                 // initialize selection for view
                 let doc = self.documents.get_mut(&id).unwrap();
                 doc.selections.insert(view_id, Selection::point(0));
@@ -403,16 +456,19 @@ impl Editor {
         self._refresh();
     }
 
-    fn new_document(&mut self, mut document: Document) -> DocumentId {
-        let id = DocumentId(self.next_document_id);
-        self.next_document_id += 1;
-        document.id = id;
-        self.documents.insert(id, document);
+    /// Generate an id for a new document and register it.
+    fn new_document(&mut self, mut doc: Document) -> DocumentId {
+        let id = self.next_document_id;
+        // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
+        self.next_document_id =
+            DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
+        doc.id = id;
+        self.documents.insert(id, doc);
         id
     }
 
-    fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
-        let id = self.new_document(document);
+    fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
+        let id = self.new_document(doc);
         self.switch(id, action);
         id
     }
@@ -428,54 +484,16 @@ impl Editor {
 
     pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
         let path = helix_core::path::get_canonicalized_path(&path)?;
-
-        let id = self
-            .documents()
-            .find(|doc| doc.path() == Some(&path))
-            .map(|doc| doc.id);
+        let id = self.document_by_path(&path).map(|doc| doc.id);
 
         let id = if let Some(id) = id {
             id
         } else {
             let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
 
-            // 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)
-                    .map_err(|e| {
-                        log::error!(
-                            "Failed to initialize the LSP for `{}` {{ {} }}",
-                            language.scope(),
-                            e
-                        )
-                    })
-                    .ok()
-            });
+            let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
 
-            if let Some(language_server) = language_server {
-                let language_id = doc
-                    .language()
-                    .and_then(|s| s.split('.').last()) // source.rust
-                    .map(ToOwned::to_owned)
-                    .unwrap_or_default();
-
-                // TODO: this now races with on_init code if the init happens too quickly
-                tokio::spawn(language_server.text_document_did_open(
-                    doc.url().unwrap(),
-                    doc.version(),
-                    doc.text(),
-                    language_id,
-                ));
-
-                doc.set_language_server(Some(language_server));
-            }
-
-            let id = DocumentId(self.next_document_id);
-            self.next_document_id += 1;
-            doc.id = id;
-            self.documents.insert(id, doc);
-            id
+            self.new_document(doc)
         };
 
         self.switch(id, action);
@@ -498,11 +516,11 @@ impl Editor {
     pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
         let doc = match self.documents.get(&doc_id) {
             Some(doc) => doc,
-            None => anyhow::bail!("document does not exist"),
+            None => bail!("document does not exist"),
         };
 
         if !force && doc.is_modified() {
-            anyhow::bail!(
+            bail!(
                 "buffer {:?} is modified",
                 doc.relative_path()
                     .map(|path| path.to_string_lossy().to_string())
@@ -535,7 +553,7 @@ impl Editor {
         // If the document we removed was visible in all views, we will have no more views. We don't
         // want to close the editor just for a simple buffer close, so we need to create a new view
         // containing either an existing document, or a brand new document.
-        if self.tree.views().peekable().peek().is_none() {
+        if self.tree.views().next().is_none() {
             let doc_id = self
                 .documents
                 .iter()
@@ -620,8 +638,7 @@ impl Editor {
     }
 
     pub fn cursor(&self) -> (Option<Position>, CursorKind) {
-        let view = view!(self);
-        let doc = &self.documents[&view.doc];
+        let (view, doc) = current_ref!(self);
         let cursor = doc
             .selection(view.id)
             .primary()
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index acdaa696..892aa646 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -33,7 +33,7 @@ pub struct Margin {
 
 /// A simple rectangle used in the computation of the layout and to give widgets an hint about the
 /// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
 pub struct Rect {
     pub x: u16,
     pub y: u16,
@@ -41,17 +41,6 @@ pub struct Rect {
     pub height: u16,
 }
 
-impl Default for Rect {
-    fn default() -> Rect {
-        Rect {
-            x: 0,
-            y: 0,
-            width: 0,
-            height: 0,
-        }
-    }
-}
-
 impl Rect {
     /// Creates a new rect, with width and height limited to keep the area under max u16.
     /// If clipped, aspect ratio will be preserved.
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
new file mode 100644
index 00000000..af016c56
--- /dev/null
+++ b/helix-view/src/gutter.rs
@@ -0,0 +1,96 @@
+use std::fmt::Write;
+
+use crate::{editor::Config, graphics::Style, Document, Theme, View};
+
+pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
+pub type Gutter =
+    for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
+
+pub fn diagnostic<'doc>(
+    doc: &'doc Document,
+    _view: &View,
+    theme: &Theme,
+    _config: &Config,
+    _is_focused: bool,
+    _width: usize,
+) -> GutterFn<'doc> {
+    let warning = theme.get("warning");
+    let error = theme.get("error");
+    let info = theme.get("info");
+    let hint = theme.get("hint");
+    let diagnostics = doc.diagnostics();
+
+    Box::new(move |line: usize, _selected: bool, out: &mut String| {
+        use helix_core::diagnostic::Severity;
+        if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
+            let diagnostic = &diagnostics[index];
+            write!(out, "●").unwrap();
+            return Some(match diagnostic.severity {
+                Some(Severity::Error) => error,
+                Some(Severity::Warning) | None => warning,
+                Some(Severity::Info) => info,
+                Some(Severity::Hint) => hint,
+            });
+        }
+        None
+    })
+}
+
+pub fn line_number<'doc>(
+    doc: &'doc Document,
+    view: &View,
+    theme: &Theme,
+    config: &Config,
+    is_focused: bool,
+    width: usize,
+) -> GutterFn<'doc> {
+    let text = doc.text().slice(..);
+    let last_line = view.last_line(doc);
+    // Whether to draw the line number for the last line of the
+    // document or not.  We only draw it if it's not an empty line.
+    let draw_last = text.line_to_byte(last_line) < text.len_bytes();
+
+    let linenr = theme.get("ui.linenr");
+    let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
+
+    let current_line = doc
+        .text()
+        .char_to_line(doc.selection(view.id).primary().cursor(text));
+
+    let config = config.line_number;
+
+    Box::new(move |line: usize, selected: bool, out: &mut String| {
+        if line == last_line && !draw_last {
+            write!(out, "{:>1$}", '~', width).unwrap();
+            Some(linenr)
+        } else {
+            use crate::editor::LineNumber;
+            let line = match config {
+                LineNumber::Absolute => line + 1,
+                LineNumber::Relative => {
+                    if current_line == line {
+                        line + 1
+                    } else {
+                        abs_diff(current_line, line)
+                    }
+                }
+            };
+            let style = if selected && is_focused {
+                linenr_select
+            } else {
+                linenr
+            };
+            write!(out, "{:>1$}", line, width).unwrap();
+            Some(style)
+        }
+    })
+}
+
+#[inline(always)]
+const fn abs_diff(a: usize, b: usize) -> usize {
+    if a > b {
+        a - b
+    } else {
+        b - a
+    }
+}
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index 580204cc..92caa517 100644
--- a/helix-view/src/input.rs
+++ b/helix-view/src/input.rs
@@ -36,7 +36,6 @@ pub(crate) mod keys {
     pub(crate) const PAGEUP: &str = "pageup";
     pub(crate) const PAGEDOWN: &str = "pagedown";
     pub(crate) const TAB: &str = "tab";
-    pub(crate) const BACKTAB: &str = "backtab";
     pub(crate) const DELETE: &str = "del";
     pub(crate) const INSERT: &str = "ins";
     pub(crate) const NULL: &str = "null";
@@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
             KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
             KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
             KeyCode::Tab => f.write_str(keys::TAB)?,
-            KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
             KeyCode::Delete => f.write_str(keys::DELETE)?,
             KeyCode::Insert => f.write_str(keys::INSERT)?,
             KeyCode::Null => f.write_str(keys::NULL)?,
@@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
             KeyCode::PageUp => keys::PAGEUP.len(),
             KeyCode::PageDown => keys::PAGEDOWN.len(),
             KeyCode::Tab => keys::TAB.len(),
-            KeyCode::BackTab => keys::BACKTAB.len(),
             KeyCode::Delete => keys::DELETE.len(),
             KeyCode::Insert => keys::INSERT.len(),
             KeyCode::Null => keys::NULL.len(),
@@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
             keys::PAGEUP => KeyCode::PageUp,
             keys::PAGEDOWN => KeyCode::PageDown,
             keys::TAB => KeyCode::Tab,
-            keys::BACKTAB => KeyCode::BackTab,
             keys::DELETE => KeyCode::Delete,
             keys::INSERT => KeyCode::Insert,
             keys::NULL => KeyCode::Null,
@@ -220,12 +216,40 @@ impl<'de> Deserialize<'de> for KeyEvent {
 
 #[cfg(feature = "term")]
 impl From<crossterm::event::KeyEvent> for KeyEvent {
-    fn from(
-        crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
-    ) -> KeyEvent {
-        KeyEvent {
-            code: code.into(),
-            modifiers: modifiers.into(),
+    fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
+        if code == crossterm::event::KeyCode::BackTab {
+            // special case for BackTab -> Shift-Tab
+            let mut modifiers: KeyModifiers = modifiers.into();
+            modifiers.insert(KeyModifiers::SHIFT);
+            Self {
+                code: KeyCode::Tab,
+                modifiers,
+            }
+        } else {
+            Self {
+                code: code.into(),
+                modifiers: modifiers.into(),
+            }
+        }
+    }
+}
+
+#[cfg(feature = "term")]
+impl From<KeyEvent> for crossterm::event::KeyEvent {
+    fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
+        if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
+            // special case for Shift-Tab -> BackTab
+            let mut modifiers = modifiers;
+            modifiers.remove(KeyModifiers::SHIFT);
+            crossterm::event::KeyEvent {
+                code: crossterm::event::KeyCode::BackTab,
+                modifiers: modifiers.into(),
+            }
+        } else {
+            crossterm::event::KeyEvent {
+                code: code.into(),
+                modifiers: modifiers.into(),
+            }
         }
     }
 }
diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs
index 810aa063..f1717209 100644
--- a/helix-view/src/keyboard.rs
+++ b/helix-view/src/keyboard.rs
@@ -79,8 +79,6 @@ pub enum KeyCode {
     PageDown,
     /// Tab key.
     Tab,
-    /// Shift + Tab key.
-    BackTab,
     /// Delete key.
     Delete,
     /// Insert key.
@@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode {
             KeyCode::PageUp => CKeyCode::PageUp,
             KeyCode::PageDown => CKeyCode::PageDown,
             KeyCode::Tab => CKeyCode::Tab,
-            KeyCode::BackTab => CKeyCode::BackTab,
             KeyCode::Delete => CKeyCode::Delete,
             KeyCode::Insert => CKeyCode::Insert,
             KeyCode::F(f_number) => CKeyCode::F(f_number),
@@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode {
             CKeyCode::PageUp => KeyCode::PageUp,
             CKeyCode::PageDown => KeyCode::PageDown,
             CKeyCode::Tab => KeyCode::Tab,
-            CKeyCode::BackTab => KeyCode::BackTab,
+            CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
             CKeyCode::Delete => KeyCode::Delete,
             CKeyCode::Insert => KeyCode::Insert,
             CKeyCode::F(f_number) => KeyCode::F(f_number),
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index 3e779356..a56c914d 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -5,6 +5,7 @@ pub mod clipboard;
 pub mod document;
 pub mod editor;
 pub mod graphics;
+pub mod gutter;
 pub mod info;
 pub mod input;
 pub mod keyboard;
@@ -12,8 +13,18 @@ pub mod theme;
 pub mod tree;
 pub mod view;
 
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
-pub struct DocumentId(usize);
+use std::num::NonZeroUsize;
+
+// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+pub struct DocumentId(NonZeroUsize);
+
+impl Default for DocumentId {
+    fn default() -> DocumentId {
+        // Safety: 1 is non-zero
+        DocumentId(unsafe { NonZeroUsize::new_unchecked(1) })
+    }
+}
 
 slotmap::new_key_type! {
     pub struct ViewId;
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 757316bd..4a2ecbba 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style};
 pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
     toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
 });
+pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
+    toml::from_slice(include_bytes!("../../base16_theme.toml"))
+        .expect("Failed to parse base 16 default theme")
+});
 
 #[derive(Clone, Debug)]
 pub struct Loader {
@@ -35,6 +39,9 @@ impl Loader {
         if name == "default" {
             return Ok(self.default());
         }
+        if name == "base16_default" {
+            return Ok(self.base16_default());
+        }
         let filename = format!("{}.toml", name);
 
         let user_path = self.user_dir.join(&filename);
@@ -74,12 +81,20 @@ impl Loader {
     pub fn default(&self) -> Theme {
         DEFAULT_THEME.clone()
     }
+
+    /// Returns the alternative 16-color default theme
+    pub fn base16_default(&self) -> Theme {
+        BASE16_DEFAULT_THEME.clone()
+    }
 }
 
 #[derive(Clone, Debug)]
 pub struct Theme {
-    scopes: Vec<String>,
+    // UI styles are stored in a HashMap
     styles: HashMap<String, Style>,
+    // tree-sitter highlight styles are stored in a Vec to optimize lookups
+    scopes: Vec<String>,
+    highlights: Vec<Style>,
 }
 
 impl<'de> Deserialize<'de> for Theme {
@@ -88,6 +103,8 @@ impl<'de> Deserialize<'de> for Theme {
         D: Deserializer<'de>,
     {
         let mut styles = HashMap::new();
+        let mut scopes = Vec::new();
+        let mut highlights = Vec::new();
 
         if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
             // TODO: alert user of parsing failures in editor
@@ -102,24 +119,38 @@ impl<'de> Deserialize<'de> for Theme {
                 .unwrap_or_default();
 
             styles.reserve(colors.len());
+            scopes.reserve(colors.len());
+            highlights.reserve(colors.len());
+
             for (name, style_value) in colors {
                 let mut style = Style::default();
                 if let Err(err) = palette.parse_style(&mut style, style_value) {
                     warn!("{}", err);
                 }
-                styles.insert(name, style);
+
+                // these are used both as UI and as highlights
+                styles.insert(name.clone(), style);
+                scopes.push(name);
+                highlights.push(style);
             }
         }
 
-        let scopes = styles.keys().map(ToString::to_string).collect();
-        Ok(Self { scopes, styles })
+        Ok(Self {
+            scopes,
+            styles,
+            highlights,
+        })
     }
 }
 
 impl Theme {
+    #[inline]
+    pub fn highlight(&self, index: usize) -> Style {
+        self.highlights[index]
+    }
+
     pub fn get(&self, scope: &str) -> Style {
-        self.try_get(scope)
-            .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
+        self.try_get(scope).unwrap_or_default()
     }
 
     pub fn try_get(&self, scope: &str) -> Option<Style> {
@@ -134,6 +165,14 @@ impl Theme {
     pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
         self.scopes().iter().position(|s| s == scope)
     }
+
+    pub fn is_16_color(&self) -> bool {
+        self.styles.iter().all(|(_, style)| {
+            [style.fg, style.bg]
+                .into_iter()
+                .all(|color| !matches!(color, Some(Color::Rgb(..))))
+        })
+    }
 }
 
 struct ThemePalette {
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index a77f1562..94d67acd 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -1,6 +1,10 @@
 use std::borrow::Cow;
 
-use crate::{graphics::Rect, Document, DocumentId, ViewId};
+use crate::{
+    graphics::Rect,
+    gutter::{self, Gutter},
+    Document, DocumentId, ViewId,
+};
 use helix_core::{
     graphemes::{grapheme_width, RopeGraphemes},
     line_ending::line_end_char_index,
@@ -60,6 +64,8 @@ impl JumpList {
     }
 }
 
+const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
+
 #[derive(Debug)]
 pub struct View {
     pub id: ViewId,
@@ -69,6 +75,11 @@ pub struct View {
     pub jumps: JumpList,
     /// the last accessed file before the current one
     pub last_accessed_doc: Option<DocumentId>,
+    /// the last modified files before the current one
+    /// ordered from most frequent to least frequent
+    // uses two docs because we want to be able to swap between the
+    // two last modified docs which we need to manually keep track of
+    pub last_modified_docs: [Option<DocumentId>; 2],
 }
 
 impl View {
@@ -80,13 +91,23 @@ impl View {
             area: Rect::default(), // will get calculated upon inserting into tree
             jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
             last_accessed_doc: None,
+            last_modified_docs: [None, None],
         }
     }
 
+    pub fn gutters(&self) -> &[(Gutter, usize)] {
+        GUTTERS
+    }
+
     pub fn inner_area(&self) -> Rect {
-        // TODO: not ideal
-        const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
-        self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
+        // TODO: cache this
+        let offset = self
+            .gutters()
+            .iter()
+            .map(|(_, width)| *width as u16)
+            .sum::<u16>()
+            + 1; // +1 for some space between gutters and line
+        self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
     }
 
     //
@@ -276,6 +297,7 @@ mod tests {
     use super::*;
     use helix_core::Rope;
     const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
+                           // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
 
     #[test]
     fn test_text_pos_at_screen_coords() {
diff --git a/languages.toml b/languages.toml
index 2ce9321e..abb94acf 100644
--- a/languages.toml
+++ b/languages.toml
@@ -11,6 +11,7 @@ indent = { tab-width = 4, unit = "    " }
 [language.config]
 cargo = { loadOutDirsFromCheck = true }
 procMacro = { enable = false }
+diagnostics = { disabled = ["unresolved-proc-macro"] }
 
 [[language]]
 name = "toml"
@@ -249,7 +250,6 @@ language-server = { command = "julia", args = [
                 using Pkg;
                 import StaticLint;
                 env_path = dirname(Pkg.Types.Context().env.project_file);
-
                 server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
                 server.runlinter = true;
                 run(server);
@@ -396,3 +396,37 @@ shebangs = ["perl"]
 roots = []
 comment-token = "#"
 indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "racket"
+scope = "source.rkt"
+roots = []
+file-types = ["rkt"]
+shebangs = ["racket"]
+comment-token = ";"
+language-server = { command = "racket", args = ["-l", "racket-langserver"] }
+
+[[language]]
+name = "wgsl"
+scope = "source.wgsl"
+file-types = ["wgsl"]
+roots = []
+comment-token = "//"
+indent = { tab-width = 4, unit = "    " }
+
+[[language]]
+name = "llvm"
+scope = "source.llvm"
+roots = []
+file-types = ["ll"]
+comment-token = ";"
+indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "markdown"
+scope = "source.md"
+injection-regex = "md|markdown"
+file-types = ["md"]
+roots = []
+
+indent = { tab-width = 2, unit = "  " }
diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm
index 72187876..8006cb07 100644
--- a/runtime/queries/haskell/highlights.scm
+++ b/runtime/queries/haskell/highlights.scm
@@ -8,7 +8,7 @@
 (constructor) @constructor
 (pragma) @pragma
 (comment) @comment
-(signature name: (variable) @fun_type_name)
+(signature name: (variable) @type)
 (function name: (variable) @function)
 (constraint class: (class_name (type)) @class)
 (class (class_head class: (class_name (type)) @class))
diff --git a/runtime/queries/julia/locals.scm b/runtime/queries/julia/locals.scm
index f8b34f71..d5ac794e 100644
--- a/runtime/queries/julia/locals.scm
+++ b/runtime/queries/julia/locals.scm
@@ -2,24 +2,24 @@
 (import_statement
  (identifier) @definition.import)
 (variable_declaration
- (identifier) @definition.var)
+ (identifier) @local.definition)
 (variable_declaration
  (tuple_expression
-  (identifier) @definition.var))
+  (identifier) @local.definition))
 (for_binding
- (identifier) @definition.var)
+ (identifier) @local.definition)
 (for_binding
  (tuple_expression
-  (identifier) @definition.var))
+  (identifier) @local.definition))
 
 (assignment_expression
  (tuple_expression
-  (identifier) @definition.var))
+  (identifier) @local.definition))
 (assignment_expression
  (bare_tuple_expression
-  (identifier) @definition.var))
+  (identifier) @local.definition))
 (assignment_expression
- (identifier) @definition.var)
+ (identifier) @local.definition)
 
 (type_parameter_list
   (identifier) @definition.type)
@@ -43,11 +43,11 @@
  (identifier) @definition.parameter)
 
 (function_definition
- name: (identifier) @definition.function) @scope
+ name: (identifier) @definition.function) @local.scope
 (macro_definition 
- name: (identifier) @definition.macro) @scope
+ name: (identifier) @definition.macro) @local.scope
 
-(identifier) @reference
+(identifier) @local.reference
 
 [
   (try_statement)
@@ -56,4 +56,4 @@
   (let_statement)
   (compound_expression)
   (for_statement)
-] @scope
+] @local.scope
diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm
index f045c82d..2e308f77 100644
--- a/runtime/queries/latex/highlights.scm
+++ b/runtime/queries/latex/highlights.scm
@@ -278,7 +278,7 @@
   "\\includeinkscape"
   "\\usepgflibrary"
   "\\usetikzlibrary"
-] @include
+] @keyword.control.import
 
 [
   "\\part"
@@ -318,60 +318,60 @@
 ["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
 
 (chapter
-  text: (brace_group) @text.title)
+  text: (brace_group) @markup.heading)
 
 (part
-  text: (brace_group) @text.title)
+  text: (brace_group) @markup.heading)
 
 (section
-  text: (brace_group) @text.title)
+  text: (brace_group) @markup.heading)
 
 (subsection
-  text: (brace_group) @text.title)
+  text: (brace_group) @markup.heading)
 
 (subsubsection
-  text: (brace_group) @text.title)
+  text: (brace_group) @markup.heading)
 
 (paragraph
-  text: (brace_group) @text.title)
+  text: (brace_group) @markup.heading)
 
 (subparagraph
-  text: (brace_group) @text.title)
+  text: (brace_group) @markup.heading)
 
 ((environment
   (begin
    name: (word) @_frame)
    (brace_group
-        child: (text) @text.title))
+        child: (text) @markup.heading))
  (#eq? @_frame "frame"))
 
 ((generic_command
   name:(generic_command_name) @_name
   arg: (brace_group
-          (text) @text.title))
+          (text) @markup.heading))
  (#eq? @_name "\\frametitle"))
 
 ;; Formatting
 
 ((generic_command
   name:(generic_command_name) @_name
-  arg: (_) @text.emphasis)
+  arg: (_) @markup.italic)
  (#eq? @_name "\\emph"))
 
 ((generic_command
   name:(generic_command_name) @_name
-  arg: (_) @text.emphasis)
+  arg: (_) @markup.italic)
  (#match? @_name "^(\\\\textit|\\\\mathit)$"))
 
 ((generic_command
   name:(generic_command_name) @_name
-  arg: (_) @text.strong)
+  arg: (_) @markup.bold)
  (#match? @_name "^(\\\\textbf|\\\\mathbf)$"))
 
 ((generic_command
   name:(generic_command_name) @_name
   .
-  arg: (_) @text.uri)
+  arg: (_) @markup.underline.link)
  (#match? @_name "^(\\\\url|\\\\href)$"))
 
 (ERROR) @error
diff --git a/runtime/queries/ledger/highlights.scm b/runtime/queries/ledger/highlights.scm
index bdf5f2db..02a9ea9a 100644
--- a/runtime/queries/ledger/highlights.scm
+++ b/runtime/queries/ledger/highlights.scm
@@ -12,7 +12,7 @@
 ((account) @variable.other.member)
 ((commodity) @text.literal)
 
-"include" @include
+"include" @keyword.local.import
 
 [
     "account"
diff --git a/runtime/queries/llvm/highlights.scm b/runtime/queries/llvm/highlights.scm
new file mode 100644
index 00000000..73afe85e
--- /dev/null
+++ b/runtime/queries/llvm/highlights.scm
@@ -0,0 +1,14 @@
+(type) @type
+(statement) @keyword.operator
+(number) @constant.numeric.integer
+(comment) @comment
+(string) @string
+(label) @label
+(keyword) @keyword
+"ret" @keyword.control.return
+(boolean) @constant.builtin.boolean
+(float) @constant.numeric.float
+(constant) @constant
+(identifier) @variable
+(symbol) @punctuation.delimiter
+(bracket) @punctuation.bracket
diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm
new file mode 100644
index 00000000..a102719b
--- /dev/null
+++ b/runtime/queries/markdown/highlights.scm
@@ -0,0 +1,35 @@
+[
+  (atx_heading)
+  (setext_heading)
+] @markup.heading
+
+(code_fence_content) @none
+
+[
+  (indented_code_block)
+  (fenced_code_block)
+] @markup.raw.block
+
+(code_span) @markup.raw.inline
+
+(emphasis) @markup.italic
+
+(strong_emphasis) @markup.bold
+
+(link_destination) @markup.underline.link
+
+; (link_label) @markup.label ; TODO: rename
+
+[
+  (list_marker_plus)
+  (list_marker_minus)
+  (list_marker_star)
+  (list_marker_dot)
+  (list_marker_parenthesis)
+] @punctuation.special
+
+[
+  (backslash_escape)
+  (hard_line_break)
+] @string.character.escape
+
diff --git a/runtime/queries/markdown/injections.scm b/runtime/queries/markdown/injections.scm
new file mode 100644
index 00000000..ff3c5fe6
--- /dev/null
+++ b/runtime/queries/markdown/injections.scm
@@ -0,0 +1,8 @@
+(fenced_code_block
+  (info_string) @injection.language
+  (code_fence_content) @injection.content)
+
+((html_block) @injection.content
+ (#set! injection.language "html"))
+((html_tag) @injection.content
+ (#set! injection.language "html"))
diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm
index 15f46cc1..a08b1267 100644
--- a/runtime/queries/ocaml/highlights.scm
+++ b/runtime/queries/ocaml/highlights.scm
@@ -90,7 +90,7 @@
 
 ["exception" "try"] @keyword.control.exception
 
-["include" "open"] @include
+["include" "open"] @keyword.control.import
 
 ["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat
 
diff --git a/runtime/queries/perl/indents.toml b/runtime/queries/perl/indents.toml
new file mode 100644
index 00000000..365e0663
--- /dev/null
+++ b/runtime/queries/perl/indents.toml
@@ -0,0 +1,17 @@
+indent = [
+  "function",
+  "identifier",
+  "method_invocation",
+  "if_statement",
+  "unless_statement",
+  "if_simple_statement",
+  "unless_simple_statement",
+  "variable_declaration",
+  "block",
+  "list_item",
+  "word_list_qw"
+]
+
+outdent = [
+ "}"
+]
diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm
index 4fcdfd66..f9eef6b5 100644
--- a/runtime/queries/svelte/highlights.scm
+++ b/runtime/queries/svelte/highlights.scm
@@ -25,7 +25,7 @@
 
 ((attribute
    (attribute_name) @_attr
-   (quoted_attribute_value (attribute_value) @markup.undeline.link))
+   (quoted_attribute_value (attribute_value) @markup.underline.link))
  (#match? @_attr "^(href|src)$"))
 
 (tag_name) @tag
diff --git a/runtime/queries/wgsl/highlights.scm b/runtime/queries/wgsl/highlights.scm
new file mode 100644
index 00000000..7fbc87d8
--- /dev/null
+++ b/runtime/queries/wgsl/highlights.scm
@@ -0,0 +1,102 @@
+(const_literal) @constant.numeric
+
+(type_declaration) @type
+
+(function_declaration
+    (identifier) @function)
+
+(struct_declaration
+    (identifier) @type)
+
+(type_constructor_or_function_call_expression
+    (type_declaration) @function)
+
+(parameter
+    (variable_identifier_declaration (identifier) @variable.parameter))
+
+[
+    "struct"
+    "bitcast"
+    ; "block"
+    "discard"
+    "enable"
+    "fallthrough"
+    "fn"
+    "let"
+    "private"
+    "read"
+    "read_write"
+    "return"
+    "storage"
+    "type"
+    "uniform"
+    "var"
+    "workgroup"
+    "write"
+    (texel_format)
+] @keyword ; TODO reserved keywords
+
+[
+    (true)
+    (false)
+] @constant.builtin.boolean
+
+[ "," "." ":" ";" ] @punctuation.delimiter
+
+;; brackets
+[
+    "("
+    ")"
+    "["
+    "]"
+    "{"
+    "}"
+] @punctuation.bracket
+
+[
+    "loop"
+    "for"
+    "break"
+    "continue"
+    "continuing"
+] @keyword.control.repeat
+
+[
+    "if"
+    "else"
+    "elseif"
+    "switch"
+    "case"
+    "default"
+] @keyword.control.conditional
+
+[
+    "&"
+    "&&"
+    "/"
+    "!"
+    "="
+    "=="
+    "!="
+    ">"
+    ">="
+    ">>"
+    "<"
+    "<="
+    "<<"
+    "%"
+    "-"
+    "+"
+    "|"
+    "||"
+    "*"
+    "~"
+    "^"
+] @operator
+
+(attribute
+    (identifier) @variable.other.member)
+
+(comment) @comment
+
+(ERROR) @error
diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml
index d65995c0..fd4b0fea 100644
--- a/runtime/themes/base16_default_dark.toml
+++ b/runtime/themes/base16_default_dark.toml
@@ -1,27 +1,28 @@
-# Author: RayGervais<raygervais@hotmail.ca>
+# Author: RayGervais <raygervais@hotmail.ca>
 
 "ui.background" = { bg = "base00" }
 "ui.menu" = "base01"
-"ui.menu.selected" = { fg = "base04", bg = "base01" }
-"ui.linenr" = {fg = "base01" }
+"ui.menu.selected" = { fg = "base01", bg = "base04" }
+"ui.linenr" = { fg = "base03", bg = "base01" }
 "ui.popup" = { bg = "base01" }
 "ui.window" = { bg = "base01" }
-"ui.liner.selected" = "base02"
-"ui.selection" = "base02"
-"comment" = "base03"
-"ui.statusline" = {fg = "base04", bg = "base01" }
+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
+"ui.selection" = { bg = "base02" }
+"comment" = { fg = "base03", modifiers = ["italic"] }
+"ui.statusline" = { fg = "base04", bg = "base01" }
 "ui.help" = { fg = "base04", bg = "base01" }
-"ui.cursor" = { fg = "base05", modifiers = ["reversed"] }
-"ui.text" = { fg = "base05" }
+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
+"ui.text" = "base05"
 "operator" = "base05"
-"ui.text.focus" = { fg = "base05" }
+"ui.text.focus" = "base05"
 "variable" = "base08"
 "constant.numeric" = "base09"
 "constant" = "base09"
-"attributes" = "base09" 
+"attributes" = "base09"
 "type" = "base0A"
 "ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
-"strings"  = "base0B"
+"string"  = "base0B"
 "variable.other.member" = "base0B"
 "constant.character.escape" = "base0C"
 "function" = "base0D"
@@ -30,15 +31,15 @@
 "keyword" = "base0E"
 "label" = "base0E"
 "namespace" = "base0E"
-"ui.popup" = { bg = "base01" }
-"ui.window" = { bg = "base00" }
-"ui.help" = { bg = "base01", fg = "base06" }
+"ui.help" = { fg = "base06", bg = "base01" }
 
-"info" = "base03"
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "base01" }
+"info" = "base0D"
 "hint" = "base03"
 "debug" = "base03"
-"diagnostic" = "base03"
-"error" = "base0E"
+"warning" = "base09"
+"error" = "base08"
 
 [palette]
 base00 = "#181818" # Default Background
diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml
new file mode 100644
index 00000000..596990da
--- /dev/null
+++ b/runtime/themes/base16_default_light.toml
@@ -0,0 +1,60 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.background" = { bg = "base00" }
+"ui.menu" = "base01"
+"ui.menu.selected" = { fg = "base01", bg = "base04" }
+"ui.linenr" = { fg = "base03", bg = "base01" }
+"ui.popup" = { bg = "base01" }
+"ui.window" = { bg = "base01" }
+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
+"ui.selection" = { bg = "base02" }
+"comment" = { fg = "base03", modifiers = ["italic"] }
+"ui.statusline" = { fg = "base04", bg = "base01" }
+"ui.help" = { fg = "base04", bg = "base01" }
+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
+"ui.text" = "base05"
+"operator" = "base05"
+"ui.text.focus" = "base05"
+"variable" = "base08"
+"constant.numeric" = "base09"
+"constant" = "base09"
+"attributes" = "base09"
+"type" = "base0A"
+"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
+"string"  = "base0B"
+"variable.other.member" = "base0B"
+"constant.character.escape" = "base0C"
+"function" = "base0D"
+"constructor" = "base0D"
+"special" = "base0D"
+"keyword" = "base0E"
+"label" = "base0E"
+"namespace" = "base0E"
+"ui.help" = { fg = "base06", bg = "base01" }
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "base01" }
+"info" = "base0D"
+"hint" = "base03"
+"debug" = "base03"
+"warning" = "base09"
+"error" = "base08"
+
+[palette]
+base00 = "#f8f8f8" # Default Background
+base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks)
+base02 = "#d8d8d8" # Selection Background
+base03 = "#b8b8b8" # Comments, Invisibles, Line Highlighting
+base04 = "#585858" # Dark Foreground (Used for status bars)
+base05 = "#383838" # Default Foreground, Caret, Delimiters, Operators
+base06 = "#282828" # Light Foreground (Not often used)
+base07 = "#181818" # Light Background (Not often used)
+base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
+base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
+base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
+base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
+base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
+base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
+base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
+base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml
new file mode 100644
index 00000000..123ceaea
--- /dev/null
+++ b/runtime/themes/base16_terminal.toml
@@ -0,0 +1,39 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.menu" = "black"
+"ui.menu.selected" = { modifiers = ["reversed"] }
+"ui.linenr" = { fg = "light-gray", bg = "black" }
+"ui.popup" = { bg = "black" }
+"ui.window" = { bg = "black" }
+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
+"ui.selection" = { fg = "gray", modifiers = ["reversed"] }
+"comment" = { fg = "light-gray", modifiers = ["italic"] }
+"ui.statusline" = { fg = "white", bg = "black" }
+"ui.statusline.inactive" = { fg = "gray", bg = "black" }
+"ui.help" = { fg = "white", bg = "black" }
+"ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] }
+"ui.cursor.primary" = { modifiers = ["reversed"] }
+"variable" = "light-red"
+"constant.numeric" = "yellow"
+"constant" = "yellow"
+"attributes" = "yellow"
+"type" = "light-yellow"
+"ui.cursor.match" = { fg = "light-yellow", modifiers = ["underlined"] }
+"string"  = "light-green"
+"variable.other.member" = "light-green"
+"constant.character.escape" = "light-cyan"
+"function" = "light-blue"
+"constructor" = "light-blue"
+"special" = "light-blue"
+"keyword" = "light-magenta"
+"label" = "light-magenta"
+"namespace" = "light-magenta"
+"ui.help" = { fg = "white", bg = "black" }
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "black" }
+"info" = "light-blue"
+"hint" = "gray"
+"debug" = "gray"
+"warning" = "yellow"
+"error" = "light-red"
diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml
new file mode 100644
index 00000000..bf8a4a84
--- /dev/null
+++ b/runtime/themes/monokai_pro.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable"  = "base8"
+"constant"  = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#ff6188"
+"orange" = "#fc9867"
+"yellow" = "#ffd866"
+"green" = "#a9dc76"
+"blue" = "#78dce8"
+"purple" = "#ab9df2"
+# base colors, sorted from darkest to lightest
+"base0" = "#19181a"
+"base1" = "#221f22"
+"base2" = "#2d2a2e"
+"base3" = "#403e41"
+"base4" = "#5b595c"
+"base5" = "#727072"
+"base6" = "#939293"
+"base7" = "#c1c0c0"
+"base8" = "#fcfcfa"
+# variants (for when transparency isn't supported)
+"base8x0c" = "#363337" # using base2 as bg
diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml
new file mode 100644
index 00000000..d8a701f1
--- /dev/null
+++ b/runtime/themes/monokai_pro_machine.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable"  = "base8"
+"constant"  = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#ff6d7e"
+"orange" = "#ffb270"
+"yellow" = "#ffed72"
+"green" = "#a2e57b"
+"blue" = "#7cd5f1"
+"purple" = "#baa0f8"
+# base colors
+"base0" = "#161b1e"
+"base1" = "#1d2528"
+"base2" = "#273136"
+"base3" = "#3a4449"
+"base4" = "#545f62"
+"base5" = "#6b7678"
+"base6" = "#798384"
+"base7" = "#b8c4c3"
+"base8" = "#f2fffc"
+# variants
+"base8x0c" = "#303a3e"
diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml
new file mode 100644
index 00000000..74459472
--- /dev/null
+++ b/runtime/themes/monokai_pro_octagon.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable"  = "base8"
+"constant"  = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#ff657a"
+"orange" = "#ff9b5e"
+"yellow" = "#ffd76d"
+"green" = "#bad761"
+"blue" = "#9cd1bb"
+"purple" = "#c39ac9"
+# base colors
+"base0" = "#161821"
+"base1" = "#1e1f2b"
+"base2" = "#282a3a"
+"base3" = "#3a3d4b"
+"base4" = "#535763"
+"base5" = "#696d77"
+"base6" = "#767b81"
+"base7" = "#b2b9bd"
+"base8" = "#eaf2f1"
+# variants
+"base8x0c" = "#303342"
diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml
new file mode 100644
index 00000000..a9cf4b34
--- /dev/null
+++ b/runtime/themes/monokai_pro_ristretto.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable"  = "base8"
+"constant"  = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#fd6883"
+"orange" = "#f38d70"
+"yellow" = "#f9cc6c"
+"green" = "#adda78"
+"blue" = "#85dacc"
+"purple" = "#a8a9eb"
+# base colors
+"base0" = "#191515"
+"base1" = "#211c1c"
+"base2" = "#2c2525"
+"base3" = "#403838"
+"base4" = "#5b5353"
+"base5" = "#72696a"
+"base6" = "#8c8384"
+"base7" = "#c3b7b8"
+"base8" = "#fff1f3"
+# variants
+"base8x0c" = "#352e2e"
diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml
new file mode 100644
index 00000000..232adfbd
--- /dev/null
+++ b/runtime/themes/monokai_pro_spectrum.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable"  = "base8"
+"constant"  = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#fc618d"
+"orange" = "#fd9353"
+"yellow" = "#fce566"
+"green" = "#7bd88f"
+"blue" = "#5ad4e6"
+"purple" = "#948ae3"
+# base colors
+"base0" = "#131313"
+"base1" = "#191919"
+"base2" = "#222222"
+"base3" = "#363537"
+"base4" = "#525053"
+"base5" = "#69676c"
+"base6" = "#8b888f"
+"base7" = "#bab6c0"
+"base8" = "#f7f1ff"
+# variants
+"base8x0c" = "#2b2b2b"
diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml
index 53777008..256b33c8 100644
--- a/runtime/themes/rose_pine.toml
+++ b/runtime/themes/rose_pine.toml
@@ -10,6 +10,7 @@
 "ui.selection" = "highlight"
 "comment" = "subtle"
 "ui.statusline" = {fg = "foam", bg = "surface" }
+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
 "ui.help" = { fg = "foam", bg = "surface" }
 "ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
 "ui.text" = { fg = "text" }
diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml
new file mode 100644
index 00000000..43ba24ed
--- /dev/null
+++ b/runtime/themes/rose_pine_dawn.toml
@@ -0,0 +1,63 @@
+# Author: ChrisHa<chunghha@users.noreply.github.com>
+# Author: RayGervais<raygervais@hotmail.ca>
+
+"ui.background" = { bg = "base" }
+"ui.menu" = "surface"
+"ui.menu.selected" = { fg = "iris", bg = "surface" }
+"ui.linenr" = {fg = "subtle" }
+"ui.popup" = { bg = "overlay" }
+"ui.window" = { bg = "overlay" }
+"ui.liner.selected" = "highlightOverlay"
+"ui.selection" = "highlight"
+"comment" = "subtle"
+"ui.statusline" = {fg = "foam", bg = "surface" }
+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
+"ui.help" = { fg = "foam", bg = "surface" }
+"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
+"ui.text" = { fg = "text" }
+"operator" = "rose"
+"ui.text.focus" = { fg = "base05" }
+"variable" = "text"
+"number" = "iris"
+"constant" = "gold"
+"attributes" = "gold" 
+"type" = "foam"
+"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] }
+"string"  = "gold"
+"property" = "foam"
+"escape" = "subtle"
+"function" = "rose"
+"function.builtin" = "rose"
+"function.method"  = "foam"
+"constructor" = "gold"
+"special" = "gold"
+"keyword" = "pine"
+"label" = "iris"
+"namespace" = "pine"
+"ui.popup" = { bg = "overlay" }
+"ui.window" = { bg = "base" }
+"ui.help" = { bg = "overlay", fg = "foam" }
+"text" = "text"
+
+"info" = "gold"
+"hint" = "gold"
+"debug" = "rose"
+"diagnostic" = "rose"
+"error" = "love"
+
+[palette]
+base     = "#faf4ed" 
+surface  = "#fffaf3" 
+overlay  = "#f2e9de"
+inactive = "#9893a5"
+subtle   = "#6e6a86"
+text     = "#575279"
+love     = "#b4637a"
+gold     = "#ea9d34"
+rose     = "#d7827e"
+pine     = "#286983"
+foam     = "#56949f"
+iris     = "#907aa9"
+highlight = "#eee9e6"
+highlightInactive = "#f2ede9"
+highlightOverlay = "#e4dfde"
diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml
index 984c86ee..979fdaac 100644
--- a/runtime/themes/solarized_dark.toml
+++ b/runtime/themes/solarized_dark.toml
@@ -58,13 +58,13 @@
 "ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
 
 # 主光标/selectio
-"ui.cursor.primary" = {fg = "base03", bg = "base1"}
-"ui.selection.primary" = { fg = "base03", bg = "base01" }
-"ui.cursor.select" = {fg = "base02", bg = "green"}
-"ui.selection" = { fg = "base02", bg = "yellow" }
+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
+"ui.selection" = { bg = "base0175" }
+"ui.selection.primary" = { bg = "base015" }
 
 # normal模式的光标
-"ui.cursor" = {fg = "base03", bg = "green"}
+"ui.cursor" = {fg = "base02", bg = "cyan"}
 "ui.cursor.insert" = {fg = "base03", bg = "base3"}
 # 当前光标匹配的标点符号
 "ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,18 +73,20 @@
 "error" = { fg = "red", modifiers= ["bold", "underlined"] }
 "info" = { fg = "blue", modifiers= ["bold", "underlined"] }
 "hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
-"diagnostic" = { mdifiers = ["underlined"] }
+"diagnostic" = { modifiers = ["underlined"] }
 
 [palette]
 # 深色 越来越深
-base03  = "#002b36"
-base02  = "#073642"
-base01  = "#586e75"
-base00  = "#657b83"
-base0   = "#839496"
-base1   = "#93a1a1"
-base2   = "#eee8d5"
-base3   = "#fdf6e3"
+base03   = "#002b36"
+base02   = "#073642"
+base0175 = "#16404b"
+base015  = "#2c4f59"
+base01   = "#586e75"
+base00   = "#657b83"
+base0    = "#839496"
+base1    = "#93a1a1"
+base2    = "#eee8d5"
+base3    = "#fdf6e3"
 
 # 浅色 越來越浅
 yellow  = "#b58900"
diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml
index 0ab1b962..ded90cc4 100644
--- a/runtime/themes/solarized_light.toml
+++ b/runtime/themes/solarized_light.toml
@@ -58,13 +58,13 @@
 "ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
 
 # 主光标/selectio
-"ui.cursor.primary" = {fg = "base03", bg = "base1"}
-"ui.selection.primary" = { fg = "base03", bg = "base01" }
-"ui.cursor.select" = {fg = "base02", bg = "green"}
-"ui.selection" = { fg = "base02", bg = "yellow" }
+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
+"ui.selection" = { bg = "base0175" }
+"ui.selection.primary" = { bg = "base015" }
 
 # normal模式的光标
-"ui.cursor" = {fg = "base03", bg = "green"}
+"ui.cursor" = {fg = "base02", bg = "cyan"}
 "ui.cursor.insert" = {fg = "base03", bg = "base3"}
 # 当前光标匹配的标点符号
 "ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,26 +73,28 @@
 "error" = { fg = "red", modifiers= ["bold", "underlined"] }
 "info" = { fg = "blue", modifiers= ["bold", "underlined"] }
 "hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
-"diagnostic" = { mdifiers = ["underlined"] }
+"diagnostic" = { modifiers = ["underlined"] }
 
 [palette]
-red     	= '#dc322f'
-green   	= '#859900'
-yellow  	= '#b58900'
-blue    	= '#268bd2'
-magenta 	= '#d33682'
-cyan    	= '#2aa198'
-orange  	= '#cb4b16'
-violet  	= '#6c71c4'
+red      = '#dc322f'
+green    = '#859900'
+yellow   = '#b58900'
+blue     = '#268bd2'
+magenta  = '#d33682'
+cyan     = '#2aa198'
+orange   = '#cb4b16'
+violet   = '#6c71c4'
 
 # 深色 越来越深
-base0   	= '#657b83'
-base1   	= '#586e75'
-base2   	= '#073642'
-base3   	= '#002b36'
+base0    = '#657b83'
+base1    = '#586e75'
+base2    = '#073642'
+base3    = '#002b36'
 
 ## 浅色 越來越浅
-base00  	= '#839496'
-base01  	= '#93a1a1'
-base02  	= '#eee8d5'
-base03  	= '#fdf6e3'
+base00   = '#839496'
+base01   = '#93a1a1'
+base015  = '#c5c8bd'
+base0175 = '#dddbcc'
+base02   = '#eee8d5'
+base03   = '#fdf6e3'
diff --git a/theme.toml b/theme.toml
index 8c0d1f6c..0a79861e 100644
--- a/theme.toml
+++ b/theme.toml
@@ -28,6 +28,12 @@ string = "silver"
 # used for lifetimes
 label = "honey"
 
+"markup.heading" = "lilac"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.underline.link" = { fg = "silver", modifiers = ["underlined"] }
+"markup.raw" = "almond"
+
 # TODO: diferentiate doc comment
 # concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
 
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
new file mode 100644
index 00000000..fe5d55d4
--- /dev/null
+++ b/xtask/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "xtask"
+version = "0.5.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+helix-term = { version = "0.5", path = "../helix-term" }
+helix-core = { version = "0.5", path = "../helix-core" }
+toml = "0.5"
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
new file mode 100644
index 00000000..7256653a
--- /dev/null
+++ b/xtask/src/main.rs
@@ -0,0 +1,277 @@
+use std::{env, error::Error};
+
+type DynError = Box<dyn Error>;
+
+pub mod helpers {
+    use std::{
+        fmt::Display,
+        path::{Path, PathBuf},
+    };
+
+    use crate::path;
+    use helix_core::syntax::Configuration as LangConfig;
+
+    #[derive(Copy, Clone)]
+    pub enum TsFeature {
+        Highlight,
+        TextObjects,
+        AutoIndent,
+    }
+
+    impl TsFeature {
+        pub fn all() -> &'static [Self] {
+            &[Self::Highlight, Self::TextObjects, Self::AutoIndent]
+        }
+
+        pub fn runtime_filename(&self) -> &'static str {
+            match *self {
+                Self::Highlight => "highlights.scm",
+                Self::TextObjects => "textobjects.scm",
+                Self::AutoIndent => "indents.toml",
+            }
+        }
+    }
+
+    impl Display for TsFeature {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(
+                f,
+                "{}",
+                match *self {
+                    Self::Highlight => "Syntax Highlighting",
+                    Self::TextObjects => "Treesitter Textobjects",
+                    Self::AutoIndent => "Auto Indent",
+                }
+            )
+        }
+    }
+
+    /// Get the list of languages that support a particular tree-sitter
+    /// based feature.
+    pub fn ts_lang_support(feat: TsFeature) -> Vec<String> {
+        let queries_dir = path::ts_queries();
+
+        find_files(&queries_dir, feat.runtime_filename())
+            .iter()
+            .map(|f| {
+                // .../helix/runtime/queries/python/highlights.scm
+                let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm
+                let lang = tail.components().next().unwrap(); // python
+                lang.as_os_str().to_string_lossy().to_string()
+            })
+            .collect()
+    }
+
+    /// Get the list of languages that have any form of tree-sitter
+    /// queries defined in the runtime directory.
+    pub fn langs_with_ts_queries() -> Vec<String> {
+        std::fs::read_dir(path::ts_queries())
+            .unwrap()
+            .filter_map(|entry| {
+                let entry = entry.ok()?;
+                entry
+                    .file_type()
+                    .ok()?
+                    .is_dir()
+                    .then(|| entry.file_name().to_string_lossy().to_string())
+            })
+            .collect()
+    }
+
+    // naive implementation, but suffices for our needs
+    pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> {
+        std::fs::read_dir(dir)
+            .unwrap()
+            .filter_map(|entry| {
+                let path = entry.ok()?.path();
+                if path.is_dir() {
+                    Some(find_files(&path, filename))
+                } else {
+                    (path.file_name()?.to_string_lossy() == filename).then(|| vec![path])
+                }
+            })
+            .flatten()
+            .collect()
+    }
+
+    pub fn lang_config() -> LangConfig {
+        let bytes = std::fs::read(path::lang_config()).unwrap();
+        toml::from_slice(&bytes).unwrap()
+    }
+}
+
+pub mod md_gen {
+    use crate::DynError;
+
+    use crate::helpers;
+    use crate::path;
+    use std::fs;
+
+    use helix_term::commands::cmd::TYPABLE_COMMAND_LIST;
+
+    pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
+    pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
+
+    fn md_table_heading(cols: &[String]) -> String {
+        let mut header = String::new();
+        header += &md_table_row(cols);
+        header += &md_table_row(&vec!["---".to_string(); cols.len()]);
+        header
+    }
+
+    fn md_table_row(cols: &[String]) -> String {
+        "| ".to_owned() + &cols.join(" | ") + " |\n"
+    }
+
+    fn md_mono(s: &str) -> String {
+        format!("`{}`", s)
+    }
+
+    pub fn typable_commands() -> Result<String, DynError> {
+        let mut md = String::new();
+        md.push_str(&md_table_heading(&[
+            "Name".to_owned(),
+            "Description".to_owned(),
+        ]));
+
+        let cmdify = |s: &str| format!("`:{}`", s);
+
+        for cmd in TYPABLE_COMMAND_LIST {
+            let names = std::iter::once(&cmd.name)
+                .chain(cmd.aliases.iter())
+                .map(|a| cmdify(a))
+                .collect::<Vec<_>>()
+                .join(", ");
+
+            md.push_str(&md_table_row(&[names.to_owned(), cmd.doc.to_owned()]));
+        }
+
+        Ok(md)
+    }
+
+    pub fn lang_features() -> Result<String, DynError> {
+        let mut md = String::new();
+        let ts_features = helpers::TsFeature::all();
+
+        let mut cols = vec!["Language".to_owned()];
+        cols.append(
+            &mut ts_features
+                .iter()
+                .map(|t| t.to_string())
+                .collect::<Vec<_>>(),
+        );
+        cols.push("Default LSP".to_owned());
+
+        md.push_str(&md_table_heading(&cols));
+        let config = helpers::lang_config();
+
+        let mut langs = config
+            .language
+            .iter()
+            .map(|l| l.language_id.clone())
+            .collect::<Vec<_>>();
+        langs.sort_unstable();
+
+        let mut ts_features_to_langs = Vec::new();
+        for &feat in ts_features {
+            ts_features_to_langs.push((feat, helpers::ts_lang_support(feat)));
+        }
+
+        let mut row = Vec::new();
+        for lang in langs {
+            let lc = config
+                .language
+                .iter()
+                .find(|l| l.language_id == lang)
+                .unwrap(); // lang comes from config
+            row.push(lc.language_id.clone());
+
+            for (_feat, support_list) in &ts_features_to_langs {
+                row.push(
+                    if support_list.contains(&lang) {
+                        "✓"
+                    } else {
+                        ""
+                    }
+                    .to_owned(),
+                );
+            }
+            row.push(
+                lc.language_server
+                    .as_ref()
+                    .map(|s| s.command.clone())
+                    .map(|c| md_mono(&c))
+                    .unwrap_or_default(),
+            );
+
+            md.push_str(&md_table_row(&row));
+            row.clear();
+        }
+
+        Ok(md)
+    }
+
+    pub fn write(filename: &str, data: &str) {
+        let error = format!("Could not write to {}", filename);
+        let path = path::book_gen().join(filename);
+        fs::write(path, data).expect(&error);
+    }
+}
+
+pub mod path {
+    use std::path::{Path, PathBuf};
+
+    pub fn project_root() -> PathBuf {
+        Path::new(env!("CARGO_MANIFEST_DIR"))
+            .parent()
+            .unwrap()
+            .to_path_buf()
+    }
+
+    pub fn book_gen() -> PathBuf {
+        project_root().join("book/src/generated/")
+    }
+
+    pub fn ts_queries() -> PathBuf {
+        project_root().join("runtime/queries")
+    }
+
+    pub fn lang_config() -> PathBuf {
+        project_root().join("languages.toml")
+    }
+}
+
+pub mod tasks {
+    use crate::md_gen;
+    use crate::DynError;
+
+    pub fn docgen() -> Result<(), DynError> {
+        use md_gen::*;
+        write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?);
+        write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?);
+        Ok(())
+    }
+
+    pub fn print_help() {
+        println!(
+            "
+Usage: Run with `cargo xtask <task>`, eg. `cargo xtask docgen`.
+
+    Tasks:
+        docgen: Generate files to be included in the mdbook output.
+"
+        );
+    }
+}
+
+fn main() -> Result<(), DynError> {
+    let task = env::args().nth(1);
+    match task {
+        None => tasks::print_help(),
+        Some(t) => match t.as_str() {
+            "docgen" => tasks::docgen()?,
+            invalid => return Err(format!("Invalid task name: {}", invalid).into()),
+        },
+    };
+    Ok(())
+}