diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 000000000..35049cbcb
--- /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 958407bb8..41b00230f 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -17,6 +17,7 @@ Please search on the issue tracker before creating one. -->
 ### Environment
 
 - Platform: <!--  macOS / Windows / Linux -->
+- Terminal emulator: 
 - 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 216291804..65c2f9495 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -136,4 +136,52 @@ jobs:
         uses: actions-rs/cargo@v1
         with:
           command: clippy
-          args: -- -D warnings
+          args: --all-targets -- -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:
+          path: ~/.cargo/registry
+          key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+
+      - name: Cache cargo index
+        uses: actions/cache@v2.1.6
+        with:
+          path: ~/.cargo/git
+          key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+
+      - name: Cache cargo target dir
+        uses: actions/cache@v2.1.6
+        with:
+          path: target
+          key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
+
+      - name: Generate docs
+        uses: actions-rs/cargo@v1
+        with:
+          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)
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b16fa428a..7b0b7ee24 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.1
         with:
           name: bins-${{ matrix.build }}
           path: dist
diff --git a/.gitmodules b/.gitmodules
index 6295b9e95..55ed97b32 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -142,11 +142,87 @@
 	path = helix-syntax/languages/tree-sitter-perl
 	url = https://github.com/ganezdragon/tree-sitter-perl
 	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-comment"]
+	path = helix-syntax/languages/tree-sitter-comment
+	url = https://github.com/stsewd/tree-sitter-comment
+	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"]
+[submodule "helix-syntax/languages/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
+[submodule "helix-syntax/languages/tree-sitter-dart"]
+	path = helix-syntax/languages/tree-sitter-dart
+	url = https://github.com/UserNobody14/tree-sitter-dart.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-dockerfile"]
+	path = helix-syntax/languages/tree-sitter-dockerfile
+	url = https://github.com/camdencheek/tree-sitter-dockerfile.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-fish"]
+	path = helix-syntax/languages/tree-sitter-fish
+	url = https://github.com/ram02z/tree-sitter-fish
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-commit"]
+	path = helix-syntax/languages/tree-sitter-git-commit
+	url = https://github.com/the-mikedavis/tree-sitter-git-commit.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-llvm-mir"]
+	path = helix-syntax/languages/tree-sitter-llvm-mir
+	url = https://github.com/Flakebi/tree-sitter-llvm-mir.git
+    shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-diff"]
+	path = helix-syntax/languages/tree-sitter-git-diff
+	url = https://github.com/the-mikedavis/tree-sitter-git-diff.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-tablegen"]
+	path = helix-syntax/languages/tree-sitter-tablegen
+	url = https://github.com/Flakebi/tree-sitter-tablegen
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-rebase"]
+	path = helix-syntax/languages/tree-sitter-git-rebase
+	url = https://github.com/the-mikedavis/tree-sitter-git-rebase.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-lean"]
+	path = helix-syntax/languages/tree-sitter-lean
+	url = https://github.com/Julian/tree-sitter-lean
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-regex"]
+	path = helix-syntax/languages/tree-sitter-regex
+	url = https://github.com/tree-sitter/tree-sitter-regex.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-make"]
+	path = helix-syntax/languages/tree-sitter-make
+	url = https://github.com/alemuller/tree-sitter-make
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-config"]
+	path = helix-syntax/languages/tree-sitter-git-config
+	url = https://github.com/the-mikedavis/tree-sitter-git-config.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-graphql"]
+	path = helix-syntax/languages/tree-sitter-graphql
+	url = https://github.com/bkegley/tree-sitter-graphql
+  shallow = true
+[submodule "helix-syntax/languages/tree-sitter-elm"]
+	path = helix-syntax/languages/tree-sitter-elm
+	url = https://github.com/elm-tooling/tree-sitter-elm
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-iex"]
+	path = helix-syntax/languages/tree-sitter-iex
+	url = https://github.com/elixir-lang/tree-sitter-iex
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-twig"]
+	path = helix-syntax/languages/tree-sitter-twig
+	url = https://github.com/eirabben/tree-sitter-twig.git
+	shallow = true
+[submodule "helix-syntax/languages/tree-sitter-rescript"]
+	path = helix-syntax/languages/tree-sitter-rescript
+	url = https://github.com/jaredramirez/tree-sitter-rescript
+	shallow = true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52ca2d602..389279912 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,123 @@
 
+# 0.6.0 (2022-01-04)
+
+Happy new year and a big shout out to all the contributors! We had 55 contributors in this release.
+
+Helix has popped up in DPorts and Fedora Linux via COPR ([#1270](https://github.com/helix-editor/helix/pull/1270))
+
+As usual the following is a brief summary, refer to the git history for a full log:
+
+Breaking changes:
+
+- fix: Normalize backtab into shift-tab 
+
+Features:
+
+- Macros ([#1234](https://github.com/helix-editor/helix/pull/1234)) 
+- Add reverse search functionality ([#958](https://github.com/helix-editor/helix/pull/958))
+- Allow keys to be mapped to sequences of commands ([#589](https://github.com/helix-editor/helix/pull/589)) 
+- Make it possible to keybind TypableCommands ([#1169](https://github.com/helix-editor/helix/pull/1169))
+- Detect workspace root using language markers ([#1370](https://github.com/helix-editor/helix/pull/1370))
+- Add WORD textobject ([#991](https://github.com/helix-editor/helix/pull/991))
+- Add LSP rename_symbol (space-r) ([#1011](https://github.com/helix-editor/helix/pull/1011))
+- Added workspace_symbol_picker ([#1041](https://github.com/helix-editor/helix/pull/1041)) 
+- Detect filetype from shebang line ([#1001](https://github.com/helix-editor/helix/pull/1001))
+- Allow piping from stdin into a buffer on startup ([#996](https://github.com/helix-editor/helix/pull/996)) 
+- Add auto pairs for same-char pairs ([#1219](https://github.com/helix-editor/helix/pull/1219)) 
+- Update settings at runtime ([#798](https://github.com/helix-editor/helix/pull/798)) 
+- Enable thin LTO (cccc194)
+
+Commands:
+- :wonly -- window only ([#1057](https://github.com/helix-editor/helix/pull/1057)) 
+- buffer-close (:bc, :bclose) ([#1035](https://github.com/helix-editor/helix/pull/1035)) 
+- Add :<line> and :goto <line> commands ([#1128](https://github.com/helix-editor/helix/pull/1128))
+- :sort command ([#1288](https://github.com/helix-editor/helix/pull/1288)) 
+- Add m textobject for pair under cursor ([#961](https://github.com/helix-editor/helix/pull/961)) 
+- Implement "Goto next buffer / Goto previous buffer" commands ([#950](https://github.com/helix-editor/helix/pull/950))
+- Implement "Goto last modification" command ([#1067](https://github.com/helix-editor/helix/pull/1067)) 
+- Add trim_selections command ([#1092](https://github.com/helix-editor/helix/pull/1092)) 
+- Add movement shortcut for history ([#1088](https://github.com/helix-editor/helix/pull/1088))
+- Add command to inc/dec number under cursor ([#1027](https://github.com/helix-editor/helix/pull/1027))
+  - Add support for dates for increment/decrement
+- Align selections (&) ([#1101](https://github.com/helix-editor/helix/pull/1101))
+- Implement no-yank delete/change ([#1099](https://github.com/helix-editor/helix/pull/1099)) 
+- Implement black hole register ([#1165](https://github.com/helix-editor/helix/pull/1165)) 
+- gf as goto_file (gf) ([#1102](https://github.com/helix-editor/helix/pull/1102))
+- Add last modified file (gm) ([#1093](https://github.com/helix-editor/helix/pull/1093))
+- ensure_selections_forward ([#1393](https://github.com/helix-editor/helix/pull/1393))
+- Readline style insert mode ([#1039](https://github.com/helix-editor/helix/pull/1039))
+
+Usability improvements and fixes:
+
+- Detect filetype on :write ([#1141](https://github.com/helix-editor/helix/pull/1141))
+- Add single and double quotes to matching pairs ([#995](https://github.com/helix-editor/helix/pull/995)) 
+- Launch with defaults upon invalid config/theme (rather than panicking) ([#982](https://github.com/helix-editor/helix/pull/982))
+- If switching away from an empty scratch buffer, remove it ([#935](https://github.com/helix-editor/helix/pull/935)) 
+- Truncate the starts of file paths instead of the ends in picker ([#951](https://github.com/helix-editor/helix/pull/951))
+- Truncate the start of file paths in the StatusLine ([#1351](https://github.com/helix-editor/helix/pull/1351)) 
+- Prevent picker from previewing binaries or large file ([#939](https://github.com/helix-editor/helix/pull/939))
+- Inform when reaching undo/redo bounds ([#981](https://github.com/helix-editor/helix/pull/981))
+- search_impl will only align cursor center when it isn't in view ([#959](https://github.com/helix-editor/helix/pull/959)) 
+- Add <C-h>, <C-u>, <C-d>, Delete in prompt mode ([#1034](https://github.com/helix-editor/helix/pull/1034)) 
+- Restore screen position when aborting search ([#1047](https://github.com/helix-editor/helix/pull/1047)) 
+- Buffer picker: show is_modifier flag ([#1020](https://github.com/helix-editor/helix/pull/1020)) 
+- Add commit hash to version info, if present ([#957](https://github.com/helix-editor/helix/pull/957))
+- Implement indent-aware delete ([#1120](https://github.com/helix-editor/helix/pull/1120))
+- Jump to end char of surrounding pair from any cursor pos ([#1121](https://github.com/helix-editor/helix/pull/1121))
+- File picker configuration ([#988](https://github.com/helix-editor/helix/pull/988))
+- Fix surround cursor position calculation ([#1183](https://github.com/helix-editor/helix/pull/1183))
+- Accept count for goto_window ([#1033](https://github.com/helix-editor/helix/pull/1033))
+- Make kill_to_line_end behave like emacs ([#1235](https://github.com/helix-editor/helix/pull/1235)) 
+- Only use a single documentation popup ([#1241](https://github.com/helix-editor/helix/pull/1241)) 
+- ui: popup: Don't allow scrolling past the end of content (3307f44c)
+- Open files with spaces in filename, allow opening multiple files ([#1231](https://github.com/helix-editor/helix/pull/1231)) 
+- Allow paste commands to take a count ([#1261](https://github.com/helix-editor/helix/pull/1261))
+- Auto pairs selection ([#1254](https://github.com/helix-editor/helix/pull/1254)) 
+- Use a fuzzy matcher for commands ([#1386](https://github.com/helix-editor/helix/pull/1386)) 
+- Add c-s to pick word under doc cursor to prompt line & search completion ([#831](https://github.com/helix-editor/helix/pull/831))
+- Fix :earlier/:later missing changeset update ([#1069](https://github.com/helix-editor/helix/pull/1069))
+- Support extend for multiple goto ([#909](https://github.com/helix-editor/helix/pull/909))
+- Add arrow-key bindings for window switching ([#933](https://github.com/helix-editor/helix/pull/933))
+- Implement key ordering for info box ([#952](https://github.com/helix-editor/helix/pull/952))
+
+LSP:
+- Implement MarkedString rendering (e128a8702)
+- Don't panic if init fails (d31bef7)
+- Configurable diagnostic severity ([#1325](https://github.com/helix-editor/helix/pull/1325))
+- Resolve completion item ([#1315](https://github.com/helix-editor/helix/pull/1315))
+- Code action command support ([#1304](https://github.com/helix-editor/helix/pull/1304))
+
+Grammars:
+
+- Adds mint language server ([#974](https://github.com/helix-editor/helix/pull/974)) 
+- Perl ([#978](https://github.com/helix-editor/helix/pull/978)) ([#1280](https://github.com/helix-editor/helix/pull/1280))
+- GLSL ([#993](https://github.com/helix-editor/helix/pull/993)) 
+- Racket ([#1143](https://github.com/helix-editor/helix/pull/1143)) 
+- WGSL ([#1166](https://github.com/helix-editor/helix/pull/1166)) 
+- LLVM ([#1167](https://github.com/helix-editor/helix/pull/1167)) ([#1388](https://github.com/helix-editor/helix/pull/1388)) ([#1409](https://github.com/helix-editor/helix/pull/1409)) ([#1398](https://github.com/helix-editor/helix/pull/1398))
+- Markdown (49e06787)
+- Scala ([#1278](https://github.com/helix-editor/helix/pull/1278))
+- Dart ([#1250](https://github.com/helix-editor/helix/pull/1250))
+- Fish ([#1308](https://github.com/helix-editor/helix/pull/1308)) 
+- Dockerfile ([#1303](https://github.com/helix-editor/helix/pull/1303))
+- Git (commit, rebase, diff) ([#1338](https://github.com/helix-editor/helix/pull/1338)) ([#1402](https://github.com/helix-editor/helix/pull/1402)) ([#1373](https://github.com/helix-editor/helix/pull/1373))
+- tree-sitter-comment ([#1300](https://github.com/helix-editor/helix/pull/1300))
+- Highlight comments in c, cpp, cmake and llvm ([#1309](https://github.com/helix-editor/helix/pull/1309))
+- Improve yaml syntax highlighting highlighting ([#1294](https://github.com/helix-editor/helix/pull/1294)) 
+- Improve rust syntax highlighting ([#1295](https://github.com/helix-editor/helix/pull/1295))
+- Add textobjects and indents to cmake ([#1307](https://github.com/helix-editor/helix/pull/1307)) 
+- Add textobjects and indents to c and cpp ([#1293](https://github.com/helix-editor/helix/pull/1293))
+
+New themes:
+
+- Solarized dark ([#999](https://github.com/helix-editor/helix/pull/999)) 
+- Solarized light ([#1010](https://github.com/helix-editor/helix/pull/1010)) 
+- Spacebones light ([#1131](https://github.com/helix-editor/helix/pull/1131))
+- Monokai Pro ([#1206](https://github.com/helix-editor/helix/pull/1206)) 
+- Base16 Light and Terminal ([#1078](https://github.com/helix-editor/helix/pull/1078))
+  - and a default 16 color theme, truecolor detection 
+- Dracula ([#1258](https://github.com/helix-editor/helix/pull/1258))
+
 # 0.5.0 (2021-10-28)
 
 A big shout out to all the contributors! We had 46 contributors in this release.
diff --git a/Cargo.lock b/Cargo.lock
index 89c6388ee..4234c3b5b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.51"
+version = "1.0.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
+checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
 
 [[package]]
 name = "arc-swap"
@@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chardetng"
-version = "0.1.15"
+version = "0.1.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
+checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
 dependencies = [
  "cfg-if",
  "encoding_rs",
@@ -101,9 +101,9 @@ dependencies = [
 
 [[package]]
 name = "clipboard-win"
-version = "4.2.2"
+version = "4.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
+checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db"
 dependencies = [
  "error-code",
  "str-buf",
@@ -121,9 +121,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.5"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
+checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
 dependencies = [
  "cfg-if",
  "lazy_static",
@@ -131,16 +131,16 @@ dependencies = [
 
 [[package]]
 name = "crossterm"
-version = "0.22.1"
+version = "0.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
+checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432"
 dependencies = [
  "bitflags",
  "crossterm_winapi",
  "futures-core",
  "libc",
  "mio",
- "parking_lot",
+ "parking_lot 0.12.0",
  "signal-hook",
  "signal-hook-mio",
  "winapi",
@@ -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",
 ]
@@ -202,9 +202,9 @@ dependencies = [
 
 [[package]]
 name = "error-code"
-version = "2.3.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff"
+checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
 dependencies = [
  "libc",
  "str-buf",
@@ -246,27 +246,17 @@ dependencies = [
  "percent-encoding",
 ]
 
-[[package]]
-name = "futf"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
-dependencies = [
- "mac",
- "new_debug_unreachable",
-]
-
 [[package]]
 name = "futures-core"
-version = "0.3.18"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.18"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
+checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -275,15 +265,15 @@ dependencies = [
 
 [[package]]
 name = "futures-task"
-version = "0.3.18"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
+checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
 
 [[package]]
 name = "futures-util"
-version = "0.3.18"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
+checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -303,9 +293,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
 dependencies = [
  "cfg-if",
  "libc",
@@ -366,9 +356,11 @@ dependencies = [
 
 [[package]]
 name = "helix-core"
-version = "0.5.0"
+version = "0.6.0"
 dependencies = [
  "arc-swap",
+ "chrono",
+ "encoding_rs",
  "etcetera",
  "helix-syntax",
  "log",
@@ -379,8 +371,9 @@ dependencies = [
  "serde",
  "serde_json",
  "similar",
+ "slotmap",
  "smallvec",
- "tendril",
+ "smartstring",
  "toml",
  "tree-sitter",
  "unicode-general-category",
@@ -390,7 +383,7 @@ dependencies = [
 
 [[package]]
 name = "helix-dap"
-version = "0.5.0"
+version = "0.6.0"
 dependencies = [
  "anyhow",
  "fern",
@@ -404,7 +397,7 @@ dependencies = [
 
 [[package]]
 name = "helix-lsp"
-version = "0.5.0"
+version = "0.6.0"
 dependencies = [
  "anyhow",
  "futures-executor",
@@ -422,7 +415,7 @@ dependencies = [
 
 [[package]]
 name = "helix-syntax"
-version = "0.5.0"
+version = "0.6.0"
 dependencies = [
  "anyhow",
  "cc",
@@ -433,7 +426,7 @@ dependencies = [
 
 [[package]]
 name = "helix-term"
-version = "0.5.0"
+version = "0.6.0"
 dependencies = [
  "anyhow",
  "chrono",
@@ -465,7 +458,7 @@ dependencies = [
 
 [[package]]
 name = "helix-tui"
-version = "0.5.0"
+version = "0.6.0"
 dependencies = [
  "bitflags",
  "cassowary",
@@ -478,14 +471,13 @@ dependencies = [
 
 [[package]]
 name = "helix-view"
-version = "0.5.0"
+version = "0.6.0"
 dependencies = [
  "anyhow",
  "bitflags",
  "chardetng",
  "clipboard-win",
  "crossterm",
- "encoding_rs",
  "futures-util",
  "helix-core",
  "helix-dap",
@@ -551,9 +543,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"
@@ -576,15 +568,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.104"
+version = "0.2.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
+checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
 
 [[package]]
 name = "libloading"
-version = "0.7.2"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
+checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
 dependencies = [
  "cfg-if",
  "winapi",
@@ -592,9 +584,9 @@ dependencies = [
 
 [[package]]
 name = "lock_api"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
 dependencies = [
  "scopeguard",
 ]
@@ -610,9 +602,9 @@ dependencies = [
 
 [[package]]
 name = "lsp-types"
-version = "0.91.1"
+version = "0.92.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
+checksum = "e8a69d4142d51b208c9fc3cea68b1a7fcef30354e7aa6ccad07250fd8430fc76"
 dependencies = [
  "bitflags",
  "serde",
@@ -621,12 +613,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "mac"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
-
 [[package]]
 name = "matches"
 version = "0.1.9"
@@ -670,12 +656,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "new_debug_unreachable"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
-
 [[package]]
 name = "ntapi"
 version = "0.3.6"
@@ -706,9 +686,9 @@ dependencies = [
 
 [[package]]
 name = "num_cpus"
-version = "1.13.0"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
 dependencies = [
  "hermit-abi",
  "libc",
@@ -716,9 +696,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
 
 [[package]]
 name = "parking_lot"
@@ -728,7 +708,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
 dependencies = [
  "instant",
  "lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.8.5",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.1",
 ]
 
 [[package]]
@@ -745,6 +735,19 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "parking_lot_core"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.1.0"
@@ -753,9 +756,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.7"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
 
 [[package]]
 name = "pin-utils"
@@ -765,18 +768,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.30"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
 dependencies = [
  "unicode-xid",
 ]
 
 [[package]]
 name = "pulldown-cmark"
-version = "0.8.0"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8"
+checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6"
 dependencies = [
  "bitflags",
  "memchr",
@@ -794,9 +797,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.10"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
 dependencies = [
  "proc-macro2",
 ]
@@ -863,18 +866,18 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
 
 [[package]]
 name = "ropey"
-version = "1.3.1"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28"
+checksum = "e6b9aa65bcd9f308d37c7158b4a1afaaa32b8450213e20c9b98e7d5b3cc2fec3"
 dependencies = [
  "smallvec",
 ]
 
 [[package]]
 name = "ryu"
-version = "1.0.5"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
 
 [[package]]
 name = "same-file"
@@ -893,18 +896,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "serde"
-version = "1.0.130"
+version = "1.0.136"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.130"
+version = "1.0.136"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -913,9 +916,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.72"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
+checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
 dependencies = [
  "itoa",
  "ryu",
@@ -935,9 +938,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook"
-version = "0.3.10"
+version = "0.3.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
+checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
 dependencies = [
  "libc",
  "signal-hook-registry",
@@ -965,9 +968,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook-tokio"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8"
+checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e"
 dependencies = [
  "futures-core",
  "libc",
@@ -998,9 +1001,24 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "smartstring"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31aa6a31c0c2b21327ce875f7e8952322acfcfd0c27569a6e18a647281352c9b"
+dependencies = [
+ "static_assertions",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
 name = "str-buf"
@@ -1010,26 +1028,15 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
 
 [[package]]
 name = "syn"
-version = "1.0.80"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
+checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
 dependencies = [
  "proc-macro2",
  "quote",
  "unicode-xid",
 ]
 
-[[package]]
-name = "tendril"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33"
-dependencies = [
- "futf",
- "mac",
- "utf-8",
-]
-
 [[package]]
 name = "thiserror"
 version = "1.0.30"
@@ -1052,9 +1059,9 @@ dependencies = [
 
 [[package]]
 name = "thread_local"
-version = "1.1.3"
+version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
 dependencies = [
  "once_cell",
 ]
@@ -1070,9 +1077,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -1085,18 +1092,17 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.14.0"
+version = "1.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
+checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
 dependencies = [
- "autocfg",
  "bytes",
  "libc",
  "memchr",
  "mio",
  "num_cpus",
  "once_cell",
- "parking_lot",
+ "parking_lot 0.11.2",
  "pin-project-lite",
  "signal-hook-registry",
  "tokio-macros",
@@ -1105,9 +1111,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "1.6.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
+checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1136,9 +1142,9 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter"
-version = "0.20.1"
+version = "0.20.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1"
+checksum = "4e34327f8eac545e3f037382471b2b19367725a242bba7bc45edb9efb49fe39a"
 dependencies = [
  "cc",
  "regex",
@@ -1161,9 +1167,9 @@ checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
 
 [[package]]
 name = "unicode-general-category"
-version = "0.4.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
+checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6"
 
 [[package]]
 name = "unicode-normalization"
@@ -1176,9 +1182,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-segmentation"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
 
 [[package]]
 name = "unicode-width"
@@ -1205,17 +1211,11 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
 [[package]]
 name = "version_check"
-version = "0.9.3"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
 [[package]]
 name = "walkdir"
@@ -1236,9 +1236,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
 
 [[package]]
 name = "which"
-version = "4.2.2"
+version = "4.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9"
+checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2"
 dependencies = [
  "either",
  "lazy_static",
@@ -1275,3 +1275,55 @@ 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 = "windows-sys"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
+
+[[package]]
+name = "xtask"
+version = "0.6.0"
+dependencies = [
+ "helix-core",
+ "helix-term",
+ "toml",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 6c360ffda..36dcb09f3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
   "helix-syntax",
   "helix-lsp",
   "helix-dap",
+  "xtask",
 ]
 
 # Build helix-syntax in release mode to make the code path faster in development.
@@ -18,3 +19,4 @@ split-debuginfo = "unpacked"
 
 [profile.release]
 lto = "thin"
+# debug = true
diff --git a/README.md b/README.md
index 3f4087b9f..71010cc82 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/TODO.md b/TODO.md
index 80a9be05e..ab94cf9a0 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,25 +1,12 @@
 
-- tree sitter:
-  - markdown
-  - regex
-  - kotlin
-  - clojure
-  - erlang
-
 - [ ] completion isIncomplete support
-
-1
 - [ ] respect view fullscreen flag
 - [ ] Implement marks (superset of Selection/Range)
 
 - [ ] = for auto indent line/selection
-- [ ]  :x for closing buffers
 - [ ] lsp: signature help
 
 2
-- [ ] macro recording
-- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
-- [ ] selection align
 - [ ] store some state between restarts: file positions, prompt history
 - [ ] highlight matched characters in picker
 
diff --git a/base16_theme.toml b/base16_theme.toml
new file mode 100644
index 000000000..42e02a98a
--- /dev/null
+++ b/base16_theme.toml
@@ -0,0 +1,51 @@
+# 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" = { fg = "white", 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" }
+
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "green"
+"diff.delta" = "yellow"
+"diff.minus" = "red"
+
+"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 8cadb663f..a8f165c01 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 000000000..4c4a5c05c
--- /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 2ed48d51f..8048f5484 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -5,9 +5,27 @@ To override global configuration parameters, create a `config.toml` file located
 * Linux and Mac: `~/.config/helix/config.toml`
 * Windows: `%AppData%\helix\config.toml`
 
+Example config:
+
+```toml
+theme = "onedark"
+
+[editor]
+line-number = "relative"
+mouse = false
+
+[editor.cursor-shape]
+insert = "bar"
+normal = "block"
+select = "underline"
+
+[editor.file-picker]
+hidden = false
+```
+
 ## Editor
 
-`[editor]` section of the config.
+### `[editor]` Section
 
 | Key | Description | Default |
 |--|--|---------|
@@ -16,15 +34,37 @@ To override global configuration parameters, create a `config.toml` file located
 | `middle-click-paste` | Middle click paste support. | `true` |
 | `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
 | `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
-| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
+| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
 | `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
 | `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
 | `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
 | `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.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default.
+### `[editor.cursor-shape]` Section
+
+Defines the shape of cursor in each mode. Note that due to limitations
+of the terminal environment, only the primary cursor can change shape.
+
+| Key      | Description                                | Default |
+| ---      | -----------                                | ------- |
+| `normal` | Cursor shape in [normal mode][normal mode] | `block` |
+| `insert` | Cursor shape in [insert mode][insert mode] | `block` |
+| `select` | Cursor shape in [select mode][select mode] | `block` |
+
+[normal mode]: ./keymap.md#normal-mode
+[insert mode]: ./keymap.md#insert-mode
+[select mode]: ./keymap.md#select--extend-mode
+
+### `[editor.file-picker]` Section
+
+Sets options for file picker and global search. All but the last key listed in
+the default file-picker configuration below are IgnoreOptions: whether hidden
+files and files listed within ignore files are ignored by (not visible in) the
+helix file picker and global search. There is also one other key, `max-depth`
+available, which is not defined by default.
 
 | Key | Description | Default |
 |--|--|---------|
diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md
new file mode 100644
index 000000000..64dab6d33
--- /dev/null
+++ b/book/src/generated/lang-support.md
@@ -0,0 +1,63 @@
+| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
+| --- | --- | --- | --- | --- |
+| bash | ✓ |  |  | `bash-language-server` |
+| c | ✓ | ✓ | ✓ | `clangd` |
+| c-sharp | ✓ |  |  |  |
+| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
+| comment | ✓ |  |  |  |
+| cpp | ✓ | ✓ | ✓ | `clangd` |
+| css | ✓ |  |  |  |
+| dart | ✓ |  | ✓ | `dart` |
+| dockerfile | ✓ |  |  | `docker-langserver` |
+| elixir | ✓ |  |  | `elixir-ls` |
+| elm | ✓ |  |  | `elm-language-server` |
+| fish | ✓ | ✓ | ✓ |  |
+| git-commit | ✓ |  |  |  |
+| git-config | ✓ |  |  |  |
+| git-diff | ✓ |  |  |  |
+| git-rebase | ✓ |  |  |  |
+| glsl | ✓ |  | ✓ |  |
+| go | ✓ | ✓ | ✓ | `gopls` |
+| graphql | ✓ |  |  |  |
+| haskell | ✓ |  |  | `haskell-language-server-wrapper` |
+| html | ✓ |  |  |  |
+| iex | ✓ |  |  |  |
+| java | ✓ |  |  |  |
+| javascript | ✓ |  | ✓ | `typescript-language-server` |
+| json | ✓ |  | ✓ |  |
+| julia | ✓ |  |  | `julia` |
+| latex | ✓ |  |  |  |
+| lean | ✓ |  |  | `lean` |
+| ledger | ✓ |  |  |  |
+| llvm | ✓ | ✓ | ✓ |  |
+| llvm-mir | ✓ | ✓ | ✓ |  |
+| llvm-mir-yaml | ✓ |  | ✓ |  |
+| lua | ✓ |  | ✓ |  |
+| make | ✓ |  |  |  |
+| markdown | ✓ |  |  |  |
+| mint |  |  |  | `mint` |
+| nix | ✓ |  | ✓ | `rnix-lsp` |
+| ocaml | ✓ |  | ✓ |  |
+| ocaml-interface | ✓ |  |  |  |
+| perl | ✓ | ✓ | ✓ |  |
+| php | ✓ | ✓ | ✓ |  |
+| prolog |  |  |  | `swipl` |
+| protobuf | ✓ |  | ✓ |  |
+| python | ✓ | ✓ | ✓ | `pylsp` |
+| racket |  |  |  | `racket` |
+| regex | ✓ |  |  |  |
+| rescript | ✓ | ✓ |  | `rescript-language-server` |
+| ruby | ✓ |  | ✓ | `solargraph` |
+| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
+| scala | ✓ |  | ✓ | `metals` |
+| svelte | ✓ |  | ✓ | `svelteserver` |
+| tablegen | ✓ | ✓ | ✓ |  |
+| toml | ✓ |  |  |  |
+| tsq | ✓ |  |  |  |
+| tsx | ✓ |  |  | `typescript-language-server` |
+| twig | ✓ |  |  |  |
+| 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 000000000..aed75cbd1
--- /dev/null
+++ b/book/src/generated/typable-cmd.md
@@ -0,0 +1,48 @@
+| 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). |
+| `:cquit!`, `:cq!` | Quit with exit code (default 1) forcefully (ignoring unsaved changes). 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. |
+| `:set-option`, `:set` | Set a config option at runtime |
+| `:sort` | Sort ranges in selection. |
+| `:rsort` | Sort ranges in selection in reverse order. |
+| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md
index 446eb479d..5844a48ee 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>
@@ -27,22 +27,32 @@ directory](../configuration.md).
 
 These are the available keys and descriptions for the file.
 
-| Key           | Description                                                   |
-| ----          | -----------                                                   |
-| name          | The name of the language                                      |
-| scope         | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
-| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
-| file-types    | The filetypes of the language, for example `["yml", "yaml"]`  |
-| shebangs      | The interpreters from the shebang line, for example `["sh", "bash"]` |
-| roots         | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
-| auto-format   | Whether to autoformat this language when saving               |
-| comment-token | The token to use as a comment-token                           |
-| indent        | The indent to use. Has sub keys `tab-width` and `unit`        |
-| config        | Language server configuration                                 |
+| Key                 | Description                                                   |
+| ----                | -----------                                                   |
+| name                | The name of the language                                      |
+| scope               | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
+| injection-regex     | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
+| file-types          | The filetypes of the language, for example `["yml", "yaml"]`  |
+| shebangs            | The interpreters from the shebang line, for example `["sh", "bash"]` |
+| roots               | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
+| auto-format         | Whether to autoformat this language when saving               |
+| diagnostic-severity | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
+| comment-token       | The token to use as a comment-token                           |
+| indent              | The indent to use. Has sub keys `tab-width` and `unit`        |
+| config              | Language server configuration                                 |
 
 ## Queries
 
-For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries/<name>/`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries.
+For a language to have syntax-highlighting and indentation among
+other things, you have to add queries. Add a directory for your
+language with the path `runtime/queries/<name>/`. The tree-sitter
+[website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
+gives more info on how to write queries.
+
+> NOTE: When evaluating queries, the first matching query takes
+precedence, which is different from other editors like neovim where
+the last matching query supercedes the ones before it. See
+[this issue][neovim-query-precedence] for an example.
 
 ## Common Issues
 
@@ -58,3 +68,4 @@ For a language to have syntax-highlighting and indentation among other things, y
 
 [treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
 [languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
+[neovim-query-precedence]: https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090
diff --git a/book/src/install.md b/book/src/install.md
index d831934c4..1a5a9daa9 100644
--- a/book/src/install.md
+++ b/book/src/install.md
@@ -27,6 +27,15 @@ Releases are available in the `community` repository.
 
 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 865a700b4..19fd21bbc 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -25,7 +25,9 @@
 | `f`         | Find next char                                     | `find_next_char`            |
 | `T`         | Find 'till previous char                           | `till_prev_char`            |
 | `F`         | Find previous char                                 | `find_prev_char`            |
+| `G`         | Go to line number `<n>`                            | `goto_line`                 |
 | `Alt-.`     | Repeat last motion (`f`, `t` or `m`)               | `repeat_last_motion`        |
+| `Alt-:`     | Ensures the selection is in forward direction      | `ensure_selections_forward` |
 | `Home`      | Move to the start of the line                      | `goto_line_start`           |
 | `End`       | Move to the end of the line                        | `goto_line_end`             |
 | `PageUp`    | Move page up                                       | `page_up`                   |
@@ -34,6 +36,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,37 +48,39 @@
 
 ### 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`        |
-| `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`               |
+| 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 (experimental)   | `record_macro`            |
+| `q`         | Play back a recorded macro from the selected register (experimental) | `replay_macro`            |
 
 #### Shell
 
@@ -85,6 +90,7 @@
 | <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` |
+| `$`     | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe`     |
 
 
 ### Selection manipulation
@@ -109,12 +115,14 @@
 | `%`      | Select entire file                                                | `select_all`                         |
 | `x`      | Select current line, if already selected, extend to next line     | `extend_line`                        |
 | `X`      | Extend selection to line bounds (line-wise selection)             | `extend_to_line_bounds`              |
-|          | Expand selection to parent syntax node TODO: pick a key (**TS**)  | `expand_selection`                   |
 | `J`      | Join lines inside selection                                       | `join_selections`                    |
 | `K`      | Keep selections matching the regex                                | `keep_selections`                    |
 | `Alt-K`  | Remove selections matching the regex                              | `remove_selections`                  |
-| `$`      | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe`     |
 | `Ctrl-c` | Comment/uncomment the selections                                  | `toggle_comments`                    |
+| `Alt-k`  | Expand selection to parent syntax node (**TS**)                   | `expand_selection`                   |
+| `Alt-j`  | Shrink syntax tree object selection (**TS**)                      | `shrink_selection`                   |
+| `Alt-h`  | Select previous sibling node in syntax tree (**TS**)              | `select_prev_sibling`                |
+| `Alt-l`  | Select next sibling node in syntax tree (**TS**)                  | `select_next_sibling`                |
 
 ### Search
 
@@ -147,10 +155,10 @@ over text and not actively editing it).
 | `m`           | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
 | `j` , `down`  | Scroll the view downwards                                 | `scroll_down`       |
 | `k` , `up`    | Scroll the view upwards                                   | `scroll_up`         |
-| `f`           | Move page down                                            | `page_down`         |
-| `b`           | Move page up                                              | `page_up`           |
-| `d`           | Move half page down                                       | `half_page_down`    |
-| `u`           | Move half page up                                         | `half_page_up`      |
+| `Ctrl-f`      | Move page down                                            | `page_down`         |
+| `Ctrl-b`      | Move page up                                              | `page_up`           |
+| `Ctrl-d`      | Move half page down                                       | `half_page_down`    |
+| `Ctrl-u`      | Move half page up                                         | `half_page_up`      |
 
 #### Goto mode
 
@@ -158,20 +166,21 @@ Jumps to various locations.
 
 | Key   | Description                                      | Command                    |
 | ----- | -----------                                      | -------                    |
-| `g`   | Go to the start of the file                      | `goto_file_start`          |
+| `g`   | Go to line number `<n>` else start of 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`   |
diff --git a/book/src/lang-support.md b/book/src/lang-support.md
new file mode 100644
index 000000000..3920f3424
--- /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/languages.md b/book/src/languages.md
index cef61501f..4c4dc326d 100644
--- a/book/src/languages.md
+++ b/book/src/languages.md
@@ -11,4 +11,3 @@ Changes made to the `languages.toml` file in a user's [configuration directory](
 name = "rust"
 auto-format = false
 ```
-
diff --git a/book/src/remapping.md b/book/src/remapping.md
index fffd189b7..1cdf9b1f2 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 fd3f5b1eb..9abcfe8c1 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -1,14 +1,14 @@
 # Themes
 
-First you'll need to place selected themes in your `themes` directory (i.e `~/.config/helix/themes`), the directory might have to be created beforehand.
-
-To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
-
-The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes). 
+To use a theme add `theme = "<name>"` to your [`config.toml`](./configuration.md) at the very top of the file before the first section or select it during runtime using `:theme <name>`.
 
 ## Creating a theme
 
-First create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`).
+Create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`). The directory might have to be created beforehand.
+
+The names "default" and "base16_default" are reserved for the builtin themes and cannot be overridden by user defined themes.
+
+The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes). 
 
 Each line in the theme file is specified as below:
 
@@ -105,6 +105,7 @@ We use a similar set of scopes as
 
 - `type` - Types
   - `builtin` - Primitive types provided by the language (`int`, `usize`)
+- `constructor`
 
 - `constant` (TODO: constant.other.placeholder for %v)
   - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
@@ -146,6 +147,7 @@ We use a similar set of scopes as
     - `repeat` - `for`, `while`, `loop`
     - `import` - `import`, `export`
     - `return`
+    - `exception`
   - `operator` - `or`, `in`
   - `directive` - Preprocessor directives (`#if` in C) 
   - `function` - `fn`, `func`
@@ -162,10 +164,44 @@ We use a similar set of scopes as
 
 - `namespace`
 
+- `markup`
+  - `heading`
+  - `list`
+    - `unnumbered`
+    - `numbered`
+  - `bold`
+  - `italic`
+  - `link`
+    - `url` - urls pointed to by links
+    - `label` - non-url link references
+    - `text` - url and image descriptions in links
+  - `quote`
+  - `raw`
+    - `inline`
+    - `block`
+
+- `diff` - version control changes
+  - `plus` - additions
+  - `minus` - deletions
+  - `delta` - modifications
+    - `moved` - renamed or moved files/changes
+
 #### Interface
 
 These scopes are used for theming the editor interface.
 
+- `markup`
+  - `normal`
+    - `completion` - for completion doc popup ui
+    - `hover` - for hover popup ui
+  - `heading`
+    - `completion` - for completion doc popup ui
+    - `hover` - for hover popup ui
+  - `raw`
+    - `inline`
+      - `completion` - for completion doc popup ui
+      - `hover` - for hover popup ui
+
 
 | Key                      | Notes                               |
 | ---                      | ---                                 |
diff --git a/book/src/usage.md b/book/src/usage.md
index cf7d9d488..a76bfafcc 100644
--- a/book/src/usage.md
+++ b/book/src/usage.md
@@ -42,7 +42,7 @@ helix. The keymappings have been inspired from [vim-sandwich](https://github.com
 `ms` acts on a selection, so select the text first and use `ms<char>`. `mr` and `md` work
 on the closest pairs found and selections are not required; use counts to act in outer pairs.
 
-It can also act on multiple seletions (yay!). For example, to change every occurance of `(use)` to `[use]`:
+It can also act on multiple selections (yay!). For example, to change every occurrence of `(use)` to `[use]`:
 
 - `%` to select the whole file
 - `s` to split the selections on a search term
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 000000000..bdd771aaf
--- /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 db0fface5..94e443e3a 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "devshell": {
       "locked": {
-        "lastModified": 1632436039,
-        "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
+        "lastModified": 1641980203,
+        "narHash": "sha256-RiWJ3+6V267Ji+P54K1Xrj1Nsah9BfG/aLfIhqgVyBY=",
         "owner": "numtide",
         "repo": "devshell",
-        "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
+        "rev": "d897c1ddb4eab66cc2b783c7868d78555b9880ad",
         "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": {
@@ -41,11 +41,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1634796585,
-        "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
+        "lastModified": 1642054253,
+        "narHash": "sha256-kHh9VmaB7gbS6pheheC4x0uT84LEmhfbsbWEQJgU2E4=",
         "owner": "yusdacra",
         "repo": "nix-cargo-integration",
-        "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
+        "rev": "f8fa9af990195a3f63fe2dde84aa187e193da793",
         "type": "github"
       },
       "original": {
@@ -56,11 +56,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1634782485,
-        "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
+        "lastModified": 1641887635,
+        "narHash": "sha256-kDGpufwzVaiGe5e1sBUBPo9f1YN+nYHJlYqCaVpZTQQ=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
+        "rev": "b2737d4980a17cc2b7d600d7d0b32fd7333aca88",
         "type": "github"
       },
       "original": {
@@ -72,15 +72,16 @@
     },
     "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"
       }
@@ -98,11 +99,11 @@
         "nixpkgs": "nixpkgs_2"
       },
       "locked": {
-        "lastModified": 1634869268,
-        "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
+        "lastModified": 1642128126,
+        "narHash": "sha256-av8JUACdrTfQYl/ftZJvKpZEmZfa0avCq7tt5Usdoq0=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "c02c2d86354327317546501af001886fbb53d374",
+        "rev": "ce4ef6f2d74f2b68f7547df1de22d1b0037ce4ad",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index cbf10c975..0d22c5c1a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,50 +20,63 @@
       # Set default package to helix-term release build
       defaultOutputs = { app = "hx"; package = "helix"; };
       overrides = {
-        crateOverrides = common: _: {
-          helix-term = prev: {
-            # link languages and theme toml files since helix-term expects them (for tests)
-            preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
-            buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
-          };
+        crateOverrides = common: _: rec {
           # link languages and theme toml files since helix-view expects them
-          helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
-          helix-syntax = _prev: {
+          helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml,base16_theme.toml} .."; };
+          helix-syntax = prev: {
+            src =
+              let
+                pkgs = common.pkgs;
+                helix = pkgs.fetchgit {
+                  url = "https://github.com/helix-editor/helix.git";
+                  rev = "a8fd33ac012a79069ef1409503a2edcf3a585153";
+                  fetchSubmodules = true;
+                  sha256 = "sha256-5AtOC55ttWT+7RYMboaFxpGZML51ix93wAkYJTt+8JI=";
+                };
+              in
+              pkgs.runCommand prev.src.name { } ''
+                mkdir -p $out
+                ln -s ${prev.src}/* $out
+                ln -sf ${helix}/helix-syntax/languages $out
+              '';
             preConfigure = "mkdir -p ../runtime/grammars";
             postInstall = "cp -r ../runtime $out/runtime";
           };
-        };
-        mainBuild = common: prev:
-          let
-            inherit (common) pkgs lib;
-            helixSyntax = lib.buildCrate {
-              root = self;
-              memberName = "helix-syntax";
-              defaultCrateOverrides = {
-                helix-syntax = common.crateOverrides.helix-syntax;
+          helix-term = prev:
+            let
+              inherit (common) pkgs lib;
+              helixSyntax = lib.buildCrate {
+                root = self;
+                memberName = "helix-syntax";
+                defaultCrateOverrides = {
+                  helix-syntax = helix-syntax;
+                };
+                release = false;
               };
-              release = false;
+              runtimeDir = pkgs.runCommand "helix-runtime" { } ''
+                mkdir -p $out
+                ln -s ${common.root}/runtime/* $out
+                ln -sf ${helixSyntax}/runtime/grammars $out
+              '';
+            in
+            {
+              # link languages and theme toml files since helix-term expects them (for tests)
+              preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml,base16_theme.toml} ..";
+              buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
+              nativeBuildInputs = [ pkgs.makeWrapper ];
+              postFixup = ''
+                if [ -f "$out/bin/hx" ]; then
+                  wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
+                fi
+              '';
             };
-            runtimeDir = pkgs.runCommand "helix-runtime" { } ''
-              mkdir -p $out
-              ln -s ${common.root}/runtime/* $out
-              ln -sf ${helixSyntax}/runtime/grammars $out
-            '';
-          in
-          lib.optionalAttrs (common.memberName == "helix-term") {
-            nativeBuildInputs = [ pkgs.makeWrapper ];
-            postFixup = ''
-              if [ -f "$out/bin/hx" ]; then
-                wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
-              fi
-            '';
-          };
+        };
         shell = common: prev: {
-          packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]);
+          packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin cargo-flamegraph ]);
           env = prev.env ++ [
             { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
             { name = "RUST_BACKTRACE"; value = "1"; }
-            { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
+            { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"; }
           ];
         };
       };
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index ea695d34a..7ff91cfda 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "helix-core"
-version = "0.5.0"
+version = "0.6.0"
 authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
 edition = "2021"
 license = "MPL-2.0"
@@ -13,17 +13,18 @@ include = ["src/**/*", "README.md"]
 [features]
 
 [dependencies]
-helix-syntax = { version = "0.5", path = "../helix-syntax" }
+helix-syntax = { version = "0.6", path = "../helix-syntax" }
 
 ropey = "1.3"
-smallvec = "1.7"
-tendril = "0.4.2"
-unicode-segmentation = "1.8"
+smallvec = "1.8"
+smartstring = "0.2.9"
+unicode-segmentation = "1.9"
 unicode-width = "0.1"
-unicode-general-category = "0.4"
+unicode-general-category = "0.5"
 # slab = "0.4.2"
+slotmap = "1.0"
 tree-sitter = "0.20"
-once_cell = "1.8"
+once_cell = "1.9"
 arc-swap = "1"
 regex = "1"
 
@@ -35,6 +36,9 @@ toml = "0.5"
 similar = "2.1"
 
 etcetera = "0.3"
+encoding_rs = "0.8"
+
+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 cc9668529..f4359a342 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,7 +1,10 @@
 //! When typing the opening character of one of the possible pairs defined below,
 //! this module provides the functionality to insert the paired closing character.
 
-use crate::{Range, Rope, Selection, Tendril, Transaction};
+use crate::{
+    graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
+};
+use log::debug;
 use smallvec::SmallVec;
 
 // Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +18,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,14 +30,19 @@ 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 (|) -> |
+// * 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);
+
     for &(open, close) in PAIRS {
         if open == ch {
             if open == close {
-                return handle_same(doc, selection, open);
+                return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
             } else {
                 return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
             }
@@ -47,18 +57,145 @@ pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction>
     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 is_single_grapheme(doc: &Rope, range: &Range) -> bool {
+    let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to()));
+    let first = graphemes.next();
+    let second = graphemes.next();
+    debug!("first: {:#?}, second: {:#?}", first, second);
+    first.is_some() && second.is_none()
+}
+
+/// calculate what the resulting range should be for an auto pair insertion
+fn get_next_range(
+    doc: &Rope,
+    start_range: &Range,
+    offset: usize,
+    typed_char: char,
+    len_inserted: usize,
+) -> Range {
+    // When the character under the cursor changes due to complete pair
+    // insertion, we must look backward a grapheme and then add the length
+    // of the insertion to put the resulting cursor in the right place, e.g.
+    //
+    // foo[\r\n] - anchor: 3, head: 5
+    // foo([)]\r\n - anchor: 4, head: 5
+    //
+    // foo[\r\n] - anchor: 3, head: 5
+    // foo'[\r\n] - anchor: 4, head: 6
+    //
+    // foo([)]\r\n - anchor: 4, head: 5
+    // foo()[\r\n] - anchor: 5, head: 7
+    //
+    // [foo]\r\n - anchor: 0, head: 3
+    // [foo(])\r\n - anchor: 0, head: 5
+
+    // inserting at the very end of the document after the last newline
+    if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
+        return Range::new(
+            start_range.anchor + offset + typed_char.len_utf8(),
+            start_range.head + offset + typed_char.len_utf8(),
+        );
+    }
+
+    let single_grapheme = is_single_grapheme(doc, start_range);
+    let doc_slice = doc.slice(..);
+
+    // just skip over graphemes
+    if len_inserted == 0 {
+        let end_anchor = if single_grapheme {
+            graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
+
+        // even for backward inserts with multiple grapheme selections,
+        // we want the anchor to stay where it is so that the relative
+        // selection does not change, e.g.:
+        //
+        // foo([) wor]d -> insert ) -> foo()[ wor]d
+        } else {
+            start_range.anchor + offset
+        };
+
+        return Range::new(
+            end_anchor,
+            graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
+        );
+    }
+
+    // trivial case: only inserted a single-char opener, just move the selection
+    if len_inserted == 1 {
+        let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
+            start_range.anchor + offset + typed_char.len_utf8()
+        } else {
+            start_range.anchor + offset
+        };
+
+        return Range::new(
+            end_anchor,
+            start_range.head + offset + typed_char.len_utf8(),
+        );
+    }
+
+    // If the head = 0, then we must be in insert mode with a backward
+    // cursor, which implies the head will just move
+    let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
+        start_range.head + offset + typed_char.len_utf8()
+    } else {
+        // We must have a forward cursor, which means we must move to the
+        // other end of the grapheme to get to where the new characters
+        // are inserted, then move the head to where it should be
+        let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
+        debug!(
+            "prev_bound: {}, offset: {}, len_inserted: {}",
+            prev_bound, offset, len_inserted
+        );
+        prev_bound + offset + len_inserted
+    };
+
+    let end_anchor = match (start_range.len(), start_range.direction()) {
+        // if we have a zero width cursor, it shifts to the same number
+        (0, _) => end_head,
+
+        // If we are inserting for a regular one-width cursor, the anchor
+        // moves with the head. This is the fast path for ASCII.
+        (1, Direction::Forward) => end_head - 1,
+        (1, Direction::Backward) => end_head + 1,
+
+        (_, Direction::Forward) => {
+            if single_grapheme {
+                graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head)
+                    + typed_char.len_utf8()
+
+            // if we are appending, the anchor stays where it is; only offset
+            // for multiple range insertions
+            } else {
+                start_range.anchor + offset
+            }
+        }
+
+        (_, Direction::Backward) => {
+            if single_grapheme {
+                // if we're backward, then the head is at the first char
+                // of the typed char, so we need to add the length of
+                // the closing char
+                graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
+            } else {
+                // when we are inserting in front of a selection, we need to move
+                // the anchor over by however many characters were inserted overall
+                start_range.anchor + offset + len_inserted
+            }
+        }
+    };
+
+    Range::new(end_anchor, end_head)
+}
+
 fn handle_open(
     doc: &Rope,
     selection: &Selection,
@@ -66,98 +203,584 @@ 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 cursor = start_range.cursor(doc.slice(..));
+        let next_char = doc.get_char(cursor);
+        let len_inserted;
 
-        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,
-        ));
-
-        match next {
+        let change = match next_char {
             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)))
+                len_inserted = open.len_utf8();
+                let mut tendril = Tendril::new();
+                tendril.push(open);
+                (cursor, cursor, Some(tendril))
             }
             // 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]);
+                len_inserted = open.len_utf8() + close.len_utf8();
+                (cursor, cursor, Some(pair))
             }
-        }
+        };
+
+        let next_range = get_next_range(doc, start_range, offs, open, len_inserted);
+        end_ranges.push(next_range);
+        offs += len_inserted;
+
+        change
     });
 
-    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 cursor = start_range.cursor(doc.slice(..));
+        let next_char = doc.get_char(cursor);
+        let mut len_inserted = 0;
 
-        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,
-        ));
-
-        if next == Some(close) {
-            //  return transaction that moves past close
-            (pos, pos, None) // no-op
+        let change = if next_char == Some(close) {
+            // return transaction that moves past close
+            (cursor, cursor, None) // no-op
         } else {
-            offs += close.len_utf8();
+            len_inserted += close.len_utf8();
+            let mut tendril = Tendril::new();
+            tendril.push(close);
+            (cursor, cursor, Some(tendril))
+        };
 
-            // TODO: else return (use default handler that inserts close)
-            (pos, pos, Some(Tendril::from_char(close)))
-        }
+        let next_range = get_next_range(doc, start_range, offs, close, len_inserted);
+        end_ranges.push(next_range);
+        offs += len_inserted;
+
+        change
     });
 
-    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
 }
 
-// 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 cursor = start_range.cursor(doc.slice(..));
+        let mut len_inserted = 0;
+
+        let next_char = doc.get_char(cursor);
+        let prev_char = prev_char(doc, cursor);
+
+        let change = if next_char == Some(token) {
+            //  return transaction that moves past close
+            (cursor, cursor, None) // no-op
+        } else {
+            let mut pair = Tendril::new();
+            pair.push(token);
+
+            // for equal pairs, don't insert both open and close if either
+            // side has a non-pair char
+            if (next_char.is_none() || close_before.contains(next_char.unwrap()))
+                && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
+            {
+                pair.push(token);
+            }
+
+            len_inserted += pair.len();
+            (cursor, cursor, Some(pair))
+        };
+
+        let next_range = get_next_range(doc, start_range, offs, token, len_inserted);
+        end_ranges.push(next_range);
+        offs += len_inserted;
+
+        change
+    });
+
+    let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+    debug!("auto pair transaction: {:#?}", t);
+    t
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use smallvec::smallvec;
+
+    const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
+
+    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::from(LINE_END),
+            &Selection::single(1, 0),
+            PAIRS,
+            |open, close| format!("{}{}{}", open, close, LINE_END),
+            &Selection::single(2, 1),
+        );
+
+        let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END));
+
+        test_hooks_with_pairs(
+            &empty_doc,
+            &Selection::single(empty_doc.len_chars(), LINE_END.len()),
+            PAIRS,
+            |open, close| {
+                format!(
+                    "{line_end}{open}{close}{line_end}",
+                    open = open,
+                    close = close,
+                    line_end = LINE_END
+                )
+            },
+            &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1),
+        );
+    }
+
+    #[test]
+    fn test_insert_before_multi_code_point_graphemes() {
+        test_hooks_with_pairs(
+            &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
+            &Selection::single(13, 6),
+            PAIRS,
+            |open, _| format!("hello {}👨‍👩‍👧‍👦 goodbye{}", open, LINE_END),
+            &Selection::single(14, 7),
+        );
+    }
+
+    #[test]
+    fn test_insert_at_end_of_document() {
+        test_hooks_with_pairs(
+            &Rope::from(LINE_END),
+            &Selection::single(LINE_END.len(), LINE_END.len()),
+            PAIRS,
+            |open, close| format!("{}{}{}", LINE_END, open, close),
+            &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
+        );
+
+        test_hooks_with_pairs(
+            &Rope::from(format!("foo{}", LINE_END)),
+            &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
+            PAIRS,
+            |open, close| format!("foo{}{}{}", LINE_END, open, close),
+            &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
+        );
+    }
+
+    /// [] -> append ( -> ([])
+    #[test]
+    fn test_append_blank() {
+        test_hooks_with_pairs(
+            // this is what happens when you have a totally blank document and then append
+            &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
+            // before inserting the pair, the cursor covers all of both empty lines
+            &Selection::single(0, LINE_END.len() * 2),
+            PAIRS,
+            |open, close| {
+                format!(
+                    "{line_end}{open}{close}{line_end}",
+                    line_end = LINE_END,
+                    open = open,
+                    close = close
+                )
+            },
+            // after inserting pair, the cursor covers the first new line and the open char
+            &Selection::single(0, LINE_END.len() + 2),
+        );
+    }
+
+    /// []              ([])
+    /// [] -> 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::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+                0,
+            ),
+        );
+    }
+
+    /// fo[o] -> append ( -> fo[o(])
+    #[test]
+    fn test_append() {
+        test_hooks_with_pairs(
+            &Rope::from("foo\n"),
+            &Selection::single(2, 4),
+            differing_pairs(),
+            |open, close| format!("foo{}{}\n", open, close),
+            &Selection::single(2, 5),
+        );
+    }
+
+    /// foo[] -> append to end of line ( -> foo([])
+    #[test]
+    fn test_append_single_cursor() {
+        test_hooks_with_pairs(
+            &Rope::from(format!("foo{}", LINE_END)),
+            &Selection::single(3, 3 + LINE_END.len()),
+            differing_pairs(),
+            |open, close| format!("foo{}{}{}", open, close, LINE_END),
+            &Selection::single(4, 5),
+        );
+    }
+
+    /// fo[o]                fo[o(])
+    /// fo[o] -> append ( -> fo[o(])
+    /// fo[o]                fo[o(])
+    #[test]
+    fn test_append_multi() {
+        test_hooks_with_pairs(
+            &Rope::from("foo\nfoo\nfoo\n"),
+            &Selection::new(
+                smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)),
+                0,
+            ),
+            differing_pairs(),
+            |open, close| {
+                format!(
+                    "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
+                    open = open,
+                    close = close
+                )
+            },
+            &Selection::new(
+                smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)),
+                0,
+            ),
+        );
+    }
+
+    /// ([)] -> insert ) -> ()[]
+    #[test]
+    fn test_insert_close_inside_pair() {
+        for (open, close) in PAIRS {
+            let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+            test_hooks(
+                &doc,
+                &Selection::single(2, 1),
+                *close,
+                &doc,
+                &Selection::single(2 + LINE_END.len(), 2),
+            );
+        }
+    }
+
+    /// [(]) -> append ) -> [()]
+    #[test]
+    fn test_append_close_inside_pair() {
+        for (open, close) in PAIRS {
+            let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+            test_hooks(
+                &doc,
+                &Selection::single(0, 2),
+                *close,
+                &doc,
+                &Selection::single(0, 2 + LINE_END.len()),
+            );
+        }
+    }
+
+    /// ([])                ()[]
+    /// ([]) -> 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),),
+            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);
+        }
+    }
+
+    /// [(])                [()]
+    /// [(]) -> append ) -> [()]
+    /// [(])                [()]
+    #[test]
+    fn test_append_close_inside_pair_multi_cursor() {
+        let sel = Selection::new(
+            smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),),
+            0,
+        );
+
+        let expected_sel = Selection::new(
+            smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),),
+            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::single(3, 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);
+        }
+    }
+
+    /// [word(]) -> append ( -> [word((]))
+    #[test]
+    fn test_append_open_inside_pair() {
+        let sel = Selection::single(0, 6);
+        let expected_sel = Selection::single(0, 7);
+
+        for (open, close) in differing_pairs() {
+            let doc = Rope::from(format!("word{}{}", open, close));
+            let expected_doc = Rope::from(format!(
+                "word{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::single(3, 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);
+            }
+        }
+    }
+
+    /// [(]) -> append " -> [("]")
+    #[test]
+    fn test_append_nested_open_inside_pair() {
+        let sel = Selection::single(0, 2);
+        let expected_sel = Selection::single(0, 3);
+
+        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::single(2, 1),
+        )
+    }
+
+    /// [wor]d -> insert ( -> ([wor]d
+    #[test]
+    fn test_insert_open_with_selection() {
+        test_hooks_with_pairs(
+            &Rope::from("word"),
+            &Selection::single(3, 0),
+            PAIRS,
+            |open, _| format!("{}word", open),
+            &Selection::single(4, 1),
+        )
+    }
+
+    /// [wor]d -> append ) -> [wor)]d
+    #[test]
+    fn test_append_close_inside_non_pair_with_selection() {
+        let sel = Selection::single(0, 4);
+        let expected_sel = Selection::single(0, 5);
+
+        for (_, close) in PAIRS {
+            let doc = Rope::from("word");
+            let expected_doc = Rope::from(format!("wor{}d", close));
+            test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
+        }
+    }
+
+    /// foo[ wor]d -> insert ( -> foo([) wor]d
+    #[test]
+    fn test_insert_open_trailing_word_with_selection() {
+        test_hooks_with_pairs(
+            &Rope::from("foo word"),
+            &Selection::single(7, 3),
+            differing_pairs(),
+            |open, close| format!("foo{}{} word", open, close),
+            &Selection::single(9, 4),
+        )
+    }
+
+    /// foo([) wor]d -> insert ) -> foo()[ wor]d
+    #[test]
+    fn test_insert_close_inside_pair_trailing_word_with_selection() {
+        for (open, close) in differing_pairs() {
+            test_hooks(
+                &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+                &Selection::single(9, 4),
+                *close,
+                &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+                &Selection::single(9, 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(format!("word{}", LINE_END));
+        let sel = Selection::single(5, 4);
+        let expected_sel = Selection::single(6, 5);
+
+        test_hooks_with_pairs(
+            &doc,
+            &sel,
+            differing_pairs(),
+            |open, close| format!("word{}{}{}", open, close, LINE_END),
+            &expected_sel,
+        );
+
+        test_hooks_with_pairs(
+            &doc,
+            &sel,
+            matching_pairs(),
+            |open, _| format!("word{}{}", open, LINE_END),
+            &expected_sel,
+        );
+    }
 }
diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs
index c8e5efbde..549915740 100644
--- a/helix-core/src/chars.rs
+++ b/helix-core/src/chars.rs
@@ -91,12 +91,11 @@ mod test {
 
     #[test]
     fn test_categorize() {
-        const EOL_TEST_CASE: &'static str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
-        const WORD_TEST_CASE: &'static str =
-            "_hello_world_あいうえおー12345678901234567890";
-        const PUNCTUATION_TEST_CASE: &'static str =
+        const EOL_TEST_CASE: &str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+        const WORD_TEST_CASE: &str = "_hello_world_あいうえおー12345678901234567890";
+        const PUNCTUATION_TEST_CASE: &str =
             "!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
-        const WHITESPACE_TEST_CASE: &'static str = "      ";
+        const WHITESPACE_TEST_CASE: &str = "      ";
 
         for ch in EOL_TEST_CASE.chars() {
             assert_eq!(CharCategory::Eol, categorize_char(ch));
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index 4fcf51c9c..210ad6391 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,12 +1,19 @@
 //! LSP diagnostic utility types.
+use serde::{Deserialize, Serialize};
 
 /// Describes the severity level of a [`Diagnostic`].
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
 pub enum Severity {
-    Error,
-    Warning,
-    Info,
     Hint,
+    Info,
+    Warning,
+    Error,
+}
+
+impl Default for Severity {
+    fn default() -> Self {
+        Self::Hint
+    }
 }
 
 /// A range of `char`s within the text.
diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs
index a83db3338..6960c679c 100644
--- a/helix-core/src/diff.rs
+++ b/helix-core/src/diff.rs
@@ -11,10 +11,6 @@ pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
     // A timeout is set so after 1 seconds, the algorithm will start
     // approximating. This is especially important for big `Rope`s or
     // `Rope`s that are extremely dissimilar to each other.
-    //
-    // Note: Ignore the clippy warning, as the trait bounds of
-    // `Transaction::change()` require an iterator implementing
-    // `ExactIterator`.
     let mut config = similar::TextDiff::configure();
     config.timeout(std::time::Duration::from_secs(1));
 
@@ -62,7 +58,7 @@ mod tests {
             let mut old = Rope::from(a);
             let new = Rope::from(b);
             compare_ropes(&old, &new).apply(&mut old);
-            old.to_string() == new.to_string()
+            old == new
         }
     }
 }
diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs
index c63988757..aa8986844 100644
--- a/helix-core/src/graphemes.rs
+++ b/helix-core/src/graphemes.rs
@@ -120,6 +120,43 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
     chunk_char_idx + tmp
 }
 
+#[must_use]
+pub fn nth_next_grapheme_boundary_byte(slice: RopeSlice, mut byte_idx: usize, n: usize) -> usize {
+    // Bounds check
+    debug_assert!(byte_idx <= slice.len_bytes());
+
+    // Get the chunk with our byte index in it.
+    let (mut chunk, mut chunk_byte_idx, mut _chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
+
+    // Set up the grapheme cursor.
+    let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
+
+    // Find the nth next grapheme cluster boundary.
+    for _ in 0..n {
+        loop {
+            match gc.next_boundary(chunk, chunk_byte_idx) {
+                Ok(None) => return slice.len_bytes(),
+                Ok(Some(n)) => {
+                    byte_idx = n;
+                    break;
+                }
+                Err(GraphemeIncomplete::NextChunk) => {
+                    chunk_byte_idx += chunk.len();
+                    let (a, _, _c, _) = slice.chunk_at_byte(chunk_byte_idx);
+                    chunk = a;
+                    // chunk_char_idx = c;
+                }
+                Err(GraphemeIncomplete::PreContext(n)) => {
+                    let ctx_chunk = slice.chunk_at_byte(n - 1).0;
+                    gc.provide_context(ctx_chunk, n - ctx_chunk.len());
+                }
+                _ => unreachable!(),
+            }
+        }
+    }
+    byte_idx
+}
+
 /// Finds the next grapheme boundary after the given char position.
 #[must_use]
 #[inline(always)]
@@ -127,6 +164,13 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
     nth_next_grapheme_boundary(slice, char_idx, 1)
 }
 
+/// Finds the next grapheme boundary after the given byte position.
+#[must_use]
+#[inline(always)]
+pub fn next_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> usize {
+    nth_next_grapheme_boundary_byte(slice, byte_idx, 1)
+}
+
 /// Returns the passed char index if it's already a grapheme boundary,
 /// or the next grapheme boundary char index if not.
 #[must_use]
@@ -151,6 +195,23 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
     }
 }
 
+/// Returns the passed byte index if it's already a grapheme boundary,
+/// or the next grapheme boundary byte index if not.
+#[must_use]
+#[inline]
+pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize {
+    if byte_idx == 0 {
+        byte_idx
+    } else {
+        // TODO: optimize so we're not constructing grapheme cursor twice
+        if is_grapheme_boundary_byte(slice, byte_idx) {
+            byte_idx
+        } else {
+            next_grapheme_boundary_byte(slice, byte_idx)
+        }
+    }
+}
+
 /// Returns whether the given char position is a grapheme boundary.
 #[must_use]
 pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
@@ -179,6 +240,31 @@ pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
     }
 }
 
+/// Returns whether the given byte position is a grapheme boundary.
+#[must_use]
+pub fn is_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> bool {
+    // Bounds check
+    debug_assert!(byte_idx <= slice.len_bytes());
+
+    // Get the chunk with our byte index in it.
+    let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
+
+    // Set up the grapheme cursor.
+    let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
+
+    // Determine if the given position is a grapheme cluster boundary.
+    loop {
+        match gc.is_boundary(chunk, chunk_byte_idx) {
+            Ok(n) => return n,
+            Err(GraphemeIncomplete::PreContext(n)) => {
+                let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
+                gc.provide_context(ctx_chunk, ctx_byte_start);
+            }
+            Err(_) => unreachable!(),
+        }
+    }
+}
+
 /// An iterator over the graphemes of a `RopeSlice`.
 #[derive(Clone)]
 pub struct RopeGraphemes<'a> {
diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs
index 4b1c8d3b3..bb95213c1 100644
--- a/helix-core/src/history.rs
+++ b/helix-core/src/history.rs
@@ -448,8 +448,8 @@ mod test {
             change: crate::transaction::Change,
             instant: Instant,
         ) {
-            let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter());
-            history.commit_revision_at_timestamp(&txn, &state, instant);
+            let txn = Transaction::change(&state.doc, vec![change].into_iter());
+            history.commit_revision_at_timestamp(&txn, state, instant);
             txn.apply(&mut state.doc);
         }
 
diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs
new file mode 100644
index 000000000..91fa59631
--- /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(Self {
+                regex: r"\d{4}",
+                unit: DateUnit::Years,
+                max_len: 5,
+            }),
+            "y" => Some(Self {
+                regex: r"\d\d",
+                unit: DateUnit::Years,
+                max_len: 2,
+            }),
+            "m" => Some(Self {
+                regex: r"[0-1]\d",
+                unit: DateUnit::Months,
+                max_len: 2,
+            }),
+            "d" => Some(Self {
+                regex: r"[0-3]\d",
+                unit: DateUnit::Days,
+                max_len: 2,
+            }),
+            "-d" => Some(Self {
+                regex: r"[1-3]?\d",
+                unit: DateUnit::Days,
+                max_len: 2,
+            }),
+            "a" => Some(Self {
+                regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
+                unit: DateUnit::Days,
+                max_len: 3,
+            }),
+            "A" => Some(Self {
+                regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
+                unit: DateUnit::Days,
+                max_len: 9,
+            }),
+            "b" | "h" => Some(Self {
+                regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
+                unit: DateUnit::Months,
+                max_len: 3,
+            }),
+            "B" => Some(Self {
+                regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
+                unit: DateUnit::Months,
+                max_len: 9,
+            }),
+            "H" => Some(Self {
+                regex: r"[0-2]\d",
+                unit: DateUnit::Hours,
+                max_len: 2,
+            }),
+            "M" => Some(Self {
+                regex: r"[0-5]\d",
+                unit: DateUnit::Minutes,
+                max_len: 2,
+            }),
+            "S" => Some(Self {
+                regex: r"[0-5]\d",
+                unit: DateUnit::Seconds,
+                max_len: 2,
+            }),
+            "I" => Some(Self {
+                regex: r"[0-1]\d",
+                unit: DateUnit::Hours,
+                max_len: 2,
+            }),
+            "-I" => Some(Self {
+                regex: r"1?\d",
+                unit: DateUnit::Hours,
+                max_len: 2,
+            }),
+            "P" => Some(Self {
+                regex: r"am|pm",
+                unit: DateUnit::AmPm,
+                max_len: 2,
+            }),
+            "p" => Some(Self {
+                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,
+                Tendril::from(expected)
+            );
+        }
+    }
+
+    #[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 000000000..f59457748
--- /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 95%
rename from helix-core/src/numbers.rs
rename to helix-core/src/increment/number.rs
index e9f3c898d..57171f671 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,8 +369,9 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
-                expected.into()
+                    .increment(amount)
+                    .1,
+                Tendril::from(expected)
             );
         }
     }
@@ -392,8 +396,9 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
-                expected.into()
+                    .increment(amount)
+                    .1,
+                Tendril::from(expected)
             );
         }
     }
@@ -419,8 +424,9 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
-                expected.into()
+                    .increment(amount)
+                    .1,
+                Tendril::from(expected)
             );
         }
     }
@@ -464,8 +470,9 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
-                expected.into()
+                    .increment(amount)
+                    .1,
+                Tendril::from(expected)
             );
         }
     }
@@ -491,8 +498,9 @@ mod test {
             assert_eq!(
                 NumberIncrementor::from_range(rope.slice(..), range)
                     .unwrap()
-                    .incremented_text(amount),
-                expected.into()
+                    .increment(amount)
+                    .1,
+                Tendril::from(expected)
             );
         }
     }
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 8ccc0120a..5d20edc1a 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -1,6 +1,5 @@
 use crate::{
     chars::{char_is_line_ending, char_is_whitespace},
-    find_first_non_whitespace_char,
     syntax::{IndentQuery, LanguageConfiguration, Syntax},
     tree_sitter::Node,
     Rope, RopeSlice,
@@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
 
 /// To determine indentation of a newly inserted line, figure out the indentation at the last col
 /// of the previous line.
-#[allow(dead_code)]
-fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
+pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
     let mut len = 0;
     for ch in line.chars() {
         match ch {
@@ -207,10 +205,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
     Some(node)
 }
 
-fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
-    // NOTE: can't use contains() on query because of comparing Vec<String> and &str
-    // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
-
+/// Calculate the indentation at a given treesitter node.
+/// If newline is false, then any "indent" nodes on the line are ignored ("outdent" still applies).
+/// This is because the indentation is only increased starting at the second line of the node.
+fn calculate_indentation(
+    query: &IndentQuery,
+    node: Option<Node>,
+    line: usize,
+    newline: bool,
+) -> usize {
     let mut increment: isize = 0;
 
     let mut node = match node {
@@ -218,70 +221,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
         None => return 0,
     };
 
-    let mut prev_start = node.start_position().row;
+    let mut current_line = line;
+    let mut consider_indent = newline;
+    let mut increment_from_line: isize = 0;
 
-    // if we're calculating indentation for a brand new line then the current node will become the
-    // parent node. We need to take it's indentation level into account too.
-    let node_kind = node.kind();
-    if newline && query.indent.contains(node_kind) {
-        increment += 1;
-    }
-
-    while let Some(parent) = node.parent() {
-        let parent_kind = parent.kind();
-        let start = parent.start_position().row;
-
-        // detect deeply nested indents in the same line
-        // .map(|a| {       <-- ({ is two scopes
-        //     let len = 1; <-- indents one level
-        // })               <-- }) is two scopes
-        let starts_same_line = start == prev_start;
-
-        if query.outdent.contains(node.kind()) && !starts_same_line {
-            // we outdent by skipping the rules for the current level and jumping up
-            // node = parent;
-            increment -= 1;
-            // continue;
+    loop {
+        let node_kind = node.kind();
+        let start = node.start_position().row;
+        if current_line != start {
+            // Indent/dedent by at most one per line:
+            // .map(|a| {       <-- ({ is two scopes
+            //     let len = 1; <-- indents one level
+            // })               <-- }) is two scopes
+            if consider_indent || increment_from_line < 0 {
+                increment += increment_from_line.signum();
+            }
+            increment_from_line = 0;
+            current_line = start;
+            consider_indent = true;
         }
 
-        if query.indent.contains(parent_kind) // && not_first_or_last_sibling
-            && !starts_same_line
-        {
-            // println!("is_scope {}", parent_kind);
-            prev_start = start;
-            increment += 1
+        if query.outdent.contains(node_kind) {
+            increment_from_line -= 1;
+        }
+        if query.indent.contains(node_kind) {
+            increment_from_line += 1;
         }
 
-        // if last_scope && increment > 0 && ...{ ignore }
-
-        node = parent;
+        if let Some(parent) = node.parent() {
+            node = parent;
+        } else {
+            break;
+        }
+    }
+    if consider_indent || increment_from_line < 0 {
+        increment += increment_from_line.signum();
     }
-
     increment.max(0) as usize
 }
 
-#[allow(dead_code)]
-fn suggested_indent_for_line(
-    language_config: &LanguageConfiguration,
-    syntax: Option<&Syntax>,
-    text: RopeSlice,
-    line_num: usize,
-    _tab_width: usize,
-) -> usize {
-    if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
-        return suggested_indent_for_pos(
-            Some(language_config),
-            syntax,
-            text,
-            start + text.line_to_char(line_num),
-            false,
-        );
-    };
-
-    // if the line is blank, indent should be zero
-    0
-}
-
 // TODO: two usecases: if we are triggering this for a new, blank line:
 // - it should return 0 when mass indenting stuff
 // - it should look up the wrapper node and count it too when we press o/O
@@ -290,23 +268,20 @@ pub fn suggested_indent_for_pos(
     syntax: Option<&Syntax>,
     text: RopeSlice,
     pos: usize,
+    line: usize,
     new_line: bool,
-) -> usize {
+) -> Option<usize> {
     if let (Some(query), Some(syntax)) = (
         language_config.and_then(|config| config.indent_query()),
         syntax,
     ) {
         let byte_start = text.char_to_byte(pos);
         let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
-
-        // let config = load indentation query config from Syntax(should contain language_config)
-
         // TODO: special case for comments
         // TODO: if preserve_leading_whitespace
-        calculate_indentation(query, node, new_line)
+        Some(calculate_indentation(query, node, line, new_line))
     } else {
-        // TODO: heuristics for non-tree sitter grammars
-        0
+        None
     }
 }
 
@@ -438,7 +413,8 @@ where
 ",
         );
 
-        let doc = Rope::from(doc);
+        let doc = doc;
+        use crate::diagnostic::Severity;
         use crate::syntax::{
             Configuration, IndentationConfiguration, LanguageConfiguration, Loader,
         };
@@ -456,6 +432,8 @@ where
                 roots: vec![],
                 comment_token: None,
                 auto_format: false,
+                diagnostic_severity: Severity::Warning,
+                tree_sitter_library: None,
                 language_server: None,
                 indent: Some(IndentationConfiguration {
                     tab_width: 4,
@@ -474,20 +452,29 @@ where
 
         let language_config = loader.language_config_for_scope("source.rust").unwrap();
         let highlight_config = language_config.highlight_config(&[]).unwrap();
-        let syntax = Syntax::new(&doc, highlight_config.clone());
+        let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader));
         let text = doc.slice(..);
         let tab_width = 4;
 
         for i in 0..doc.len_lines() {
             let line = text.line(i);
-            let indent = indent_level_for_line(line, tab_width);
-            assert_eq!(
-                suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
-                indent,
-                "line {}: {}",
-                i,
-                line
-            );
+            if let Some(pos) = crate::find_first_non_whitespace_char(line) {
+                let indent = indent_level_for_line(line, tab_width);
+                assert_eq!(
+                    suggested_indent_for_pos(
+                        Some(&language_config),
+                        Some(&syntax),
+                        text,
+                        text.line_to_char(i) + pos,
+                        i,
+                        false
+                    ),
+                    Some(indent),
+                    "line {}: \"{}\"",
+                    i,
+                    line
+                );
+            }
         }
     }
 }
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 7d7904061..fa8566ab4 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -1,3 +1,5 @@
+pub use encoding_rs as encoding;
+
 pub mod auto_pairs;
 pub mod chars;
 pub mod comment;
@@ -5,18 +7,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;
@@ -36,8 +39,14 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
     line.chars().position(|ch| !ch.is_whitespace())
 }
 
-/// Find `.git` root.
-pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
+/// Find project root.
+///
+/// Order of detection:
+/// * Top-most folder containing a root marker in current git repository
+/// * Git repostory root if no marker detected
+/// * Top-most folder containing a root marker if not git repository detected
+/// * Current working directory as fallback
+pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::path::PathBuf> {
     let current_dir = std::env::current_dir().expect("unable to determine current directory");
 
     let root = match root {
@@ -49,16 +58,30 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
                 current_dir.join(root)
             }
         }
-        None => current_dir,
+        None => current_dir.clone(),
     };
 
+    let mut top_marker = None;
     for ancestor in root.ancestors() {
-        // TODO: also use defined roots if git isn't found
+        for marker in root_markers {
+            if ancestor.join(marker).exists() {
+                top_marker = Some(ancestor);
+                break;
+            }
+        }
+        // don't go higher than repo
         if ancestor.join(".git").is_dir() {
-            return Some(ancestor.to_path_buf());
+            // Use workspace if detected from marker
+            return Some(top_marker.unwrap_or(ancestor).to_path_buf());
         }
     }
-    None
+
+    // In absence of git repo, use workspace if detected
+    if top_marker.is_some() {
+        top_marker.map(|a| a.to_path_buf())
+    } else {
+        Some(current_dir)
+    }
 }
 
 pub fn runtime_dir() -> std::path::PathBuf {
@@ -158,7 +181,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);
@@ -189,7 +212,10 @@ use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
 
 pub use ropey::{Rope, RopeBuilder, RopeSlice};
 
-pub use tendril::StrTendril as Tendril;
+// pub use tendril::StrTendril as Tendril;
+pub use smartstring::SmartString;
+
+pub type Tendril = SmartString<smartstring::LazyCompact>;
 
 #[doc(inline)]
 pub use {regex, tree_sitter};
diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs
index 3541305c3..8eb426e1e 100644
--- a/helix-core/src/line_ending.rs
+++ b/helix-core/src/line_ending.rs
@@ -250,7 +250,7 @@ mod line_ending_tests {
         assert_eq!(get_line_ending_of_str(&text[..6]), Some(LineEnding::CR));
         assert_eq!(get_line_ending_of_str(&text[..12]), Some(LineEnding::LF));
         assert_eq!(get_line_ending_of_str(&text[..17]), Some(LineEnding::Crlf));
-        assert_eq!(get_line_ending_of_str(&text[..]), None);
+        assert_eq!(get_line_ending_of_str(text), None);
     }
 
     #[test]
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs
index cd554005a..0189deddb 100644
--- a/helix-core/src/match_brackets.rs
+++ b/helix-core/src/match_brackets.rs
@@ -11,7 +11,7 @@ const PAIRS: &[(char, char)] = &[
     ('\"', '\"'),
 ];
 
-// limit matching pairs to only ( ) { } [ ] < >
+// limit matching pairs to only ( ) { } [ ] < > ' ' " "
 
 // Returns the position of the matching bracket under cursor.
 //
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index 01a8f890e..47fe68272 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -307,8 +307,6 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
 
 #[cfg(test)]
 mod test {
-    use std::array::{self, IntoIter};
-
     use ropey::Rope;
 
     use super::*;
@@ -360,7 +358,7 @@ mod test {
             ((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line
         ];
 
-        for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
+        for ((direction, amount), coordinates) in moves_and_expected_coordinates {
             range = move_horizontally(slice, range, direction, amount, Movement::Move);
             assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
         }
@@ -374,7 +372,7 @@ mod test {
 
         let mut range = Range::point(position);
 
-        let moves_and_expected_coordinates = IntoIter::new([
+        let moves_and_expected_coordinates = [
             ((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n...
             ((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n...
             ((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n...
@@ -384,7 +382,7 @@ mod test {
             ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
             ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
             ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
-        ]);
+        ];
 
         for ((direction, amount), coordinates) in moves_and_expected_coordinates {
             range = move_horizontally(slice, range, direction, amount, Movement::Move);
@@ -402,11 +400,11 @@ mod test {
         let mut range = Range::point(position);
         let original_anchor = range.anchor;
 
-        let moves = IntoIter::new([
+        let moves = [
             (Direction::Forward, 1usize),
             (Direction::Forward, 5usize),
             (Direction::Backward, 3usize),
-        ]);
+        ];
 
         for (direction, amount) in moves {
             range = move_horizontally(slice, range, direction, amount, Movement::Extend);
@@ -420,7 +418,7 @@ mod test {
         let slice = text.slice(..);
         let position = pos_at_coords(slice, (0, 0).into(), true);
         let mut range = Range::point(position);
-        let moves_and_expected_coordinates = IntoIter::new([
+        let moves_and_expected_coordinates = [
             ((Direction::Forward, 1usize), (1, 0)),
             ((Direction::Forward, 2usize), (3, 0)),
             ((Direction::Forward, 1usize), (4, 0)),
@@ -430,7 +428,7 @@ mod test {
             ((Direction::Backward, 0usize), (4, 0)),
             ((Direction::Forward, 5), (5, 0)),
             ((Direction::Forward, 999usize), (5, 0)),
-        ]);
+        ];
 
         for ((direction, amount), coordinates) in moves_and_expected_coordinates {
             range = move_vertically(slice, range, direction, amount, Movement::Move);
@@ -450,7 +448,7 @@ mod test {
             H,
             V,
         }
-        let moves_and_expected_coordinates = IntoIter::new([
+        let moves_and_expected_coordinates = [
             // Places cursor at the end of line
             ((Axis::H, Direction::Forward, 8usize), (0, 8)),
             // First descent preserves column as the target line is wider
@@ -463,7 +461,7 @@ mod test {
             ((Axis::V, Direction::Backward, 999usize), (0, 8)),
             ((Axis::V, Direction::Forward, 4usize), (4, 8)),
             ((Axis::V, Direction::Forward, 999usize), (5, 0)),
-        ]);
+        ];
 
         for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
             range = match axis {
@@ -489,7 +487,7 @@ mod test {
             H,
             V,
         }
-        let moves_and_expected_coordinates = IntoIter::new([
+        let moves_and_expected_coordinates = [
             // Places cursor at the fourth kana.
             ((Axis::H, Direction::Forward, 4), (0, 4)),
             // Descent places cursor at the 4th character.
@@ -498,7 +496,7 @@ mod test {
             ((Axis::H, Direction::Backward, 1usize), (1, 3)),
             // Jumping back up 1 line.
             ((Axis::V, Direction::Backward, 1usize), (0, 3)),
-        ]);
+        ];
 
         for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
             range = match axis {
@@ -530,7 +528,7 @@ mod test {
 
     #[test]
     fn test_behaviour_when_moving_to_start_of_next_words() {
-        let tests = array::IntoIter::new([
+        let tests = [
             ("Basic forward motion stops at the first space",
                 vec![(1, Range::new(0, 0), Range::new(0, 6))]),
             (" Starting from a boundary advances the anchor",
@@ -604,7 +602,7 @@ mod test {
                 vec![
                     (1, Range::new(0, 0), Range::new(0, 6)),
                 ]),
-        ]);
+        ];
 
         for (sample, scenario) in tests {
             for (count, begin, expected_end) in scenario.into_iter() {
@@ -616,7 +614,7 @@ mod test {
 
     #[test]
     fn test_behaviour_when_moving_to_start_of_next_long_words() {
-        let tests = array::IntoIter::new([
+        let tests = [
             ("Basic forward motion stops at the first space",
                 vec![(1, Range::new(0, 0), Range::new(0, 6))]),
             (" Starting from a boundary advances the anchor",
@@ -688,7 +686,7 @@ mod test {
                 vec![
                     (1, Range::new(0, 0), Range::new(0, 8)),
                 ]),
-        ]);
+        ];
 
         for (sample, scenario) in tests {
             for (count, begin, expected_end) in scenario.into_iter() {
@@ -700,7 +698,7 @@ mod test {
 
     #[test]
     fn test_behaviour_when_moving_to_start_of_previous_words() {
-        let tests = array::IntoIter::new([
+        let tests = [
             ("Basic backward motion from the middle of a word",
                 vec![(1, Range::new(3, 3), Range::new(4, 0))]),
 
@@ -773,7 +771,7 @@ mod test {
                 vec![
                     (1, Range::new(0, 6), Range::new(6, 0)),
                 ]),
-        ]);
+        ];
 
         for (sample, scenario) in tests {
             for (count, begin, expected_end) in scenario.into_iter() {
@@ -785,7 +783,7 @@ mod test {
 
     #[test]
     fn test_behaviour_when_moving_to_start_of_previous_long_words() {
-        let tests = array::IntoIter::new([
+        let tests = [
             (
                 "Basic backward motion from the middle of a word",
                 vec![(1, Range::new(3, 3), Range::new(4, 0))],
@@ -870,7 +868,7 @@ mod test {
                 vec![
                     (1, Range::new(0, 8), Range::new(8, 0)),
                 ]),
-        ]);
+        ];
 
         for (sample, scenario) in tests {
             for (count, begin, expected_end) in scenario.into_iter() {
@@ -882,7 +880,7 @@ mod test {
 
     #[test]
     fn test_behaviour_when_moving_to_end_of_next_words() {
-        let tests = array::IntoIter::new([
+        let tests = [
             ("Basic forward motion from the start of a word to the end of it",
                 vec![(1, Range::new(0, 0), Range::new(0, 5))]),
             ("Basic forward motion from the end of a word to the end of the next",
@@ -954,7 +952,7 @@ mod test {
                 vec![
                     (1, Range::new(0, 0), Range::new(0, 5)),
                 ]),
-        ]);
+        ];
 
         for (sample, scenario) in tests {
             for (count, begin, expected_end) in scenario.into_iter() {
@@ -966,7 +964,7 @@ mod test {
 
     #[test]
     fn test_behaviour_when_moving_to_end_of_previous_words() {
-        let tests = array::IntoIter::new([
+        let tests = [
             ("Basic backward motion from the middle of a word",
                 vec![(1, Range::new(9, 9), Range::new(10, 5))]),
             ("Starting from after boundary retreats the anchor",
@@ -1036,7 +1034,7 @@ mod test {
                 vec![
                     (1, Range::new(0, 10), Range::new(10, 4)),
                 ]),
-        ]);
+        ];
 
         for (sample, scenario) in tests {
             for (count, begin, expected_end) in scenario.into_iter() {
@@ -1048,7 +1046,7 @@ mod test {
 
     #[test]
     fn test_behaviour_when_moving_to_end_of_next_long_words() {
-        let tests = array::IntoIter::new([
+        let tests = [
             ("Basic forward motion from the start of a word to the end of it",
                 vec![(1, Range::new(0, 0), Range::new(0, 5))]),
             ("Basic forward motion from the end of a word to the end of the next",
@@ -1118,7 +1116,7 @@ mod test {
                 vec![
                     (1, Range::new(0, 0), Range::new(0, 7)),
                 ]),
-        ]);
+        ];
 
         for (sample, scenario) in tests {
             for (count, begin, expected_end) in scenario.into_iter() {
diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs
index 717c59947..b06f41444 100644
--- a/helix-core/src/object.rs
+++ b/helix-core/src/object.rs
@@ -1,31 +1,72 @@
 use crate::{Range, RopeSlice, Selection, Syntax};
+use tree_sitter::Node;
 
-// TODO: to contract_selection we'd need to store the previous ranges before expand.
-// Maybe just contract to the first child node?
-pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
+pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+    select_node_impl(syntax, text, selection, |descendant, from, to| {
+        if descendant.start_byte() == from && descendant.end_byte() == to {
+            descendant.parent()
+        } else {
+            Some(descendant)
+        }
+    })
+}
+
+pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+    select_node_impl(syntax, text, selection, |descendant, _from, _to| {
+        descendant.child(0).or(Some(descendant))
+    })
+}
+
+pub fn select_sibling<F>(
+    syntax: &Syntax,
+    text: RopeSlice,
+    selection: Selection,
+    sibling_fn: &F,
+) -> Selection
+where
+    F: Fn(Node) -> Option<Node>,
+{
+    select_node_impl(syntax, text, selection, |descendant, _from, _to| {
+        find_sibling_recursive(descendant, sibling_fn)
+    })
+}
+
+fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
+where
+    F: Fn(Node) -> Option<Node>,
+{
+    sibling_fn(node).or_else(|| {
+        node.parent()
+            .and_then(|node| find_sibling_recursive(node, sibling_fn))
+    })
+}
+
+fn select_node_impl<F>(
+    syntax: &Syntax,
+    text: RopeSlice,
+    selection: Selection,
+    select_fn: F,
+) -> Selection
+where
+    F: Fn(Node, usize, usize) -> Option<Node>,
+{
     let tree = syntax.tree();
 
-    selection.clone().transform(|range| {
+    selection.transform(|range| {
         let from = text.char_to_byte(range.from());
         let to = text.char_to_byte(range.to());
 
-        // find parent of a descendant that matches the range
-        let parent = match tree
+        let node = match tree
             .root_node()
             .descendant_for_byte_range(from, to)
-            .and_then(|node| {
-                if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) {
-                    node.parent()
-                } else {
-                    Some(node)
-                }
-            }) {
-            Some(parent) => parent,
+            .and_then(|node| select_fn(node, from, to))
+        {
+            Some(node) => node,
             None => return range,
         };
 
-        let from = text.byte_to_char(parent.start_byte());
-        let to = text.byte_to_char(parent.end_byte());
+        let from = text.byte_to_char(node.start_byte());
+        let to = text.byte_to_char(node.end_byte());
 
         if range.head < range.anchor {
             Range::new(to, from)
diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs
index c6018ce69..93362c775 100644
--- a/helix-core/src/position.rs
+++ b/helix-core/src/position.rs
@@ -109,7 +109,10 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
 /// TODO: this should be changed to work in terms of visual row/column, not
 /// graphemes.
 pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending: bool) -> usize {
-    let Position { row, col } = coords;
+    let Position { mut row, col } = coords;
+    if limit_before_line_ending {
+        row = row.min(text.len_lines() - 1);
+    };
     let line_start = text.line_to_char(row);
     let line_end = if limit_before_line_ending {
         line_end_char_index(&text, row)
@@ -290,5 +293,12 @@ mod test {
         assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
         assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
         assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
+
+        // Test out of bounds.
+        let text = Rope::new();
+        let slice = text.slice(..);
+        assert_eq!(pos_at_coords(slice, (10, 0).into(), true), 0);
+        assert_eq!(pos_at_coords(slice, (0, 10).into(), true), 0);
+        assert_eq!(pos_at_coords(slice, (10, 10).into(), true), 0);
     }
 }
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index b9eb497df..b39e4034e 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -68,4 +68,8 @@ impl Registers {
     pub fn read(&self, name: char) -> Option<&[String]> {
         self.get(name).map(|reg| reg.read())
     }
+
+    pub fn inner(&self) -> &HashMap<char, Register> {
+        &self.inner
+    }
 }
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 116a1c7c0..c6eceb4b5 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -7,6 +7,7 @@ use crate::{
         ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
         prev_grapheme_boundary,
     },
+    movement::Direction,
     Assoc, ChangeSet, RopeSlice,
 };
 use smallvec::{smallvec, SmallVec};
@@ -82,6 +83,13 @@ impl Range {
         std::cmp::max(self.anchor, self.head)
     }
 
+    /// Total length of the range.
+    #[inline]
+    #[must_use]
+    pub fn len(&self) -> usize {
+        self.to() - self.from()
+    }
+
     /// The (inclusive) range of lines that the range overlaps.
     #[inline]
     #[must_use]
@@ -102,6 +110,27 @@ impl Range {
         self.anchor == self.head
     }
 
+    /// `Direction::Backward` when head < anchor.
+    /// `Direction::Backward` otherwise.
+    #[inline]
+    #[must_use]
+    pub fn direction(&self) -> Direction {
+        if self.head < self.anchor {
+            Direction::Backward
+        } else {
+            Direction::Forward
+        }
+    }
+
+    // flips the direction of the selection
+    pub fn flip(&self) -> Self {
+        Self {
+            anchor: self.head,
+            head: self.anchor,
+            horiz: self.horiz,
+        }
+    }
+
     /// Check two ranges for overlap.
     #[must_use]
     pub fn overlaps(&self, other: &Self) -> bool {
@@ -111,6 +140,11 @@ impl Range {
         self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
     }
 
+    #[inline]
+    pub fn contains_range(&self, other: &Self) -> bool {
+        self.from() <= other.from() && self.to() >= other.to()
+    }
+
     pub fn contains(&self, pos: usize) -> bool {
         self.from() <= pos && pos < self.to()
     }
@@ -515,6 +549,39 @@ impl Selection {
     pub fn len(&self) -> usize {
         self.ranges.len()
     }
+
+    // returns true if self ⊇ other
+    pub fn contains(&self, other: &Selection) -> bool {
+        // can't contain other if it is larger
+        if other.len() > self.len() {
+            return false;
+        }
+
+        let (mut iter_self, mut iter_other) = (self.iter(), other.iter());
+        let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
+
+        loop {
+            match (ele_self, ele_other) {
+                (Some(ra), Some(rb)) => {
+                    if !ra.contains_range(rb) {
+                        // `self` doesn't contain next element from `other`, advance `self`, we need to match all from `other`
+                        ele_self = iter_self.next();
+                    } else {
+                        // matched element from `other`, advance `other`
+                        ele_other = iter_other.next();
+                    };
+                }
+                (None, Some(_)) => {
+                    // exhausted `self`, we can't match the reminder of `other`
+                    return false;
+                }
+                (_, None) => {
+                    // no elements from `other` left to match, `self` contains `other`
+                    return true;
+                }
+            }
+        }
+    }
 }
 
 impl<'a> IntoIterator for &'a Selection {
@@ -699,16 +766,16 @@ mod test {
     fn test_contains() {
         let range = Range::new(10, 12);
 
-        assert_eq!(range.contains(9), false);
-        assert_eq!(range.contains(10), true);
-        assert_eq!(range.contains(11), true);
-        assert_eq!(range.contains(12), false);
-        assert_eq!(range.contains(13), false);
+        assert!(!range.contains(9));
+        assert!(range.contains(10));
+        assert!(range.contains(11));
+        assert!(!range.contains(12));
+        assert!(!range.contains(13));
 
         let range = Range::new(9, 6);
-        assert_eq!(range.contains(9), false);
-        assert_eq!(range.contains(7), true);
-        assert_eq!(range.contains(6), true);
+        assert!(!range.contains(9));
+        assert!(range.contains(7));
+        assert!(range.contains(6));
     }
 
     #[test]
@@ -953,4 +1020,30 @@ mod test {
             &["", "abcd", "efg", "rs", "xyz"]
         );
     }
+    #[test]
+    fn test_selection_contains() {
+        fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {
+            let sela = Selection::new(a.iter().map(|a| Range::new(a.0, a.1)).collect(), 0);
+            let selb = Selection::new(b.iter().map(|b| Range::new(b.0, b.1)).collect(), 0);
+            sela.contains(&selb)
+        }
+
+        // exact match
+        assert!(contains(vec!((1, 1)), vec!((1, 1))));
+
+        // larger set contains smaller
+        assert!(contains(vec!((1, 1), (2, 2), (3, 3)), vec!((2, 2))));
+
+        // multiple matches
+        assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2))));
+
+        // smaller set can't contain bigger
+        assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2))));
+
+        assert!(contains(
+            vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)),
+            vec!((3, 4), (7, 9))
+        ));
+        assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6))));
+    }
 }
diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs
new file mode 100644
index 000000000..13f6f3e99
--- /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 b53b0a78c..58eb23cf2 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -172,6 +172,7 @@ mod test {
     use ropey::Rope;
     use smallvec::SmallVec;
 
+    #[allow(clippy::type_complexity)]
     fn check_find_nth_pair_pos(
         text: &str,
         cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index f1c399d2c..a5c5e4983 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -1,5 +1,6 @@
 use crate::{
     chars::char_is_line_ending,
+    diagnostic::Severity,
     regex::Regex,
     transaction::{ChangeSet, Operation},
     Rope, RopeSlice, Tendril,
@@ -7,12 +8,13 @@ use crate::{
 
 pub use helix_syntax::get_language;
 
-use arc_swap::ArcSwap;
+use arc_swap::{ArcSwap, Guard};
+use slotmap::{DefaultKey as LayerId, HopSlotMap};
 
 use std::{
     borrow::Cow,
     cell::RefCell,
-    collections::{HashMap, HashSet},
+    collections::{HashMap, HashSet, VecDeque},
     fmt,
     path::Path,
     sync::Arc,
@@ -50,7 +52,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)]
@@ -63,6 +65,10 @@ pub struct LanguageConfiguration {
 
     #[serde(default)]
     pub auto_format: bool,
+    #[serde(default)]
+    pub diagnostic_severity: Severity,
+
+    pub tree_sitter_library: Option<String>, // tree-sitter library name, defaults to language_id
 
     // content_regex
     #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
@@ -92,6 +98,7 @@ pub struct LanguageServerConfiguration {
     #[serde(default)]
     #[serde(skip_serializing_if = "Vec::is_empty")]
     pub args: Vec<String>,
+    pub language_id: Option<String>,
 }
 
 #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
@@ -245,20 +252,22 @@ impl LanguageConfiguration {
         if highlights_query.is_empty() {
             None
         } else {
-            let language = get_language(&crate::RUNTIME_DIR, &self.language_id)
-                .map_err(|e| log::info!("{}", e))
-                .ok()?;
+            let language = get_language(
+                &crate::RUNTIME_DIR,
+                self.tree_sitter_library
+                    .as_deref()
+                    .unwrap_or(&self.language_id),
+            )
+            .map_err(|e| log::info!("{}", e))
+            .ok()?;
             let config = HighlightConfiguration::new(
                 language,
                 &highlights_query,
                 &injections_query,
                 &locals_query,
-            );
+            )
+            .unwrap(); // TODO: avoid panic
 
-            let config = match config {
-                Ok(config) => config,
-                Err(err) => panic!("{}", err),
-            }; // TODO: avoid panic
             config.configure(scopes);
             Some(Arc::new(config))
         }
@@ -308,12 +317,16 @@ impl LanguageConfiguration {
     }
 }
 
+// Expose loader as Lazy<> global since it's always static?
+
 #[derive(Debug)]
 pub struct Loader {
     // highlight_names ?
     language_configs: Vec<Arc<LanguageConfiguration>>,
     language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
     language_config_ids_by_shebang: HashMap<String, usize>,
+
+    scopes: ArcSwap<Vec<String>>,
 }
 
 impl Loader {
@@ -322,6 +335,7 @@ impl Loader {
             language_configs: Vec::new(),
             language_config_ids_by_file_type: HashMap::new(),
             language_config_ids_by_shebang: HashMap::new(),
+            scopes: ArcSwap::from_pointee(Vec::new()),
         };
 
         for config in config.language {
@@ -366,8 +380,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]));
@@ -406,8 +421,22 @@ impl Loader {
         }
         None
     }
-    pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
-        self.language_configs.iter()
+
+    pub fn set_scopes(&self, scopes: Vec<String>) {
+        self.scopes.store(Arc::new(scopes));
+
+        // Reconfigure existing grammars
+        for config in self
+            .language_configs
+            .iter()
+            .filter(|cfg| cfg.is_highlight_initialized())
+        {
+            config.reconfigure(&self.scopes());
+        }
+    }
+
+    pub fn scopes(&self) -> Guard<Arc<Vec<String>>> {
+        self.scopes.load()
     }
 }
 
@@ -416,12 +445,6 @@ pub struct TsParser {
     cursors: Vec<QueryCursor>,
 }
 
-impl fmt::Debug for TsParser {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("TsParser").finish()
-    }
-}
-
 // could also just use a pool, or a single instance?
 thread_local! {
     pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
@@ -432,9 +455,9 @@ thread_local! {
 
 #[derive(Debug)]
 pub struct Syntax {
-    config: Arc<HighlightConfiguration>,
-
-    root_layer: LanguageLayer,
+    layers: HopSlotMap<LayerId, LanguageLayer>,
+    root: LayerId,
+    loader: Arc<Loader>,
 }
 
 fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> {
@@ -444,38 +467,34 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
 }
 
 impl Syntax {
-    // buffer, grammar, config, grammars, sync_timeout?
-    pub fn new(
-        /*language: Lang,*/ source: &Rope,
-        config: Arc<HighlightConfiguration>,
-    ) -> Self {
-        let root_layer = LanguageLayer { tree: None };
-
-        // track markers of injections
-        // track scope_descriptor: a Vec of scopes for item in tree
-
-        let mut syntax = Self {
-            // grammar,
+    pub fn new(source: &Rope, config: Arc<HighlightConfiguration>, loader: Arc<Loader>) -> Self {
+        let root_layer = LanguageLayer {
+            tree: None,
             config,
-            root_layer,
+            depth: 0,
+            ranges: vec![Range {
+                start_byte: 0,
+                end_byte: usize::MAX,
+                start_point: Point::new(0, 0),
+                end_point: Point::new(usize::MAX, usize::MAX),
+            }],
         };
 
-        // update root layer
-        PARSER.with(|ts_parser| {
-            // TODO: handle the returned `Result` properly.
-            let _ = syntax.root_layer.parse(
-                &mut ts_parser.borrow_mut(),
-                &syntax.config,
-                source,
-                0,
-                vec![Range {
-                    start_byte: 0,
-                    end_byte: usize::MAX,
-                    start_point: Point::new(0, 0),
-                    end_point: Point::new(usize::MAX, usize::MAX),
-                }],
-            );
-        });
+        // track scope_descriptor: a Vec of scopes for item in tree
+
+        let mut layers = HopSlotMap::default();
+        let root = layers.insert(root_layer);
+
+        let mut syntax = Self {
+            root,
+            layers,
+            loader,
+        };
+
+        syntax
+            .update(source, source, &ChangeSet::new(source))
+            .unwrap();
+
         syntax
     }
 
@@ -485,32 +504,255 @@ impl Syntax {
         source: &Rope,
         changeset: &ChangeSet,
     ) -> Result<(), Error> {
+        let mut queue = VecDeque::new();
+        queue.push_back(self.root);
+
+        let scopes = self.loader.scopes.load();
+        let injection_callback = |language: &str| {
+            self.loader
+                .language_configuration_for_injection_string(language)
+                .and_then(|language_config| language_config.highlight_config(&scopes))
+        };
+
+        // Convert the changeset into tree sitter edits.
+        let edits = generate_edits(old_source, changeset);
+
+        // Use the edits to update all layers markers
+        if !edits.is_empty() {
+            fn point_add(a: Point, b: Point) -> Point {
+                if b.row > 0 {
+                    Point::new(a.row.saturating_add(b.row), b.column)
+                } else {
+                    Point::new(0, a.column.saturating_add(b.column))
+                }
+            }
+            fn point_sub(a: Point, b: Point) -> Point {
+                if a.row > b.row {
+                    Point::new(a.row.saturating_sub(b.row), a.column)
+                } else {
+                    Point::new(0, a.column.saturating_sub(b.column))
+                }
+            }
+
+            for layer in &mut self.layers.values_mut() {
+                // The root layer always covers the whole range (0..usize::MAX)
+                if layer.depth == 0 {
+                    continue;
+                }
+
+                for range in &mut layer.ranges {
+                    // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
+                    for edit in edits.iter().rev() {
+                        let is_pure_insertion = edit.old_end_byte == edit.start_byte;
+
+                        // if edit is after range, skip
+                        if edit.start_byte > range.end_byte {
+                            // TODO: || (is_noop && edit.start_byte == range.end_byte)
+                            continue;
+                        }
+
+                        // if edit is before range, shift entire range by len
+                        if edit.old_end_byte < range.start_byte {
+                            range.start_byte =
+                                edit.new_end_byte + (range.start_byte - edit.old_end_byte);
+                            range.start_point = point_add(
+                                edit.new_end_position,
+                                point_sub(range.start_point, edit.old_end_position),
+                            );
+
+                            range.end_byte = edit
+                                .new_end_byte
+                                .saturating_add(range.end_byte - edit.old_end_byte);
+                            range.end_point = point_add(
+                                edit.new_end_position,
+                                point_sub(range.end_point, edit.old_end_position),
+                            );
+                        }
+                        // if the edit starts in the space before and extends into the range
+                        else if edit.start_byte < range.start_byte {
+                            range.start_byte = edit.new_end_byte;
+                            range.start_point = edit.new_end_position;
+
+                            range.end_byte = range
+                                .end_byte
+                                .saturating_sub(edit.old_end_byte)
+                                .saturating_add(edit.new_end_byte);
+                            range.end_point = point_add(
+                                edit.new_end_position,
+                                point_sub(range.end_point, edit.old_end_position),
+                            );
+                        }
+                        // If the edit is an insertion at the start of the tree, shift
+                        else if edit.start_byte == range.start_byte && is_pure_insertion {
+                            range.start_byte = edit.new_end_byte;
+                            range.start_point = edit.new_end_position;
+                        } else {
+                            range.end_byte = range
+                                .end_byte
+                                .saturating_sub(edit.old_end_byte)
+                                .saturating_add(edit.new_end_byte);
+                            range.end_point = point_add(
+                                edit.new_end_position,
+                                point_sub(range.end_point, edit.old_end_position),
+                            );
+                        }
+                    }
+                }
+            }
+        }
+
         PARSER.with(|ts_parser| {
-            self.root_layer.update(
-                &mut ts_parser.borrow_mut(),
-                &self.config,
-                old_source,
-                source,
-                changeset,
-            )
+            let ts_parser = &mut ts_parser.borrow_mut();
+            let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
+            // TODO: might need to set cursor range
+            cursor.set_byte_range(0..usize::MAX);
+
+            let source_slice = source.slice(..);
+
+            let mut touched = HashSet::new();
+
+            // TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit
+
+            while let Some(layer_id) = queue.pop_front() {
+                // Mark the layer as touched
+                touched.insert(layer_id);
+
+                let layer = &mut self.layers[layer_id];
+
+                // If a tree already exists, notify it of changes.
+                if let Some(tree) = &mut layer.tree {
+                    for edit in edits.iter().rev() {
+                        // Apply the edits in reverse.
+                        // If we applied them in order then edit 1 would disrupt the positioning of edit 2.
+                        tree.edit(edit);
+                    }
+                }
+
+                // Re-parse the tree.
+                layer.parse(&mut ts_parser.parser, source)?;
+
+                // Switch to an immutable borrow.
+                let layer = &self.layers[layer_id];
+
+                // Process injections.
+                let matches = cursor.matches(
+                    &layer.config.injections_query,
+                    layer.tree().root_node(),
+                    RopeProvider(source_slice),
+                );
+                let mut injections = Vec::new();
+                for mat in matches {
+                    let (language_name, content_node, include_children) = injection_for_match(
+                        &layer.config,
+                        &layer.config.injections_query,
+                        &mat,
+                        source_slice,
+                    );
+
+                    // Explicitly remove this match so that none of its other captures will remain
+                    // in the stream of captures.
+                    mat.remove();
+
+                    // If a language is found with the given name, then add a new language layer
+                    // to the highlighted document.
+                    if let (Some(language_name), Some(content_node)) = (language_name, content_node)
+                    {
+                        if let Some(config) = (injection_callback)(&language_name) {
+                            let ranges =
+                                intersect_ranges(&layer.ranges, &[content_node], include_children);
+
+                            if !ranges.is_empty() {
+                                injections.push((config, ranges));
+                            }
+                        }
+                    }
+                }
+
+                // Process combined injections.
+                if let Some(combined_injections_query) = &layer.config.combined_injections_query {
+                    let mut injections_by_pattern_index =
+                        vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
+                    let matches = cursor.matches(
+                        combined_injections_query,
+                        layer.tree().root_node(),
+                        RopeProvider(source_slice),
+                    );
+                    for mat in matches {
+                        let entry = &mut injections_by_pattern_index[mat.pattern_index];
+                        let (language_name, content_node, include_children) = injection_for_match(
+                            &layer.config,
+                            combined_injections_query,
+                            &mat,
+                            source_slice,
+                        );
+                        if language_name.is_some() {
+                            entry.0 = language_name;
+                        }
+                        if let Some(content_node) = content_node {
+                            entry.1.push(content_node);
+                        }
+                        entry.2 = include_children;
+                    }
+                    for (lang_name, content_nodes, includes_children) in injections_by_pattern_index
+                    {
+                        if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
+                            if let Some(config) = (injection_callback)(&lang_name) {
+                                let ranges = intersect_ranges(
+                                    &layer.ranges,
+                                    &content_nodes,
+                                    includes_children,
+                                );
+                                if !ranges.is_empty() {
+                                    injections.push((config, ranges));
+                                }
+                            }
+                        }
+                    }
+                }
+
+                let depth = layer.depth + 1;
+                // TODO: can't inline this since matches borrows self.layers
+                for (config, ranges) in injections {
+                    // Find an existing layer
+                    let layer = self
+                        .layers
+                        .iter_mut()
+                        .find(|(_, layer)| {
+                            layer.depth == depth && // TODO: track parent id instead
+                            layer.config.language == config.language && layer.ranges == ranges
+                        })
+                        .map(|(id, _layer)| id);
+
+                    // ...or insert a new one.
+                    let layer_id = layer.unwrap_or_else(|| {
+                        self.layers.insert(LanguageLayer {
+                            tree: None,
+                            config,
+                            depth,
+                            ranges,
+                        })
+                    });
+
+                    queue.push_back(layer_id);
+                }
+
+                // TODO: pre-process local scopes at this time, rather than highlight?
+                // would solve problems with locals not working across boundaries
+            }
+
+            // Return the cursor back in the pool.
+            ts_parser.cursors.push(cursor);
+
+            // Remove all untouched layers
+            self.layers.retain(|id, _| touched.contains(&id));
+
+            Ok(())
         })
-
-        // TODO: deal with injections and update them too
     }
 
-    // fn buffer_changed -> call layer.update(range, new_text) on root layer and then all marker layers
-
-    // call this on transaction.apply() -> buffer_changed(changes)
-    //
-    // fn parse(language, old_tree, ranges)
-    //
     pub fn tree(&self) -> &Tree {
-        self.root_layer.tree()
+        self.layers[self.root].tree()
     }
-    //
-    // <!--update_for_injection(grammar)-->
-
-    // Highlighting
 
     /// Iterate over the highlighted regions for a given slice of source code.
     pub fn highlight_iter<'a>(
@@ -518,65 +760,76 @@ impl Syntax {
         source: RopeSlice<'a>,
         range: Option<std::ops::Range<usize>>,
         cancellation_flag: Option<&'a AtomicUsize>,
-        injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
     ) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a {
-        // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
-        // prevents them from being moved. But both of these values are really just
-        // pointers, so it's actually ok to move them.
+        let mut layers = self
+            .layers
+            .iter()
+            .filter_map(|(_, layer)| {
+                // TODO: if range doesn't overlap layer range, skip it
 
-        // reuse a cursor from the pool if possible
-        let mut cursor = PARSER.with(|ts_parser| {
-            let highlighter = &mut ts_parser.borrow_mut();
-            highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
+                // Reuse a cursor from the pool if available.
+                let mut cursor = PARSER.with(|ts_parser| {
+                    let highlighter = &mut ts_parser.borrow_mut();
+                    highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
+                });
+
+                // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
+                // prevents them from being moved. But both of these values are really just
+                // pointers, so it's actually ok to move them.
+                let cursor_ref =
+                    unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
+
+                // if reusing cursors & no range this resets to whole range
+                cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
+
+                let mut captures = cursor_ref
+                    .captures(
+                        &layer.config.query,
+                        layer.tree().root_node(),
+                        RopeProvider(source),
+                    )
+                    .peekable();
+
+                // If there's no captures, skip the layer
+                captures.peek()?;
+
+                Some(HighlightIterLayer {
+                    highlight_end_stack: Vec::new(),
+                    scope_stack: vec![LocalScope {
+                        inherits: false,
+                        range: 0..usize::MAX,
+                        local_defs: Vec::new(),
+                    }],
+                    cursor,
+                    _tree: None,
+                    captures,
+                    config: layer.config.as_ref(), // TODO: just reuse `layer`
+                    depth: layer.depth,            // TODO: just reuse `layer`
+                    ranges: &layer.ranges,         // TODO: temp
+                })
+            })
+            .collect::<Vec<_>>();
+
+        // HAXX: arrange layers by byte range, with deeper layers positioned first
+        layers.sort_by_key(|layer| {
+            (
+                layer.ranges.first().cloned(),
+                std::cmp::Reverse(layer.depth),
+            )
         });
-        let tree_ref = self.tree();
-        let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
-        let query_ref = &self.config.query;
-        let config_ref = self.config.as_ref();
-
-        // if reusing cursors & no range this resets to whole range
-        cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
-
-        let captures = cursor_ref
-            .captures(query_ref, tree_ref.root_node(), RopeProvider(source))
-            .peekable();
-
-        // manually craft the root layer based on the existing tree
-        let layer = HighlightIterLayer {
-            highlight_end_stack: Vec::new(),
-            scope_stack: vec![LocalScope {
-                inherits: false,
-                range: 0..usize::MAX,
-                local_defs: Vec::new(),
-            }],
-            cursor,
-            depth: 0,
-            _tree: None,
-            captures,
-            config: config_ref,
-            ranges: vec![Range {
-                start_byte: 0,
-                end_byte: usize::MAX,
-                start_point: Point::new(0, 0),
-                end_point: Point::new(usize::MAX, usize::MAX),
-            }],
-        };
 
         let mut result = HighlightIter {
             source,
-            byte_offset: range.map_or(0, |r| r.start), // TODO: simplify
-            injection_callback,
+            byte_offset: range.map_or(0, |r| r.start),
             cancellation_flag,
             iter_count: 0,
-            layers: vec![layer],
+            layers,
             next_event: None,
             last_highlight_range: None,
         };
         result.sort_layers();
         result
     }
-    // on_tokenize
-    // on_change_highlighting
 
     // Commenting
     // comment_strings_for_pos
@@ -588,245 +841,156 @@ impl Syntax {
     // indent_level_for_line
 
     // TODO: Folding
-
-    // Syntax APIs
-    // get_syntax_node_containing_range ->
-    // ...
-    // get_syntax_node_at_pos
-    // buffer_range_for_scope_at_pos
 }
 
 #[derive(Debug)]
 pub struct LanguageLayer {
     // mode
     // grammar
-    // depth
+    pub config: Arc<HighlightConfiguration>,
     pub(crate) tree: Option<Tree>,
+    pub ranges: Vec<Range>,
+    pub depth: usize,
 }
 
 impl LanguageLayer {
-    // pub fn new() -> Self {
-    //     Self { tree: None }
-    // }
-
     pub fn tree(&self) -> &Tree {
         // TODO: no unwrap
         self.tree.as_ref().unwrap()
     }
 
-    fn parse(
-        &mut self,
-        ts_parser: &mut TsParser,
-        config: &HighlightConfiguration,
-        source: &Rope,
-        _depth: usize,
-        ranges: Vec<Range>,
-    ) -> Result<(), Error> {
-        if ts_parser.parser.set_included_ranges(&ranges).is_ok() {
-            ts_parser
-                .parser
-                .set_language(config.language)
-                .map_err(|_| Error::InvalidLanguage)?;
+    fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> {
+        parser.set_included_ranges(&self.ranges).unwrap();
 
-            // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
-            let tree = ts_parser
-                .parser
-                .parse_with(
-                    &mut |byte, _| {
-                        if byte <= source.len_bytes() {
-                            let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
-                            chunk[byte - start_byte..].as_bytes()
-                        } else {
-                            // out of range
-                            &[]
-                        }
-                    },
-                    self.tree.as_ref(),
-                )
-                .ok_or(Error::Cancelled)?;
+        parser
+            .set_language(self.config.language)
+            .map_err(|_| Error::InvalidLanguage)?;
 
-            self.tree = Some(tree)
-        }
+        // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
+        let tree = parser
+            .parse_with(
+                &mut |byte, _| {
+                    if byte <= source.len_bytes() {
+                        let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
+                        chunk[byte - start_byte..].as_bytes()
+                    } else {
+                        // out of range
+                        &[]
+                    }
+                },
+                self.tree.as_ref(),
+            )
+            .ok_or(Error::Cancelled)?;
+        // unsafe { ts_parser.parser.set_cancellation_flag(None) };
+        self.tree = Some(tree);
         Ok(())
     }
-
-    pub(crate) fn generate_edits(
-        old_text: RopeSlice,
-        changeset: &ChangeSet,
-    ) -> Vec<tree_sitter::InputEdit> {
-        use Operation::*;
-        let mut old_pos = 0;
-
-        let mut edits = Vec::new();
-
-        let mut iter = changeset.changes.iter().peekable();
-
-        // TODO; this is a lot easier with Change instead of Operation.
-
-        fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) {
-            let byte = text.char_to_byte(pos); // <- attempted to index past end
-            let line = text.char_to_line(pos);
-            let line_start_byte = text.line_to_byte(line);
-            let col = byte - line_start_byte;
-
-            (byte, Point::new(line, col))
-        }
-
-        fn traverse(point: Point, text: &Tendril) -> Point {
-            let Point {
-                mut row,
-                mut column,
-            } = point;
-
-            // TODO: there should be a better way here.
-            let mut chars = text.chars().peekable();
-            while let Some(ch) = chars.next() {
-                if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
-                    row += 1;
-                    column = 0;
-                } else {
-                    column += 1;
-                }
-            }
-            Point { row, column }
-        }
-
-        while let Some(change) = iter.next() {
-            let len = match change {
-                Delete(i) | Retain(i) => *i,
-                Insert(_) => 0,
-            };
-            let mut old_end = old_pos + len;
-
-            match change {
-                Retain(_) => {}
-                Delete(_) => {
-                    let (start_byte, start_position) = point_at_pos(old_text, old_pos);
-                    let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
-
-                    // TODO: Position also needs to be byte based...
-                    // let byte = char_to_byte(old_pos)
-                    // let line = char_to_line(old_pos)
-                    // let line_start_byte = line_to_byte()
-                    // Position::new(line, line_start_byte - byte)
-
-                    // deletion
-                    edits.push(tree_sitter::InputEdit {
-                        start_byte,                       // old_pos to byte
-                        old_end_byte,                     // old_end to byte
-                        new_end_byte: start_byte,         // old_pos to byte
-                        start_position,                   // old pos to coords
-                        old_end_position,                 // old_end to coords
-                        new_end_position: start_position, // old pos to coords
-                    });
-                }
-                Insert(s) => {
-                    let (start_byte, start_position) = point_at_pos(old_text, old_pos);
-
-                    // a subsequent delete means a replace, consume it
-                    if let Some(Delete(len)) = iter.peek() {
-                        old_end = old_pos + len;
-                        let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
-
-                        iter.next();
-
-                        // replacement
-                        edits.push(tree_sitter::InputEdit {
-                            start_byte,                                    // old_pos to byte
-                            old_end_byte,                                  // old_end to byte
-                            new_end_byte: start_byte + s.len(), // old_pos to byte + s.len()
-                            start_position,                     // old pos to coords
-                            old_end_position,                   // old_end to coords
-                            new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
-                        });
-                    } else {
-                        // insert
-                        edits.push(tree_sitter::InputEdit {
-                            start_byte,                                    // old_pos to byte
-                            old_end_byte: start_byte,                      // same
-                            new_end_byte: start_byte + s.len(),            // old_pos + s.len()
-                            start_position,                                // old pos to coords
-                            old_end_position: start_position,              // same
-                            new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
-                        });
-                    }
-                }
-            }
-            old_pos = old_end;
-        }
-        edits
-    }
-
-    fn update(
-        &mut self,
-        ts_parser: &mut TsParser,
-        config: &HighlightConfiguration,
-        old_source: &Rope,
-        source: &Rope,
-        changeset: &ChangeSet,
-    ) -> Result<(), Error> {
-        if changeset.is_empty() {
-            return Ok(());
-        }
-
-        let edits = Self::generate_edits(old_source.slice(..), changeset);
-
-        // Notify the tree about all the changes
-        for edit in edits.iter().rev() {
-            // apply the edits in reverse. If we applied them in order then edit 1 would disrupt
-            // the positioning of edit 2
-            self.tree.as_mut().unwrap().edit(edit);
-        }
-
-        self.parse(
-            ts_parser,
-            config,
-            source,
-            0,
-            // TODO: what to do about this range on update
-            vec![Range {
-                start_byte: 0,
-                end_byte: usize::MAX,
-                start_point: Point::new(0, 0),
-                end_point: Point::new(usize::MAX, usize::MAX),
-            }],
-        )
-    }
-
-    // fn highlight_iter() -> same as Mode but for this layer. Mode composits these
-    // fn buffer_changed
-    // fn update(range)
-    // fn update_injections()
 }
 
-// -- refactored from tree-sitter-highlight to be able to retain state
-// TODO: add seek() to iter
+pub(crate) fn generate_edits(
+    old_text: &Rope,
+    changeset: &ChangeSet,
+) -> Vec<tree_sitter::InputEdit> {
+    use Operation::*;
+    let mut old_pos = 0;
 
-// problem: any time a layer is updated it must update it's injections on the parent (potentially
-// removing some from use)
-// can't modify to vec and exist in it at the same time since that would violate borrows
-// maybe we can do with an arena
-// maybe just caching on the top layer and nevermind the injections for now?
-//
-// Grammar {
-//  layers: Vec<Box<Layer>> to prevent memory moves when vec is modified
-// }
-// injections tracked by marker:
-// if marker areas match it's fine and update
-// if not found add new layer
-// if length 0 then area got removed, clean up the layer
-//
-// layer update:
-// if range.len = 0 then remove the layer
-// for change in changes { tree.edit(change) }
-// tree = parser.parse(.., tree, ..)
-// calculate affected range and update injections
-// injection update:
-// look for existing injections
-// if present, range = (first injection start, last injection end)
-//
-// For now cheat and just throw out non-root layers if they exist. This should still improve
-// parsing in majority of cases.
+    let mut edits = Vec::new();
+
+    if changeset.changes.is_empty() {
+        return edits;
+    }
+
+    let mut iter = changeset.changes.iter().peekable();
+
+    // TODO; this is a lot easier with Change instead of Operation.
+
+    fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) {
+        let byte = text.char_to_byte(pos); // <- attempted to index past end
+        let line = text.char_to_line(pos);
+        let line_start_byte = text.line_to_byte(line);
+        let col = byte - line_start_byte;
+
+        (byte, Point::new(line, col))
+    }
+
+    fn traverse(point: Point, text: &Tendril) -> Point {
+        let Point {
+            mut row,
+            mut column,
+        } = point;
+
+        // TODO: there should be a better way here.
+        let mut chars = text.chars().peekable();
+        while let Some(ch) = chars.next() {
+            if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
+                row += 1;
+                column = 0;
+            } else {
+                column += 1;
+            }
+        }
+        Point { row, column }
+    }
+
+    while let Some(change) = iter.next() {
+        let len = match change {
+            Delete(i) | Retain(i) => *i,
+            Insert(_) => 0,
+        };
+        let mut old_end = old_pos + len;
+
+        match change {
+            Retain(_) => {}
+            Delete(_) => {
+                let (start_byte, start_position) = point_at_pos(old_text, old_pos);
+                let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
+
+                // deletion
+                edits.push(tree_sitter::InputEdit {
+                    start_byte,                       // old_pos to byte
+                    old_end_byte,                     // old_end to byte
+                    new_end_byte: start_byte,         // old_pos to byte
+                    start_position,                   // old pos to coords
+                    old_end_position,                 // old_end to coords
+                    new_end_position: start_position, // old pos to coords
+                });
+            }
+            Insert(s) => {
+                let (start_byte, start_position) = point_at_pos(old_text, old_pos);
+
+                // a subsequent delete means a replace, consume it
+                if let Some(Delete(len)) = iter.peek() {
+                    old_end = old_pos + len;
+                    let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
+
+                    iter.next();
+
+                    // replacement
+                    edits.push(tree_sitter::InputEdit {
+                        start_byte,                                    // old_pos to byte
+                        old_end_byte,                                  // old_end to byte
+                        new_end_byte: start_byte + s.len(),            // old_pos to byte + s.len()
+                        start_position,                                // old pos to coords
+                        old_end_position,                              // old_end to coords
+                        new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
+                    });
+                } else {
+                    // insert
+                    edits.push(tree_sitter::InputEdit {
+                        start_byte,                                    // old_pos to byte
+                        old_end_byte: start_byte,                      // same
+                        new_end_byte: start_byte + s.len(),            // old_pos + s.len()
+                        start_position,                                // old pos to coords
+                        old_end_position: start_position,              // same
+                        new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
+                    });
+                }
+            }
+        }
+        old_pos = old_end;
+    }
+    edits
+}
 
 use std::sync::atomic::{AtomicUsize, Ordering};
 use std::{iter, mem, ops, str, usize};
@@ -864,8 +1028,8 @@ pub enum HighlightEvent {
 pub struct HighlightConfiguration {
     pub language: Grammar,
     pub query: Query,
+    injections_query: Query,
     combined_injections_query: Option<Query>,
-    locals_pattern_index: usize,
     highlights_pattern_index: usize,
     highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
     non_local_variable_patterns: Vec<bool>,
@@ -892,13 +1056,9 @@ struct LocalScope<'a> {
 }
 
 #[derive(Debug)]
-struct HighlightIter<'a, F>
-where
-    F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+struct HighlightIter<'a> {
     source: RopeSlice<'a>,
     byte_offset: usize,
-    injection_callback: F,
     cancellation_flag: Option<&'a AtomicUsize>,
     layers: Vec<HighlightIterLayer<'a>>,
     iter_count: usize,
@@ -938,8 +1098,8 @@ struct HighlightIterLayer<'a> {
     config: &'a HighlightConfiguration,
     highlight_end_stack: Vec<usize>,
     scope_stack: Vec<LocalScope<'a>>,
-    ranges: Vec<Range>,
     depth: usize,
+    ranges: &'a [Range],
 }
 
 impl<'a> fmt::Debug for HighlightIterLayer<'a> {
@@ -971,38 +1131,32 @@ impl HighlightConfiguration {
     ) -> Result<Self, QueryError> {
         // Concatenate the query strings, keeping track of the start offset of each section.
         let mut query_source = String::new();
-        query_source.push_str(injection_query);
-        let locals_query_offset = query_source.len();
         query_source.push_str(locals_query);
         let highlights_query_offset = query_source.len();
         query_source.push_str(highlights_query);
 
         // Construct a single query by concatenating the three query strings, but record the
         // range of pattern indices that belong to each individual string.
-        let mut query = Query::new(language, &query_source)?;
-        let mut locals_pattern_index = 0;
+        let query = Query::new(language, &query_source)?;
         let mut highlights_pattern_index = 0;
         for i in 0..(query.pattern_count()) {
             let pattern_offset = query.start_byte_for_pattern(i);
             if pattern_offset < highlights_query_offset {
-                if pattern_offset < highlights_query_offset {
-                    highlights_pattern_index += 1;
-                }
-                if pattern_offset < locals_query_offset {
-                    locals_pattern_index += 1;
-                }
+                highlights_pattern_index += 1;
             }
         }
 
+        let mut injections_query = Query::new(language, injection_query)?;
+
         // Construct a separate query just for dealing with the 'combined injections'.
         // Disable the combined injection patterns in the main query.
         let mut combined_injections_query = Query::new(language, injection_query)?;
         let mut has_combined_queries = false;
-        for pattern_index in 0..locals_pattern_index {
-            let settings = query.property_settings(pattern_index);
+        for pattern_index in 0..injections_query.pattern_count() {
+            let settings = injections_query.property_settings(pattern_index);
             if settings.iter().any(|s| &*s.key == "injection.combined") {
                 has_combined_queries = true;
-                query.disable_pattern(pattern_index);
+                injections_query.disable_pattern(pattern_index);
             } else {
                 combined_injections_query.disable_pattern(pattern_index);
             }
@@ -1034,8 +1188,6 @@ impl HighlightConfiguration {
         for (i, name) in query.capture_names().iter().enumerate() {
             let i = Some(i as u32);
             match name.as_str() {
-                "injection.content" => injection_content_capture_index = i,
-                "injection.language" => injection_language_capture_index = i,
                 "local.definition" => local_def_capture_index = i,
                 "local.definition-value" => local_def_value_capture_index = i,
                 "local.reference" => local_ref_capture_index = i,
@@ -1044,12 +1196,21 @@ impl HighlightConfiguration {
             }
         }
 
+        for (i, name) in injections_query.capture_names().iter().enumerate() {
+            let i = Some(i as u32);
+            match name.as_str() {
+                "injection.content" => injection_content_capture_index = i,
+                "injection.language" => injection_language_capture_index = i,
+                _ => {}
+            }
+        }
+
         let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
         Ok(Self {
             language,
             query,
+            injections_query,
             combined_injections_query,
-            locals_pattern_index,
             highlights_pattern_index,
             highlight_indices,
             non_local_variable_patterns,
@@ -1114,238 +1275,6 @@ impl HighlightConfiguration {
 }
 
 impl<'a> HighlightIterLayer<'a> {
-    /// Create a new 'layer' of highlighting for this document.
-    ///
-    /// In the even that the new layer contains "combined injections" (injections where multiple
-    /// disjoint ranges are parsed as one syntax tree), these will be eagerly processed and
-    /// added to the returned vector.
-    fn new<F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a>(
-        source: RopeSlice<'a>,
-        cancellation_flag: Option<&'a AtomicUsize>,
-        injection_callback: &mut F,
-        mut config: &'a HighlightConfiguration,
-        mut depth: usize,
-        mut ranges: Vec<Range>,
-    ) -> Result<Vec<Self>, Error> {
-        let mut result = Vec::with_capacity(1);
-        let mut queue = Vec::new();
-        loop {
-            // --> Tree parsing part
-
-            PARSER.with(|ts_parser| {
-                let highlighter = &mut ts_parser.borrow_mut();
-
-                if highlighter.parser.set_included_ranges(&ranges).is_ok() {
-                    highlighter
-                        .parser
-                        .set_language(config.language)
-                        .map_err(|_| Error::InvalidLanguage)?;
-
-                    unsafe { highlighter.parser.set_cancellation_flag(cancellation_flag) };
-                    let tree = highlighter
-                        .parser
-                        .parse_with(
-                            &mut |byte, _| {
-                                if byte <= source.len_bytes() {
-                                    let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
-                                    chunk[byte - start_byte..].as_bytes()
-                                } else {
-                                    // out of range
-                                    &[]
-                                }
-                            },
-                            None,
-                        )
-                        .ok_or(Error::Cancelled)?;
-                    unsafe { highlighter.parser.set_cancellation_flag(None) };
-                    let mut cursor = highlighter.cursors.pop().unwrap_or_else(QueryCursor::new);
-
-                    // Process combined injections.
-                    if let Some(combined_injections_query) = &config.combined_injections_query {
-                        let mut injections_by_pattern_index = vec![
-                            (None, Vec::new(), false);
-                            combined_injections_query
-                                .pattern_count()
-                        ];
-                        let matches = cursor.matches(
-                            combined_injections_query,
-                            tree.root_node(),
-                            RopeProvider(source),
-                        );
-                        for mat in matches {
-                            let entry = &mut injections_by_pattern_index[mat.pattern_index];
-                            let (language_name, content_node, include_children) =
-                                injection_for_match(
-                                    config,
-                                    combined_injections_query,
-                                    &mat,
-                                    source,
-                                );
-                            if language_name.is_some() {
-                                entry.0 = language_name;
-                            }
-                            if let Some(content_node) = content_node {
-                                entry.1.push(content_node);
-                            }
-                            entry.2 = include_children;
-                        }
-                        for (lang_name, content_nodes, includes_children) in
-                            injections_by_pattern_index
-                        {
-                            if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty())
-                            {
-                                if let Some(next_config) = (injection_callback)(&lang_name) {
-                                    let ranges = Self::intersect_ranges(
-                                        &ranges,
-                                        &content_nodes,
-                                        includes_children,
-                                    );
-                                    if !ranges.is_empty() {
-                                        queue.push((next_config, depth + 1, ranges));
-                                    }
-                                }
-                            }
-                        }
-                    }
-
-                    // --> Highlighting query part
-
-                    // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
-                    // prevents them from being moved. But both of these values are really just
-                    // pointers, so it's actually ok to move them.
-                    let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(&tree) };
-                    let cursor_ref =
-                        unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
-                    let captures = cursor_ref
-                        .captures(&config.query, tree_ref.root_node(), RopeProvider(source))
-                        .peekable();
-
-                    result.push(HighlightIterLayer {
-                        highlight_end_stack: Vec::new(),
-                        scope_stack: vec![LocalScope {
-                            inherits: false,
-                            range: 0..usize::MAX,
-                            local_defs: Vec::new(),
-                        }],
-                        cursor,
-                        depth,
-                        _tree: Some(tree),
-                        captures,
-                        config,
-                        ranges,
-                    });
-                }
-
-                Ok(()) // so we can use the try operator
-            })?;
-
-            if queue.is_empty() {
-                break;
-            }
-
-            let (next_config, next_depth, next_ranges) = queue.remove(0);
-            config = next_config;
-            depth = next_depth;
-            ranges = next_ranges;
-        }
-
-        Ok(result)
-    }
-
-    // Compute the ranges that should be included when parsing an injection.
-    // This takes into account three things:
-    // * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
-    // * `nodes` - Every injection takes place within a set of nodes. The injection ranges
-    //   are the ranges of those nodes.
-    // * `includes_children` - For some injections, the content nodes' children should be
-    //   excluded from the nested document, so that only the content nodes' *own* content
-    //   is reparsed. For other injections, the content nodes' entire ranges should be
-    //   reparsed, including the ranges of their children.
-    fn intersect_ranges(
-        parent_ranges: &[Range],
-        nodes: &[Node],
-        includes_children: bool,
-    ) -> Vec<Range> {
-        let mut cursor = nodes[0].walk();
-        let mut result = Vec::new();
-        let mut parent_range_iter = parent_ranges.iter();
-        let mut parent_range = parent_range_iter
-            .next()
-            .expect("Layers should only be constructed with non-empty ranges vectors");
-        for node in nodes.iter() {
-            let mut preceding_range = Range {
-                start_byte: 0,
-                start_point: Point::new(0, 0),
-                end_byte: node.start_byte(),
-                end_point: node.start_position(),
-            };
-            let following_range = Range {
-                start_byte: node.end_byte(),
-                start_point: node.end_position(),
-                end_byte: usize::MAX,
-                end_point: Point::new(usize::MAX, usize::MAX),
-            };
-
-            for excluded_range in node
-                .children(&mut cursor)
-                .filter_map(|child| {
-                    if includes_children {
-                        None
-                    } else {
-                        Some(child.range())
-                    }
-                })
-                .chain([following_range].iter().cloned())
-            {
-                let mut range = Range {
-                    start_byte: preceding_range.end_byte,
-                    start_point: preceding_range.end_point,
-                    end_byte: excluded_range.start_byte,
-                    end_point: excluded_range.start_point,
-                };
-                preceding_range = excluded_range;
-
-                if range.end_byte < parent_range.start_byte {
-                    continue;
-                }
-
-                while parent_range.start_byte <= range.end_byte {
-                    if parent_range.end_byte > range.start_byte {
-                        if range.start_byte < parent_range.start_byte {
-                            range.start_byte = parent_range.start_byte;
-                            range.start_point = parent_range.start_point;
-                        }
-
-                        if parent_range.end_byte < range.end_byte {
-                            if range.start_byte < parent_range.end_byte {
-                                result.push(Range {
-                                    start_byte: range.start_byte,
-                                    start_point: range.start_point,
-                                    end_byte: parent_range.end_byte,
-                                    end_point: parent_range.end_point,
-                                });
-                            }
-                            range.start_byte = parent_range.end_byte;
-                            range.start_point = parent_range.end_point;
-                        } else {
-                            if range.start_byte < range.end_byte {
-                                result.push(range);
-                            }
-                            break;
-                        }
-                    }
-
-                    if let Some(next_range) = parent_range_iter.next() {
-                        parent_range = next_range;
-                    } else {
-                        return result;
-                    }
-                }
-            }
-        }
-        result
-    }
-
     // First, sort scope boundaries by their byte offset in the document. At a
     // given position, emit scope endings before scope beginnings. Finally, emit
     // scope boundaries from deeper layers first.
@@ -1371,10 +1300,101 @@ impl<'a> HighlightIterLayer<'a> {
     }
 }
 
-impl<'a, F> HighlightIter<'a, F>
-where
-    F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+// Compute the ranges that should be included when parsing an injection.
+// This takes into account three things:
+// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
+// * `nodes` - Every injection takes place within a set of nodes. The injection ranges
+//   are the ranges of those nodes.
+// * `includes_children` - For some injections, the content nodes' children should be
+//   excluded from the nested document, so that only the content nodes' *own* content
+//   is reparsed. For other injections, the content nodes' entire ranges should be
+//   reparsed, including the ranges of their children.
+fn intersect_ranges(
+    parent_ranges: &[Range],
+    nodes: &[Node],
+    includes_children: bool,
+) -> Vec<Range> {
+    let mut cursor = nodes[0].walk();
+    let mut result = Vec::new();
+    let mut parent_range_iter = parent_ranges.iter();
+    let mut parent_range = parent_range_iter
+        .next()
+        .expect("Layers should only be constructed with non-empty ranges vectors");
+    for node in nodes.iter() {
+        let mut preceding_range = Range {
+            start_byte: 0,
+            start_point: Point::new(0, 0),
+            end_byte: node.start_byte(),
+            end_point: node.start_position(),
+        };
+        let following_range = Range {
+            start_byte: node.end_byte(),
+            start_point: node.end_position(),
+            end_byte: usize::MAX,
+            end_point: Point::new(usize::MAX, usize::MAX),
+        };
+
+        for excluded_range in node
+            .children(&mut cursor)
+            .filter_map(|child| {
+                if includes_children {
+                    None
+                } else {
+                    Some(child.range())
+                }
+            })
+            .chain([following_range].iter().cloned())
+        {
+            let mut range = Range {
+                start_byte: preceding_range.end_byte,
+                start_point: preceding_range.end_point,
+                end_byte: excluded_range.start_byte,
+                end_point: excluded_range.start_point,
+            };
+            preceding_range = excluded_range;
+
+            if range.end_byte < parent_range.start_byte {
+                continue;
+            }
+
+            while parent_range.start_byte <= range.end_byte {
+                if parent_range.end_byte > range.start_byte {
+                    if range.start_byte < parent_range.start_byte {
+                        range.start_byte = parent_range.start_byte;
+                        range.start_point = parent_range.start_point;
+                    }
+
+                    if parent_range.end_byte < range.end_byte {
+                        if range.start_byte < parent_range.end_byte {
+                            result.push(Range {
+                                start_byte: range.start_byte,
+                                start_point: range.start_point,
+                                end_byte: parent_range.end_byte,
+                                end_point: parent_range.end_point,
+                            });
+                        }
+                        range.start_byte = parent_range.end_byte;
+                        range.start_point = parent_range.end_point;
+                    } else {
+                        if range.start_byte < range.end_byte {
+                            result.push(range);
+                        }
+                        break;
+                    }
+                }
+
+                if let Some(next_range) = parent_range_iter.next() {
+                    parent_range = next_range;
+                } else {
+                    return result;
+                }
+            }
+        }
+    }
+    result
+}
+
+impl<'a> HighlightIter<'a> {
     fn emit_event(
         &mut self,
         offset: usize,
@@ -1405,6 +1425,12 @@ where
                             i += 1;
                             continue;
                         }
+                    } else {
+                        let layer = self.layers.remove(i + 1);
+                        PARSER.with(|ts_parser| {
+                            let highlighter = &mut ts_parser.borrow_mut();
+                            highlighter.cursors.push(layer.cursor);
+                        });
                     }
                     break;
                 }
@@ -1421,30 +1447,9 @@ where
             }
         }
     }
-
-    fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
-        if let Some(sort_key) = layer.sort_key() {
-            let mut i = 1;
-            while i < self.layers.len() {
-                if let Some(sort_key_i) = self.layers[i].sort_key() {
-                    if sort_key_i > sort_key {
-                        self.layers.insert(i, layer);
-                        return;
-                    }
-                    i += 1;
-                } else {
-                    self.layers.remove(i);
-                }
-            }
-            self.layers.push(layer);
-        }
-    }
 }
 
-impl<'a, F> Iterator for HighlightIter<'a, F>
-where
-    F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+impl<'a> Iterator for HighlightIter<'a> {
     type Item = Result<HighlightEvent, Error>;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -1504,55 +1509,12 @@ where
                 layer.highlight_end_stack.pop();
                 return self.emit_event(end_byte, Some(HighlightEvent::HighlightEnd));
             } else {
-                // return self.emit_event(self.source.len(), None);
-                return None;
+                return self.emit_event(self.source.len_bytes(), None);
             };
 
             let (mut match_, capture_index) = layer.captures.next().unwrap();
             let mut capture = match_.captures[capture_index];
 
-            // If this capture represents an injection, then process the injection.
-            if match_.pattern_index < layer.config.locals_pattern_index {
-                let (language_name, content_node, include_children) =
-                    injection_for_match(layer.config, &layer.config.query, &match_, self.source);
-
-                // Explicitly remove this match so that none of its other captures will remain
-                // in the stream of captures.
-                match_.remove();
-
-                // If a language is found with the given name, then add a new language layer
-                // to the highlighted document.
-                if let (Some(language_name), Some(content_node)) = (language_name, content_node) {
-                    if let Some(config) = (self.injection_callback)(&language_name) {
-                        let ranges = HighlightIterLayer::intersect_ranges(
-                            &self.layers[0].ranges,
-                            &[content_node],
-                            include_children,
-                        );
-                        if !ranges.is_empty() {
-                            match HighlightIterLayer::new(
-                                self.source,
-                                self.cancellation_flag,
-                                &mut self.injection_callback,
-                                config,
-                                self.layers[0].depth + 1,
-                                ranges,
-                            ) {
-                                Ok(layers) => {
-                                    for layer in layers {
-                                        self.insert_layer(layer);
-                                    }
-                                }
-                                Err(e) => return Some(Err(e)),
-                            }
-                        }
-                    }
-                }
-
-                self.sort_layers();
-                continue 'main;
-            }
-
             // Remove from the local scope stack any local scopes that have already ended.
             while range.start > layer.scope_stack.last().unwrap().range.end {
                 layer.scope_stack.pop();
@@ -1747,14 +1709,6 @@ fn injection_for_match<'a>(
     (language_name, content_node, include_children)
 }
 
-// fn shrink_and_clear<T>(vec: &mut Vec<T>, capacity: usize) {
-//     if vec.len() > capacity {
-//         vec.truncate(capacity);
-//         vec.shrink_to_fit();
-//     }
-//     vec.clear();
-// }
-
 pub struct Merge<I> {
     iter: I,
     spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
@@ -1921,6 +1875,8 @@ mod test {
         .map(String::from)
         .collect();
 
+        let loader = Loader::new(Configuration { language: vec![] });
+
         let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
         let config = HighlightConfiguration::new(
             language,
@@ -1943,7 +1899,7 @@ mod test {
             fn main() {}
         ",
         );
-        let syntax = Syntax::new(&source, Arc::new(config));
+        let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
         let tree = syntax.tree();
         let root = tree.root_node();
         assert_eq!(root.kind(), "source_file");
@@ -1970,7 +1926,7 @@ mod test {
             &doc,
             vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
         );
-        let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
+        let edits = generate_edits(&doc, transaction.changes());
         // transaction.apply(&mut state);
 
         assert_eq!(
@@ -1999,7 +1955,7 @@ mod test {
         let mut doc = Rope::from("fn test() {}");
         let transaction =
             Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
-        let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
+        let edits = generate_edits(&doc, transaction.changes());
         transaction.apply(&mut doc);
 
         assert_eq!(doc, "fn test(a: u32) {}");
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index dfc18fbea..2e34a9864 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 {
@@ -95,7 +85,7 @@ impl ChangeSet {
 
         let new_last = match self.changes.as_mut_slice() {
             [.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
-                prev.push_tendril(&fragment);
+                prev.push_str(&fragment);
                 return;
             }
             [.., last @ Delete(_)] => std::mem::replace(last, Insert(fragment)),
@@ -199,7 +189,7 @@ impl ChangeSet {
                             // TODO: cover this with a test
                             // figure out the byte index of the truncated string end
                             let (pos, _) = s.char_indices().nth(j).unwrap();
-                            s.pop_front(pos as u32);
+                            s.replace_range(0..pos, "");
                             head_a = Some(Insert(s));
                             head_b = changes_b.next();
                         }
@@ -221,9 +211,11 @@ impl ChangeSet {
                         Ordering::Greater => {
                             // figure out the byte index of the truncated string end
                             let (pos, _) = s.char_indices().nth(j).unwrap();
-                            let pos = pos as u32;
-                            changes.insert(s.subtendril(0, pos));
-                            head_a = Some(Insert(s.subtendril(pos, s.len() as u32 - pos)));
+                            let mut before = s;
+                            let after = before.split_off(pos);
+
+                            changes.insert(before);
+                            head_a = Some(Insert(after));
                             head_b = changes_b.next();
                         }
                     }
@@ -287,7 +279,7 @@ impl ChangeSet {
                 }
                 Delete(n) => {
                     let text = Cow::from(original_doc.slice(pos..pos + *n));
-                    changes.insert(Tendril::from_slice(&text));
+                    changes.insert(Tendril::from(text.as_ref()));
                     pos += n;
                 }
                 Insert(s) => {
@@ -330,7 +322,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 +411,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>,
@@ -720,19 +712,19 @@ mod test {
     #[test]
     fn optimized_composition() {
         let mut state = State::new("".into());
-        let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('h'));
+        let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from("h"));
         t1.apply(&mut state.doc);
         state.selection = state.selection.clone().map(t1.changes());
-        let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('e'));
+        let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from("e"));
         t2.apply(&mut state.doc);
         state.selection = state.selection.clone().map(t2.changes());
-        let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
+        let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l"));
         t3.apply(&mut state.doc);
         state.selection = state.selection.clone().map(t3.changes());
-        let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
+        let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l"));
         t4.apply(&mut state.doc);
         state.selection = state.selection.clone().map(t4.changes());
-        let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('o'));
+        let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from("o"));
         t5.apply(&mut state.doc);
         state.selection = state.selection.clone().map(t5.changes());
 
@@ -771,7 +763,7 @@ mod test {
 
     #[test]
     fn combine_with_utf8() {
-        const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
+        const TEST_CASE: &str = "Hello, これはヘリックスエディターです!";
 
         let empty = Rope::from("");
         let a = ChangeSet::new(&empty);
diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml
index 42dd29a89..24288697e 100644
--- a/helix-dap/Cargo.toml
+++ b/helix-dap/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "helix-dap"
-version = "0.5.0"
+version = "0.6.0"
 authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
 edition = "2018"
 license = "MPL-2.0"
@@ -12,7 +12,7 @@ homepage = "https://helix-editor.com"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-helix-core = { version = "0.5", path = "../helix-core" }
+helix-core = { version = "0.6", path = "../helix-core" }
 anyhow = "1.0"
 log = "0.4"
 serde = { version = "1.0", features = ["derive"] }
diff --git a/helix-dap/src/transport.rs b/helix-dap/src/transport.rs
index 40474e99d..783a6f5d0 100644
--- a/helix-dap/src/transport.rs
+++ b/helix-dap/src/transport.rs
@@ -36,7 +36,7 @@ pub struct Response {
 #[serde(tag = "type", rename_all = "camelCase")]
 pub enum Payload {
     // type = "event"
-    Event(Event),
+    Event(Box<Event>),
     // type = "response"
     Response(Response),
     // type = "request"
@@ -45,6 +45,7 @@ pub enum Payload {
 
 #[derive(Debug)]
 pub struct Transport {
+    #[allow(unused)]
     id: usize,
     pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
 }
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index 83b2978dc..39b537063 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "helix-lsp"
-version = "0.5.0"
+version = "0.6.0"
 authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
 edition = "2021"
 license = "MPL-2.0"
@@ -12,16 +12,16 @@ homepage = "https://helix-editor.com"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-helix-core = { version = "0.5", path = "../helix-core" }
+helix-core = { version = "0.6", path = "../helix-core" }
 
 anyhow = "1.0"
 futures-executor = "0.3"
 futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
 jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
 log = "0.4"
-lsp-types = { version = "0.91", features = ["proposed"] }
+lsp-types = { version = "0.92", features = ["proposed"] }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 thiserror = "1.0"
-tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
+tokio = { version = "1.16", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
 tokio-stream = "0.1.8"
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 271fd9d59..15cbca0eb 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -31,6 +31,7 @@ pub struct Client {
     pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
     offset_encoding: OffsetEncoding,
     config: Option<Value>,
+    root_markers: Vec<String>,
 }
 
 impl Client {
@@ -39,6 +40,7 @@ impl Client {
         cmd: &str,
         args: &[String],
         config: Option<Value>,
+        root_markers: Vec<String>,
         id: usize,
     ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
         let process = Command::new(cmd)
@@ -68,6 +70,7 @@ impl Client {
             capabilities: OnceCell::new(),
             offset_encoding: OffsetEncoding::Utf8,
             config,
+            root_markers,
         };
 
         Ok((client, server_rx, initialize_notify))
@@ -202,7 +205,7 @@ impl Client {
                 Ok(result) => Output::Success(Success {
                     jsonrpc: Some(Version::V2),
                     id,
-                    result,
+                    result: serde_json::to_value(result)?,
                 }),
                 Err(error) => Output::Failure(Failure {
                     jsonrpc: Some(Version::V2),
@@ -225,7 +228,8 @@ impl Client {
 
     pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
         // TODO: delay any requests that are triggered prior to initialize
-        let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
+        let root = find_root(None, &self.root_markers)
+            .and_then(|root| lsp::Url::from_file_path(root).ok());
 
         if self.config.is_some() {
             log::info!("Using custom LSP config: {}", self.config.as_ref().unwrap());
@@ -434,7 +438,7 @@ impl Client {
 
                     changes.push(lsp::TextDocumentContentChangeEvent {
                         range: Some(lsp::Range::new(start, end)),
-                        text: s.into(),
+                        text: s.to_string(),
                         range_length: None,
                     });
                 }
@@ -556,6 +560,14 @@ impl Client {
         self.call::<lsp::request::Completion>(params)
     }
 
+    pub async fn resolve_completion_item(
+        &self,
+        completion_item: lsp::CompletionItem,
+    ) -> Result<lsp::CompletionItem> {
+        self.request::<lsp::request::ResolveCompletionItem>(completion_item)
+            .await
+    }
+
     pub fn text_document_signature_help(
         &self,
         text_document: lsp::TextDocumentIdentifier,
@@ -800,4 +812,16 @@ impl Client {
         let response = self.request::<lsp::request::Rename>(params).await?;
         Ok(response.unwrap_or_default())
     }
+
+    pub fn command(&self, command: lsp::Command) -> impl Future<Output = Result<Value>> {
+        let params = lsp::ExecuteCommandParams {
+            command: command.command,
+            arguments: command.arguments.unwrap_or_default(),
+            work_done_progress_params: lsp::WorkDoneProgressParams {
+                work_done_token: None,
+            },
+        };
+
+        self.call::<lsp::request::ExecuteCommand>(params)
+    }
 }
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 7fa65928b..109546d05 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -66,39 +66,26 @@ pub mod util {
         pos: lsp::Position,
         offset_encoding: OffsetEncoding,
     ) -> Option<usize> {
-        let max_line = doc.lines().count().saturating_sub(1);
         let pos_line = pos.line as usize;
-        let pos_line = if pos_line > max_line {
+        if pos_line > doc.len_lines() - 1 {
             return None;
-        } else {
-            pos_line
-        };
+        }
+
         match offset_encoding {
             OffsetEncoding::Utf8 => {
-                let max_char = doc
-                    .line_to_char(max_line)
-                    .checked_add(doc.line(max_line).len_chars())?;
                 let line = doc.line_to_char(pos_line);
                 let pos = line.checked_add(pos.character as usize)?;
-                if pos <= max_char {
+                if pos <= doc.len_chars() {
                     Some(pos)
                 } else {
                     None
                 }
             }
             OffsetEncoding::Utf16 => {
-                let max_char = doc
-                    .line_to_char(max_line)
-                    .checked_add(doc.line(max_line).len_chars())?;
-                let max_cu = doc.char_to_utf16_cu(max_char);
                 let line = doc.line_to_char(pos_line);
                 let line_start = doc.char_to_utf16_cu(line);
                 let pos = line_start.checked_add(pos.character as usize)?;
-                if pos <= max_cu {
-                    Some(doc.utf16_cu_to_char(pos))
-                } else {
-                    None
-                }
+                doc.try_utf16_cu_to_char(pos).ok()
             }
         }
     }
@@ -203,6 +190,7 @@ pub mod util {
 #[derive(Debug, PartialEq, Clone)]
 pub enum MethodCall {
     WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams),
+    ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams),
 }
 
 impl MethodCall {
@@ -215,6 +203,12 @@ impl MethodCall {
                     .expect("Failed to parse WorkDoneCreate params");
                 Self::WorkDoneProgressCreate(params)
             }
+            lsp::request::ApplyWorkspaceEdit::METHOD => {
+                let params: lsp::ApplyWorkspaceEditParams = params
+                    .parse()
+                    .expect("Failed to parse ApplyWorkspaceEdit params");
+                Self::ApplyWorkspaceEdit(params)
+            }
             _ => {
                 log::warn!("unhandled lsp request: {}", method);
                 return None;
@@ -319,6 +313,7 @@ impl Registry {
                     &config.command,
                     &config.args,
                     language_config.config.clone(),
+                    language_config.roots.clone(),
                     id,
                 )?;
                 self.incoming.push(UnboundedReceiverStream::new(incoming));
@@ -337,7 +332,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/Cargo.toml b/helix-syntax/Cargo.toml
index cceec4127..855839be0 100644
--- a/helix-syntax/Cargo.toml
+++ b/helix-syntax/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "helix-syntax"
-version = "0.5.0"
+version = "0.6.0"
 authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
 edition = "2021"
 license = "MPL-2.0"
diff --git a/helix-syntax/README.md b/helix-syntax/README.md
new file mode 100644
index 000000000..bba2197a3
--- /dev/null
+++ b/helix-syntax/README.md
@@ -0,0 +1,13 @@
+helix-syntax
+============
+
+Syntax highlighting for helix, (shallow) submodules resides here.
+
+Differences from nvim-treesitter
+--------------------------------
+
+As the syntax are commonly ported from
+<https://github.com/nvim-treesitter/nvim-treesitter>.
+
+Note that we do not support the custom `#any-of` predicate which is
+supported by neovim so one needs to change it to `#match` with regex.
diff --git a/helix-syntax/build.rs b/helix-syntax/build.rs
index 28f85e74f..fa8be8b38 100644
--- a/helix-syntax/build.rs
+++ b/helix-syntax/build.rs
@@ -175,7 +175,6 @@ fn build_dir(dir: &str, language: &str) {
 fn main() {
     let ignore = vec![
         "tree-sitter-typescript".to_string(),
-        "tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34
         "tree-sitter-ocaml".to_string(),
     ];
     let dirs = collect_tree_sitter_dirs(&ignore).unwrap();
diff --git a/helix-syntax/languages/tree-sitter-comment b/helix-syntax/languages/tree-sitter-comment
new file mode 160000
index 000000000..5dd3c62f1
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-comment
@@ -0,0 +1 @@
+Subproject commit 5dd3c62f1bbe378b220fe16b317b85247898639e
diff --git a/helix-syntax/languages/tree-sitter-dart b/helix-syntax/languages/tree-sitter-dart
new file mode 160000
index 000000000..6a2537668
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-dart
@@ -0,0 +1 @@
+Subproject commit 6a25376685d1d47968c2cef06d4db8d84a70025e
diff --git a/helix-syntax/languages/tree-sitter-dockerfile b/helix-syntax/languages/tree-sitter-dockerfile
new file mode 160000
index 000000000..7af32bc04
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-dockerfile
@@ -0,0 +1 @@
+Subproject commit 7af32bc04a66ab196f5b9f92ac471f29372ae2ce
diff --git a/helix-syntax/languages/tree-sitter-elm b/helix-syntax/languages/tree-sitter-elm
new file mode 160000
index 000000000..bd50ccf66
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-elm
@@ -0,0 +1 @@
+Subproject commit bd50ccf66b42c55252ac8efc1086af4ac6bab8cd
diff --git a/helix-syntax/languages/tree-sitter-fish b/helix-syntax/languages/tree-sitter-fish
new file mode 160000
index 000000000..04e54ab65
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-fish
@@ -0,0 +1 @@
+Subproject commit 04e54ab6585dfd4fee6ddfe5849af56f101b6d4f
diff --git a/helix-syntax/languages/tree-sitter-git-commit b/helix-syntax/languages/tree-sitter-git-commit
new file mode 160000
index 000000000..066e395e1
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-git-commit
@@ -0,0 +1 @@
+Subproject commit 066e395e1107df17183cf3ae4230f1a1406cc972
diff --git a/helix-syntax/languages/tree-sitter-git-config b/helix-syntax/languages/tree-sitter-git-config
new file mode 160000
index 000000000..0e4f0baf9
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-git-config
@@ -0,0 +1 @@
+Subproject commit 0e4f0baf90b57e5aeb62dcdbf03062c6315d43ea
diff --git a/helix-syntax/languages/tree-sitter-git-diff b/helix-syntax/languages/tree-sitter-git-diff
new file mode 160000
index 000000000..c12e6ecb5
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-git-diff
@@ -0,0 +1 @@
+Subproject commit c12e6ecb54485f764250556ffd7ccb18f8e2942b
diff --git a/helix-syntax/languages/tree-sitter-git-rebase b/helix-syntax/languages/tree-sitter-git-rebase
new file mode 160000
index 000000000..332dc528f
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-git-rebase
@@ -0,0 +1 @@
+Subproject commit 332dc528f27044bc4427024dbb33e6941fc131f2
diff --git a/helix-syntax/languages/tree-sitter-go b/helix-syntax/languages/tree-sitter-go
index 2a83dfdd7..0fa917a70 160000
--- a/helix-syntax/languages/tree-sitter-go
+++ b/helix-syntax/languages/tree-sitter-go
@@ -1 +1 @@
-Subproject commit 2a83dfdd759a632651f852aa4dc0af2525fae5cd
+Subproject commit 0fa917a7022d1cd2e9b779a6a8fc5dc7fad69c75
diff --git a/helix-syntax/languages/tree-sitter-graphql b/helix-syntax/languages/tree-sitter-graphql
new file mode 160000
index 000000000..5e66e961e
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-graphql
@@ -0,0 +1 @@
+Subproject commit 5e66e961eee421786bdda8495ed1db045e06b5fe
diff --git a/helix-syntax/languages/tree-sitter-haskell b/helix-syntax/languages/tree-sitter-haskell
index 237f4eb44..b6ec26f18 160000
--- a/helix-syntax/languages/tree-sitter-haskell
+++ b/helix-syntax/languages/tree-sitter-haskell
@@ -1 +1 @@
-Subproject commit 237f4eb4417c28f643a29d795ed227246afb66f9
+Subproject commit b6ec26f181dd059eedd506fa5fbeae1b8e5556c8
diff --git a/helix-syntax/languages/tree-sitter-iex b/helix-syntax/languages/tree-sitter-iex
new file mode 160000
index 000000000..3ec55082c
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-iex
@@ -0,0 +1 @@
+Subproject commit 3ec55082cf0be015d03148be8edfdfa8c56e77f9
diff --git a/helix-syntax/languages/tree-sitter-lean b/helix-syntax/languages/tree-sitter-lean
new file mode 160000
index 000000000..d98426109
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-lean
@@ -0,0 +1 @@
+Subproject commit d98426109258b266e1e92358c5f11716d2e8f638
diff --git a/helix-syntax/languages/tree-sitter-llvm b/helix-syntax/languages/tree-sitter-llvm
new file mode 160000
index 000000000..3b213925b
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-llvm
@@ -0,0 +1 @@
+Subproject commit 3b213925b9c4f42c1acfe2e10bfbb438d9c6834d
diff --git a/helix-syntax/languages/tree-sitter-llvm-mir b/helix-syntax/languages/tree-sitter-llvm-mir
new file mode 160000
index 000000000..06fabca19
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-llvm-mir
@@ -0,0 +1 @@
+Subproject commit 06fabca19454b2dc00c1b211a7cb7ad0bc2585f1
diff --git a/helix-syntax/languages/tree-sitter-make b/helix-syntax/languages/tree-sitter-make
new file mode 160000
index 000000000..a4b918741
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-make
@@ -0,0 +1 @@
+Subproject commit a4b9187417d6be349ee5fd4b6e77b4172c6827dd
diff --git a/helix-syntax/languages/tree-sitter-markdown b/helix-syntax/languages/tree-sitter-markdown
new file mode 160000
index 000000000..ad8c32917
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-markdown
@@ -0,0 +1 @@
+Subproject commit ad8c32917a16dfbb387d1da567bf0c3fb6fffde2
diff --git a/helix-syntax/languages/tree-sitter-php b/helix-syntax/languages/tree-sitter-php
index 0d63eaf94..57f855461 160000
--- a/helix-syntax/languages/tree-sitter-php
+++ b/helix-syntax/languages/tree-sitter-php
@@ -1 +1 @@
-Subproject commit 0d63eaf94e8d6c0694551b016c802787e61b3fb2
+Subproject commit 57f855461aeeca73bd4218754fb26b5ac143f98f
diff --git a/helix-syntax/languages/tree-sitter-regex b/helix-syntax/languages/tree-sitter-regex
new file mode 160000
index 000000000..e1cfca3c7
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-regex
@@ -0,0 +1 @@
+Subproject commit e1cfca3c79896ff79842f057ea13e529b66af636
diff --git a/helix-syntax/languages/tree-sitter-rescript b/helix-syntax/languages/tree-sitter-rescript
new file mode 160000
index 000000000..761eb9126
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-rescript
@@ -0,0 +1 @@
+Subproject commit 761eb9126b65e078b1b5770ac296b4af8870f933
diff --git a/helix-syntax/languages/tree-sitter-scala b/helix-syntax/languages/tree-sitter-scala
index fb23ed9a9..0a3dd53a7 160000
--- a/helix-syntax/languages/tree-sitter-scala
+++ b/helix-syntax/languages/tree-sitter-scala
@@ -1 +1 @@
-Subproject commit fb23ed9a99da012d86b7a5059b9d8928607cce29
+Subproject commit 0a3dd53a7fc4b352a538397d054380aaa28be54c
diff --git a/helix-syntax/languages/tree-sitter-tablegen b/helix-syntax/languages/tree-sitter-tablegen
new file mode 160000
index 000000000..568dd8a93
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-tablegen
@@ -0,0 +1 @@
+Subproject commit 568dd8a937347175fd58db83d4c4cdaeb6069bd2
diff --git a/helix-syntax/languages/tree-sitter-twig b/helix-syntax/languages/tree-sitter-twig
new file mode 160000
index 000000000..b7444181f
--- /dev/null
+++ b/helix-syntax/languages/tree-sitter-twig
@@ -0,0 +1 @@
+Subproject commit b7444181fb38e603e25ea8fcdac55f9492e49c27
diff --git a/helix-syntax/languages/tree-sitter-zig b/helix-syntax/languages/tree-sitter-zig
index 1f27fd1df..93331b8bd 160000
--- a/helix-syntax/languages/tree-sitter-zig
+++ b/helix-syntax/languages/tree-sitter-zig
@@ -1 +1 @@
-Subproject commit 1f27fd1dfe7f352408f01b4894c7825f3a1d6c47
+Subproject commit 93331b8bd8b4ebee2b575490b2758f16ad4e9f30
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 43268291b..e62496f29 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "helix-term"
-version = "0.5.0"
+version = "0.6.0"
 description = "A post-modern text editor."
 authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
 edition = "2021"
@@ -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
@@ -21,18 +22,18 @@ name = "hx"
 path = "src/main.rs"
 
 [dependencies]
-helix-core = { version = "0.5", path = "../helix-core" }
-helix-view = { version = "0.5", path = "../helix-view" }
-helix-lsp = { version = "0.5", path = "../helix-lsp" }
-helix-dap = { version = "0.5", path = "../helix-dap" }
+helix-core = { version = "0.6", path = "../helix-core" }
+helix-view = { version = "0.6", path = "../helix-view" }
+helix-lsp = { version = "0.6", path = "../helix-lsp" }
+helix-dap = { version = "0.6", path = "../helix-dap" }
 
 anyhow = "1"
-once_cell = "1.8"
+once_cell = "1.9"
 
 tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
 num_cpus = "1"
 tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
-crossterm = { version = "0.22", features = ["event-stream"] }
+crossterm = { version = "0.23", features = ["event-stream"] }
 signal-hook = "0.3"
 tokio-stream = "0.1"
 futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -46,7 +47,7 @@ log = "0.4"
 fuzzy-matcher = "0.3"
 ignore = "0.4"
 # markdown doc rendering
-pulldown-cmark = { version = "0.8", default-features = false }
+pulldown-cmark = { version = "0.9", default-features = false }
 # file type detection
 content_inspector = "0.2.4"
 
diff --git a/helix-term/build.rs b/helix-term/build.rs
index 61ffa6f4f..21dd5612d 100644
--- a/helix-term/build.rs
+++ b/helix-term/build.rs
@@ -1,12 +1,17 @@
+use std::borrow::Cow;
 use std::process::Command;
 
 fn main() {
     let git_hash = Command::new("git")
-        .args(&["describe", "--dirty"])
+        .args(&["rev-parse", "HEAD"])
         .output()
-        .map(|x| String::from_utf8(x.stdout).ok())
         .ok()
-        .flatten()
-        .unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
-    println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
+        .and_then(|x| String::from_utf8(x.stdout).ok());
+
+    let version: Cow<_> = match git_hash {
+        Some(git_hash) => format!("{} ({})", env!("CARGO_PKG_VERSION"), &git_hash[..8]).into(),
+        None => env!("CARGO_PKG_VERSION").into(),
+    };
+
+    println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
 }
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 55e4bb039..52a5321fc 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,10 +1,16 @@
-use helix_core::{merge_toml_values, syntax};
+use helix_core::{merge_toml_values, pos_at_coords, syntax, Selection};
 use helix_dap::{self as dap, Payload, Request};
 use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
 use helix_view::{editor::Breakpoint, theme, Editor};
+use serde_json::json;
 
 use crate::{
-    args::Args, commands::fetch_stack_trace, compositor::Compositor, config::Config, job::Jobs, ui,
+    args::Args,
+    commands::{align_view, apply_workspace_edit, fetch_stack_trace, Align},
+    compositor::Compositor,
+    config::Config,
+    job::Jobs,
+    ui,
 };
 
 use log::{error, warn};
@@ -78,17 +84,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())
@@ -118,7 +134,7 @@ impl Application {
             // Unset path to prevent accidentally saving to the original tutor file.
             doc_mut!(editor).set_path(None)?;
         } else if !args.files.is_empty() {
-            let first = &args.files[0]; // we know it's not empty
+            let first = &args.files[0].0; // we know it's not empty
             if first.is_dir() {
                 std::env::set_current_dir(&first)?;
                 editor.new_file(Action::VerticalSplit);
@@ -126,16 +142,25 @@ impl Application {
             } else {
                 let nr_of_files = args.files.len();
                 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
-                for file in args.files {
+                for (file, pos) in args.files {
                     if file.is_dir() {
                         return Err(anyhow::anyhow!(
                             "expected a path to file, found a directory. (to open a directory pass it as first argument)"
                         ));
                     } else {
-                        editor.open(file.to_path_buf(), Action::Load)?;
+                        let doc_id = editor.open(file, Action::Load)?;
+                        // with Action::Load all documents have the same view
+                        let view_id = editor.tree.focus;
+                        let doc = editor.document_mut(doc_id).unwrap();
+                        let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
+                        doc.set_selection(view_id, pos);
                     }
                 }
                 editor.set_status(format!("Loaded {} files.", nr_of_files));
+                // align the view to center after all files are loaded,
+                // does not affect views without pos since it is at the top
+                let (view, doc) = current!(editor);
+                align_view(doc, view, Align::Center);
             }
         } else if stdin().is_tty() {
             editor.new_file(Action::VerticalSplit);
@@ -197,7 +222,6 @@ impl Application {
 
         loop {
             if self.editor.should_close() {
-                self.jobs.finish();
                 break;
             }
 
@@ -328,7 +352,7 @@ impl Application {
             None => return,
         };
         match payload {
-            Payload::Event(ev) => match ev {
+            Payload::Event(ev) => match *ev {
                 Event::Stopped(events::Stopped {
                     thread_id,
                     description,
@@ -529,12 +553,8 @@ impl Application {
 
                         // trigger textDocument/didOpen for docs that are already open
                         for doc in docs {
-                            // TODO: extract and share with editor.open
-                            let language_id = doc
-                                .language()
-                                .and_then(|s| s.split('.').last()) // source.rust
-                                .map(ToOwned::to_owned)
-                                .unwrap_or_default();
+                            let language_id =
+                                doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
 
                             tokio::spawn(language_server.text_document_did_open(
                                 doc.url().unwrap(),
@@ -549,6 +569,7 @@ impl Application {
                         let doc = self.editor.document_by_path_mut(&path);
 
                         if let Some(doc) = doc {
+                            let lang_conf = doc.language_config();
                             let text = doc.text();
 
                             let diagnostics = params
@@ -586,19 +607,31 @@ impl Application {
                                         return None;
                                     };
 
+                                    let severity =
+                                        diagnostic.severity.map(|severity| match severity {
+                                            DiagnosticSeverity::ERROR => Error,
+                                            DiagnosticSeverity::WARNING => Warning,
+                                            DiagnosticSeverity::INFORMATION => Info,
+                                            DiagnosticSeverity::HINT => Hint,
+                                            severity => unreachable!(
+                                                "unrecognized diagnostic severity: {:?}",
+                                                severity
+                                            ),
+                                        });
+
+                                    if let Some(lang_conf) = lang_conf {
+                                        if let Some(severity) = severity {
+                                            if severity < lang_conf.diagnostic_severity {
+                                                return None;
+                                            }
+                                        }
+                                    };
+
                                     Some(Diagnostic {
                                         range: Range { start, end },
                                         line: diagnostic.range.start.line as usize,
                                         message: diagnostic.message,
-                                        severity: diagnostic.severity.map(
-                                            |severity| match severity {
-                                                DiagnosticSeverity::ERROR => Error,
-                                                DiagnosticSeverity::WARNING => Warning,
-                                                DiagnosticSeverity::INFORMATION => Info,
-                                                DiagnosticSeverity::HINT => Hint,
-                                                severity => unimplemented!("{:?}", severity),
-                                            },
-                                        ),
+                                        severity,
                                         // code
                                         // source
                                     })
@@ -705,14 +738,6 @@ impl Application {
             Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
                 method, params, id, ..
             }) => {
-                let language_server = match self.editor.language_servers.get_by_id(server_id) {
-                    Some(language_server) => language_server,
-                    None => {
-                        warn!("can't find language server with id `{}`", server_id);
-                        return;
-                    }
-                };
-
                 let call = match MethodCall::parse(&method, params) {
                     Some(call) => call,
                     None => {
@@ -742,8 +767,42 @@ impl Application {
                         if spinner.is_stopped() {
                             spinner.start();
                         }
+                        let language_server =
+                            match self.editor.language_servers.get_by_id(server_id) {
+                                Some(language_server) => language_server,
+                                None => {
+                                    warn!("can't find language server with id `{}`", server_id);
+                                    return;
+                                }
+                            };
+
                         tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
                     }
+                    MethodCall::ApplyWorkspaceEdit(params) => {
+                        apply_workspace_edit(
+                            &mut self.editor,
+                            helix_lsp::OffsetEncoding::Utf8,
+                            &params.edit,
+                        );
+
+                        let language_server =
+                            match self.editor.language_servers.get_by_id(server_id) {
+                                Some(language_server) => language_server,
+                                None => {
+                                    warn!("can't find language server with id `{}`", server_id);
+                                    return;
+                                }
+                            };
+
+                        tokio::spawn(language_server.reply(
+                            id,
+                            Ok(json!(lsp::ApplyWorkspaceEditResponse {
+                                applied: true,
+                                failure_reason: None,
+                                failed_change: None,
+                            })),
+                        ));
+                    }
                 }
             }
             e => unreachable!("{:?}", e),
@@ -789,6 +848,8 @@ impl Application {
 
         self.event_loop().await;
 
+        self.jobs.finish().await;
+
         if self.editor.close_language_servers(None).await.is_err() {
             log::error!("Timed out waiting for language servers to shutdown");
         };
diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs
index 40113db92..247d5b320 100644
--- a/helix-term/src/args.rs
+++ b/helix-term/src/args.rs
@@ -1,5 +1,6 @@
 use anyhow::{Error, Result};
-use std::path::PathBuf;
+use helix_core::Position;
+use std::path::{Path, PathBuf};
 
 #[derive(Default)]
 pub struct Args {
@@ -7,7 +8,7 @@ pub struct Args {
     pub display_version: bool,
     pub load_tutor: bool,
     pub verbosity: u64,
-    pub files: Vec<PathBuf>,
+    pub files: Vec<(PathBuf, Position)>,
 }
 
 impl Args {
@@ -41,15 +42,49 @@ impl Args {
                         }
                     }
                 }
-                arg => args.files.push(PathBuf::from(arg)),
+                arg => args.files.push(parse_file(arg)),
             }
         }
 
         // push the remaining args, if any to the files
-        for filename in iter {
-            args.files.push(PathBuf::from(filename));
+        for arg in iter {
+            args.files.push(parse_file(arg));
         }
 
         Ok(args)
     }
 }
+
+/// Parse arg into [`PathBuf`] and position.
+pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) {
+    let def = || (PathBuf::from(s), Position::default());
+    if Path::new(s).exists() {
+        return def();
+    }
+    split_path_row_col(s)
+        .or_else(|| split_path_row(s))
+        .unwrap_or_else(def)
+}
+
+/// Split file.rs:10:2 into [`PathBuf`], row and col.
+///
+/// Does not validate if file.rs is a file or directory.
+fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> {
+    let mut s = s.rsplitn(3, ':');
+    let col: usize = s.next()?.parse().ok()?;
+    let row: usize = s.next()?.parse().ok()?;
+    let path = s.next()?.into();
+    let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1));
+    Some((path, pos))
+}
+
+/// Split file.rs:10 into [`PathBuf`] and row.
+///
+/// Does not validate if file.rs is a file or directory.
+fn split_path_row(s: &str) -> Option<(PathBuf, Position)> {
+    let (row, path) = s.rsplit_once(':')?;
+    let row: usize = row.parse().ok()?;
+    let path = path.into();
+    let pos = Position::new(row.saturating_sub(1), 0);
+    Some((path, pos))
+}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 1871c67e1..677943e86 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -5,15 +5,17 @@ pub use dap::*;
 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},
-    search, selection, surround, textobject,
+    search, selection, shellwords, surround, textobject,
+    tree_sitter::Node,
     unicode::width::UnicodeWidthChar,
     LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
     Transaction,
@@ -22,13 +24,15 @@ use helix_view::{
     clipboard::ClipboardType,
     document::{Mode, SCRATCH_BUFFER_NAME},
     editor::{Action, Motion},
+    info::Info,
     input::KeyEvent,
     keyboard::KeyCode,
     view::View,
     Document, DocumentId, Editor, ViewId,
 };
 
-use anyhow::{anyhow, bail, Context as _};
+use anyhow::{anyhow, bail, ensure, Context as _};
+use fuzzy_matcher::FuzzyMatcher;
 use helix_lsp::{
     block_on, lsp,
     util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
@@ -38,14 +42,15 @@ use insert::*;
 use movement::Movement;
 
 use crate::{
+    args,
     compositor::{self, Component, Compositor},
-    ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
+    ui::{self, FilePicker, Popup, Prompt, PromptEvent},
 };
 
 use crate::job::{self, Job, Jobs};
 use futures_util::{FutureExt, StreamExt};
-use std::num::NonZeroUsize;
 use std::{collections::HashMap, fmt, future::Future};
+use std::{collections::HashSet, num::NonZeroUsize};
 
 use std::{
     borrow::Cow,
@@ -73,7 +78,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)
         }));
     }
@@ -138,47 +143,76 @@ pub 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 {
+            Self::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));
+                    }
+                }
+            }
+            Self::Static { fun, .. } => (fun)(cx),
+        }
     }
 
-    pub fn name(&self) -> &'static str {
-        self.name
+    pub fn name(&self) -> &str {
+        match &self {
+            Self::Typable { name, .. } => name,
+            Self::Static { name, .. } => name,
+        }
     }
 
-    pub fn doc(&self) -> &'static str {
-        self.doc
+    pub fn doc(&self) -> &str {
+        match &self {
+            Self::Typable { doc, .. } => doc,
+            Self::Static { doc, .. } => doc,
+        }
     }
 
     #[rustfmt::skip]
-    commands!(
+    static_commands!(
         no_op, "Do nothing",
         move_char_left, "Move left",
         move_char_right, "Move right",
@@ -240,6 +274,7 @@ impl Command {
         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",
+        ensure_selections_forward, "Ensure the selection is in forward direction",
         insert_mode, "Insert before selection",
         append_mode, "Insert after selection (append)",
         command_mode, "Enter command mode",
@@ -261,16 +296,17 @@ impl Command {
         add_newline_below, "Add newline below",
         goto_type_definition, "Goto type definition",
         goto_implementation, "Goto implementation",
-        goto_file_start, "Goto file start/line",
+        goto_file_start, "Goto line number <n> else file start",
         goto_file_end, "Goto file end",
-        goto_file, "Goto files in the selection",
-        goto_file_hsplit, "Goto files in the selection in horizontal splits",
-        goto_file_vsplit, "Goto files in the selection in vertical splits",
+        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",
@@ -333,8 +369,12 @@ impl Command {
         rotate_selection_contents_forward, "Rotate selection contents forward",
         rotate_selection_contents_backward, "Rotate selections contents backward",
         expand_selection, "Expand selection to parent syntax node",
+        shrink_selection, "Shrink selection to previously expanded syntax node",
+        select_next_sibling, "Select the next sibling in the syntax tree",
+        select_prev_sibling, "Select the previous sibling in the syntax tree",
         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",
@@ -382,36 +422,56 @@ impl Command {
         rename_symbol, "Rename symbol",
         increment, "Increment",
         decrement, "Decrement",
+        record_macro, "Record macro",
+        replay_macro, "Replay 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()
+                .find(|cmd| cmd.name() == s)
+                .cloned()
+                .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>,
@@ -421,9 +481,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,
+        }
     }
 }
 
@@ -622,8 +700,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);
 }
@@ -736,7 +821,6 @@ fn align_selections(cx: &mut Context) {
     });
 
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
 }
 
 fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
@@ -770,8 +854,8 @@ fn goto_window(cx: &mut Context, align: Align) {
         Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
         Align::Bottom => last_line.saturating_sub(scrolloff + count),
     }
-    .min(last_line.saturating_sub(scrolloff))
-    .max(view.offset.row + scrolloff);
+    .max(view.offset.row + scrolloff)
+    .min(last_line.saturating_sub(scrolloff));
 
     let pos = doc.text().line_to_char(line);
 
@@ -782,7 +866,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)
 }
 
@@ -1139,7 +1223,6 @@ fn replace(cx: &mut Context) {
             });
 
             doc.apply(&transaction, view.id);
-            doc.append_changes_to_history(view.id);
         }
     })
 }
@@ -1157,7 +1240,6 @@ where
     });
 
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
 }
 
 fn switch_case(cx: &mut Context) {
@@ -1222,16 +1304,23 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
         .max(view.offset.row + scrolloff)
         .min(last_line.saturating_sub(scrolloff));
 
-    let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
+    // If cursor needs moving, replace primary selection
+    if line != cursor.row {
+        let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
 
-    let anchor = if doc.mode == Mode::Select {
-        range.anchor
-    } else {
-        head
-    };
+        let anchor = if doc.mode == Mode::Select {
+            range.anchor
+        } else {
+            head
+        };
 
-    // TODO: only manipulate main selection
-    doc.set_selection(view.id, Selection::single(anchor, head));
+        // replace primary selection with an empty selection at cursor pos
+        let prim_sel = Range::new(anchor, head);
+        let mut sel = doc.selection(view.id).clone();
+        let idx = sel.primary_index();
+        sel = sel.replace(idx, prim_sel);
+        doc.set_selection(view.id, sel);
+    }
 }
 
 fn page_up(cx: &mut Context) {
@@ -1389,6 +1478,7 @@ fn split_selection_on_newline(cx: &mut Context) {
     doc.set_selection(view.id, selection);
 }
 
+#[allow(clippy::too_many_arguments)]
 fn search_impl(
     doc: &mut Document,
     view: &mut View,
@@ -1397,6 +1487,7 @@ fn search_impl(
     movement: Movement,
     direction: Direction,
     scrolloff: usize,
+    wrap_around: bool,
 ) {
     let text = doc.text().slice(..);
     let selection = doc.selection(view.id);
@@ -1422,16 +1513,22 @@ fn search_impl(
 
     // use find_at to find the next match after the cursor, loop around the end
     // Careful, `Regex` uses `bytes` as offsets, not character indices!
-    let mat = match direction {
-        Direction::Forward => regex
-            .find_at(contents, start)
-            .or_else(|| regex.find(contents)),
-        Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
-            offset = start;
-            regex.find_iter(&contents[start..]).last()
-        }),
+    let mut mat = match direction {
+        Direction::Forward => regex.find_at(contents, start),
+        Direction::Backward => regex.find_iter(&contents[..start]).last(),
     };
-    // TODO: message on wraparound
+
+    if wrap_around && mat.is_none() {
+        mat = match direction {
+            Direction::Forward => regex.find(contents),
+            Direction::Backward => {
+                offset = start;
+                regex.find_iter(&contents[start..]).last()
+            }
+        }
+        // TODO: message on wraparound
+    }
+
     if let Some(mat) = mat {
         let start = text.byte_to_char(mat.start() + offset);
         let end = text.byte_to_char(mat.end() + offset);
@@ -1483,8 +1580,9 @@ fn rsearch(cx: &mut Context) {
 fn searcher(cx: &mut Context, direction: Direction) {
     let reg = cx.register.unwrap_or('/');
     let scrolloff = cx.editor.config.scrolloff;
+    let wrap_around = cx.editor.config.search.wrap_around;
 
-    let (_, doc) = current!(cx.editor);
+    let doc = doc!(cx.editor);
 
     // TODO: could probably share with select_on_matches?
 
@@ -1516,6 +1614,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
                 Movement::Move,
                 direction,
                 scrolloff,
+                wrap_around,
             );
         },
     );
@@ -1530,16 +1629,27 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
     if let Some(query) = registers.read('/') {
         let query = query.last().unwrap();
         let contents = doc.text().slice(..).to_string();
-        let case_insensitive = if cx.editor.config.smart_case {
+        let search_config = &cx.editor.config.search;
+        let case_insensitive = if search_config.smart_case {
             !query.chars().any(char::is_uppercase)
         } else {
             false
         };
+        let wrap_around = search_config.wrap_around;
         if let Ok(regex) = RegexBuilder::new(query)
             .case_insensitive(case_insensitive)
             .build()
         {
-            search_impl(doc, view, &contents, &regex, movement, direction, scrolloff);
+            search_impl(
+                doc,
+                view,
+                &contents,
+                &regex,
+                movement,
+                direction,
+                scrolloff,
+                wrap_around,
+            );
         } else {
             // get around warning `mutable_borrow_reservation_conflict`
             // which will be a hard error in the future
@@ -1571,14 +1681,14 @@ fn search_selection(cx: &mut Context) {
     let query = doc.selection(view.id).primary().fragment(contents);
     let regex = regex::escape(&query);
     cx.editor.registers.get_mut('/').push(regex);
-    let msg = format!("register '{}' set to '{}'", '\\', query);
+    let msg = format!("register '{}' set to '{}'", '/', query);
     cx.editor.set_status(msg);
 }
 
 fn global_search(cx: &mut Context) {
     let (all_matches_sx, all_matches_rx) =
         tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
-    let smart_case = cx.editor.config.smart_case;
+    let smart_case = cx.editor.config.search.smart_case;
     let file_picker_config = cx.editor.config.file_picker.clone();
 
     let completions = search_completions(cx, None);
@@ -1789,7 +1899,6 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
 
     match op {
         Operation::Delete => {
-            doc.append_changes_to_history(view.id);
             // exit select mode, if currently in select mode
             exit_select_mode(cx);
         }
@@ -1845,7 +1954,21 @@ fn flip_selections(cx: &mut Context) {
     let selection = doc
         .selection(view.id)
         .clone()
-        .transform(|range| Range::new(range.head, range.anchor));
+        .transform(|range| range.flip());
+    doc.set_selection(view.id, selection);
+}
+
+fn ensure_selections_forward(cx: &mut Context) {
+    let (view, doc) = current!(cx.editor);
+
+    let selection = doc
+        .selection(view.id)
+        .clone()
+        .transform(|r| match r.direction() {
+            Direction::Forward => r,
+            Direction::Backward => r.flip(),
+        });
+
     doc.set_selection(view.id, selection);
 }
 
@@ -1879,7 +2002,7 @@ fn append_mode(cx: &mut Context) {
     if !last_range.is_empty() && last_range.head == end {
         let transaction = Transaction::change(
             doc.text(),
-            std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
+            [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(),
         );
         doc.apply(&transaction, view.id);
     }
@@ -1893,7 +2016,7 @@ fn append_mode(cx: &mut Context) {
     doc.set_selection(view.id, selection);
 }
 
-mod cmd {
+pub mod cmd {
     use super::*;
 
     use helix_view::editor::Action;
@@ -1905,13 +2028,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
@@ -1926,7 +2049,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);
@@ -1936,17 +2059,25 @@ 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 (path, pos) = args::parse_file(arg);
+            let _ = cx.editor.open(path, Action::Replace)?;
+            let (view, doc) = current!(cx.editor);
+            let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
+            doc.set_selection(view.id, pos);
+            // does not affect opening a buffer without pos
+            align_view(doc, view, Align::Center);
+        }
         Ok(())
     }
 
     fn buffer_close(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let view = view!(cx.editor);
@@ -1957,7 +2088,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);
@@ -1966,15 +2097,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);
+        let doc = doc_mut!(cx.editor);
 
         if let Some(ref path) = path {
-            doc.set_path(Some(path.as_ref()))
+            doc.set_path(Some(path.as_ref().as_ref()))
                 .context("invalid filepath")?;
         }
         if doc.path().is_none() {
@@ -2003,7 +2131,7 @@ mod cmd {
 
     fn write(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_impl(cx, args.first())
@@ -2011,7 +2139,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);
@@ -2021,11 +2149,10 @@ mod cmd {
 
     fn format(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        let (_, doc) = current!(cx.editor);
-
+        let doc = doc!(cx.editor);
         if let Some(format) = doc.format() {
             let callback =
                 make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
@@ -2036,7 +2163,7 @@ mod cmd {
     }
     fn set_indent_style(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         use IndentStyle::*;
@@ -2056,7 +2183,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()
@@ -2075,7 +2202,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::*;
@@ -2119,7 +2246,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))?;
@@ -2135,7 +2262,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))?;
@@ -2150,7 +2277,7 @@ mod cmd {
 
     fn write_quit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         event: PromptEvent,
     ) -> anyhow::Result<()> {
         write_impl(cx, args.first())?;
@@ -2159,7 +2286,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())?;
@@ -2190,13 +2317,13 @@ mod cmd {
 
     fn write_all_impl(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
         quit: bool,
         force: bool,
     ) -> anyhow::Result<()> {
         let mut errors = String::new();
-
+        let jobs = &mut cx.jobs;
         // save all documents
         for doc in &mut cx.editor.documents.values_mut() {
             if doc.path().is_none() {
@@ -2204,9 +2331,23 @@ mod cmd {
                 continue;
             }
 
-            // TODO: handle error.
-            let handle = doc.save();
-            cx.jobs.add(Job::new(handle).wait_before_exiting());
+            if !doc.is_modified() {
+                continue;
+            }
+
+            let fmt = doc.auto_format().map(|fmt| {
+                let shared = fmt.shared();
+                let callback = make_format_callback(
+                    doc.id(),
+                    doc.version(),
+                    Modified::SetUnmodified,
+                    shared.clone(),
+                );
+                jobs.callback(callback);
+                shared
+            });
+            let future = doc.format_and_save(fmt);
+            jobs.add(Job::new(future).wait_before_exiting());
         }
 
         if quit {
@@ -2226,7 +2367,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)
@@ -2234,7 +2375,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)
@@ -2242,18 +2383,13 @@ 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)
     }
 
-    fn quit_all_impl(
-        editor: &mut Editor,
-        _args: &[&str],
-        _event: PromptEvent,
-        force: bool,
-    ) -> anyhow::Result<()> {
+    fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
         if !force {
             buffers_remaining_impl(editor)?;
         }
@@ -2269,23 +2405,23 @@ mod cmd {
 
     fn quit_all(
         cx: &mut compositor::Context,
-        args: &[&str],
-        event: PromptEvent,
+        _args: &[Cow<str>],
+        _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        quit_all_impl(&mut cx.editor, args, event, false)
+        quit_all_impl(cx.editor, false)
     }
 
     fn force_quit_all(
         cx: &mut compositor::Context,
-        args: &[&str],
-        event: PromptEvent,
+        _args: &[Cow<str>],
+        _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        quit_all_impl(&mut cx.editor, args, event, true)
+        quit_all_impl(cx.editor, true)
     }
 
     fn cquit(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let exit_code = args
@@ -2294,95 +2430,110 @@ mod cmd {
             .unwrap_or(1);
         cx.editor.exit_code = exit_code;
 
-        let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
-        for view_id in views {
-            cx.editor.close(view_id);
-        }
+        quit_all_impl(cx.editor, false)
+    }
 
-        Ok(())
+    fn force_cquit(
+        cx: &mut compositor::Context,
+        args: &[Cow<str>],
+        _event: PromptEvent,
+    ) -> anyhow::Result<()> {
+        let exit_code = args
+            .first()
+            .and_then(|code| code.parse::<i32>().ok())
+            .unwrap_or(1);
+        cx.editor.exit_code = exit_code;
+
+        quit_all_impl(cx.editor, true)
     }
 
     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 doc = doc!(cx.editor);
+        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 doc = doc!(cx.editor);
+        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(
@@ -2409,7 +2560,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)
@@ -2417,7 +2568,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)
@@ -2425,7 +2576,7 @@ mod cmd {
 
     fn show_clipboard_provider(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         cx.editor
@@ -2435,12 +2586,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(),
         );
 
@@ -2458,7 +2610,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")?;
@@ -2470,10 +2622,10 @@ 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);
+        let doc = doc_mut!(cx.editor);
         if let Some(label) = args.first() {
             doc.set_encoding(label)
         } else {
@@ -2486,7 +2638,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);
@@ -2495,7 +2647,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);
@@ -2509,15 +2661,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(())
@@ -2525,15 +2680,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(())
@@ -2541,7 +2699,7 @@ mod cmd {
 
     fn debug_eval(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         if let Some(debugger) = cx.editor.debugger.as_mut() {
@@ -2563,7 +2721,7 @@ mod cmd {
 
     fn debug_start(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let mut args = args.to_owned();
@@ -2571,12 +2729,12 @@ mod cmd {
             0 => None,
             _ => Some(args.remove(0)),
         };
-        dap_start_impl(cx, name, None, Some(args))
+        dap_start_impl(cx, name.as_deref(), None, Some(args))
     }
 
     fn debug_remote(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let mut args = args.to_owned();
@@ -2588,12 +2746,12 @@ mod cmd {
             0 => None,
             _ => Some(args.remove(0)),
         };
-        dap_start_impl(cx, name, address, Some(args))
+        dap_start_impl(cx, name.as_deref(), address, Some(args))
     }
 
     fn tutor(
         cx: &mut compositor::Context,
-        _args: &[&str],
+        _args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
         let path = helix_core::runtime_dir().join("tutor.txt");
@@ -2605,20 +2763,135 @@ mod cmd {
 
     pub(super) fn goto_line_number(
         cx: &mut compositor::Context,
-        args: &[&str],
+        args: &[Cow<str>],
         _event: PromptEvent,
     ) -> anyhow::Result<()> {
-        if args.is_empty() {
-            bail!("Line number required");
-        }
+        ensure!(!args.is_empty(), "Line number required");
 
         let line = args[0].parse::<usize>()?;
 
-        goto_line_impl(&mut cx.editor, NonZeroUsize::new(line));
+        goto_line_impl(cx.editor, NonZeroUsize::new(line));
 
         let (view, doc) = current!(cx.editor);
 
         view.ensure_cursor_in_view(doc, line);
+        Ok(())
+    }
+
+    fn setting(
+        cx: &mut compositor::Context,
+        args: &[Cow<str>],
+        _event: PromptEvent,
+    ) -> anyhow::Result<()> {
+        let runtime_config = &mut cx.editor.config;
+
+        if args.len() != 2 {
+            anyhow::bail!("Bad arguments. Usage: `:set key field`");
+        }
+
+        let (key, arg) = (&args[0].to_lowercase(), &args[1]);
+
+        match key.as_ref() {
+            "scrolloff" => runtime_config.scrolloff = arg.parse()?,
+            "scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
+            "mouse" => runtime_config.mouse = arg.parse()?,
+            "line-number" => runtime_config.line_number = arg.parse()?,
+            "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
+            "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
+            "auto-completion" => runtime_config.auto_completion = arg.parse()?,
+            "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
+            "auto-info" => runtime_config.auto_info = arg.parse()?,
+            "true-color" => runtime_config.true_color = arg.parse()?,
+            "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
+            "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
+            _ => anyhow::bail!("Unknown key `{}`.", args[0]),
+        }
+
+        Ok(())
+    }
+
+    fn sort(
+        cx: &mut compositor::Context,
+        args: &[Cow<str>],
+        _event: PromptEvent,
+    ) -> anyhow::Result<()> {
+        sort_impl(cx, args, false)
+    }
+
+    fn sort_reverse(
+        cx: &mut compositor::Context,
+        args: &[Cow<str>],
+        _event: PromptEvent,
+    ) -> anyhow::Result<()> {
+        sort_impl(cx, args, true)
+    }
+
+    fn sort_impl(
+        cx: &mut compositor::Context,
+        _args: &[Cow<str>],
+        reverse: bool,
+    ) -> anyhow::Result<()> {
+        let (view, doc) = current!(cx.editor);
+        let text = doc.text().slice(..);
+
+        let selection = doc.selection(view.id);
+
+        let mut fragments: Vec<_> = selection
+            .fragments(text)
+            .map(|fragment| Tendril::from(fragment.as_ref()))
+            .collect();
+
+        fragments.sort_by(match reverse {
+            true => |a: &Tendril, b: &Tendril| b.cmp(a),
+            false => |a: &Tendril, b: &Tendril| a.cmp(b),
+        });
+
+        let transaction = Transaction::change(
+            doc.text(),
+            selection
+                .into_iter()
+                .zip(fragments)
+                .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
+        );
+
+        doc.apply(&transaction, view.id);
+        doc.append_changes_to_history(view.id);
+
+        Ok(())
+    }
+
+    fn tree_sitter_subtree(
+        cx: &mut compositor::Context,
+        _args: &[Cow<str>],
+        _event: PromptEvent,
+    ) -> anyhow::Result<()> {
+        let (view, doc) = current!(cx.editor);
+
+        if let Some(syntax) = doc.syntax() {
+            let primary_selection = doc.selection(view.id).primary();
+            let text = doc.text();
+            let from = text.char_to_byte(primary_selection.from());
+            let to = text.char_to_byte(primary_selection.to());
+            if let Some(selected_node) = syntax
+                .tree()
+                .root_node()
+                .descendant_for_byte_range(from, to)
+            {
+                let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
+
+                let callback = async move {
+                    let call: job::Callback =
+                        Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+                            let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
+                            let popup = Popup::new("hover", contents);
+                            compositor.replace_or_push("hover", Box::new(popup));
+                        });
+                    Ok(call)
+                };
+
+                cx.jobs.callback(callback);
+            }
+        }
 
         Ok(())
     }
@@ -2646,18 +2919,18 @@ mod cmd {
             completer: Some(completers::filename),
         },
         TypableCommand {
-          name: "buffer-close",
-          aliases: &["bc", "bclose"],
-          doc: "Close the current buffer.",
-          fun: buffer_close,
-          completer: None, // FIXME: buffer completer
+            name: "buffer-close",
+            aliases: &["bc", "bclose"],
+            doc: "Close the current buffer.",
+            fun: buffer_close,
+            completer: None, // FIXME: buffer completer
         },
         TypableCommand {
-          name: "buffer-close!",
-          aliases: &["bc!", "bclose!"],
-          doc: "Close the current buffer forcefully (ignoring unsaved changes).",
-          fun: force_buffer_close,
-          completer: None, // FIXME: buffer completer
+            name: "buffer-close!",
+            aliases: &["bc!", "bclose!"],
+            doc: "Close the current buffer forcefully (ignoring unsaved changes).",
+            fun: force_buffer_close,
+            completer: None, // FIXME: buffer completer
         },
         TypableCommand {
             name: "write",
@@ -2676,7 +2949,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,
         },
@@ -2764,10 +3037,17 @@ mod cmd {
             fun: cquit,
             completer: None,
         },
+        TypableCommand {
+            name: "cquit!",
+            aliases: &["cq!"],
+            doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
+            fun: force_cquit,
+            completer: None,
+        },
         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),
         },
@@ -2851,7 +3131,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),
         },
@@ -2931,18 +3211,47 @@ mod cmd {
             doc: "Go to line number.",
             fun: goto_line_number,
             completer: None,
-        }
+        },
+        TypableCommand {
+            name: "set-option",
+            aliases: &["set"],
+            doc: "Set a config option at runtime",
+            fun: setting,
+            completer: Some(completers::setting),
+        },
+        TypableCommand {
+            name: "sort",
+            aliases: &[],
+            doc: "Sort ranges in selection.",
+            fun: sort,
+            completer: None,
+        },
+        TypableCommand {
+            name: "rsort",
+            aliases: &[],
+            doc: "Sort ranges in selection in reverse order.",
+            fun: sort_reverse,
+            completer: None,
+        },
+        TypableCommand {
+            name: "tree-sitter-subtree",
+            aliases: &["ts-subtree"],
+            doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
+            fun: tree_sitter_subtree,
+            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) {
@@ -2950,17 +3259,28 @@ fn command_mode(cx: &mut Context) {
         ":".into(),
         Some(':'),
         |input: &str| {
+            static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
+                Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
+
             // we use .this over split_whitespace() because we care about empty segments
             let parts = input.split(' ').collect::<Vec<&str>>();
 
             // simple heuristic: if there's no just one part, complete command name.
             // if there's a space, per command completion kicks in.
             if parts.len() <= 1 {
-                let end = 0..;
-                cmd::TYPABLE_COMMAND_LIST
+                let mut matches: Vec<_> = cmd::TYPABLE_COMMAND_LIST
                     .iter()
-                    .filter(|command| command.name.contains(input))
-                    .map(|command| (end.clone(), Cow::Borrowed(command.name)))
+                    .filter_map(|command| {
+                        FUZZY_MATCHER
+                            .fuzzy_match(command.name, input)
+                            .map(|score| (command.name, score))
+                    })
+                    .collect();
+
+                matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
+                matches
+                    .into_iter()
+                    .map(|(name, _)| (0.., name.into()))
                     .collect()
             } else {
                 let part = parts.last().unwrap();
@@ -2968,7 +3288,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()
@@ -2996,15 +3316,25 @@ fn command_mode(cx: &mut Context) {
 
             // 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, &parts[0..], event) {
+                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::COMMANDS.get(parts[0]) {
-                if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
+            if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
+                let args = if cfg!(unix) {
+                    shellwords::shellwords(input)
+                } else {
+                    // Windows doesn't support POSIX, so fallback for now
+                    parts
+                        .into_iter()
+                        .map(|part| part.into())
+                        .collect::<Vec<_>>()
+                };
+
+                if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
                     cx.editor.set_error(format!("{}", e));
                 }
             } else {
@@ -3016,7 +3346,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);
         }
 
@@ -3027,7 +3357,8 @@ fn command_mode(cx: &mut Context) {
 }
 
 fn file_picker(cx: &mut Context) {
-    let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
+    // We don't specify language markers, root will be the root of the current git repo
+    let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
     let picker = ui::file_picker(root, &cx.editor.config);
     cx.push_layer(Box::new(picker));
 }
@@ -3084,8 +3415,8 @@ fn buffer_picker(cx: &mut Context) {
             .map(|(_, doc)| new_meta(doc))
             .collect(),
         BufferMeta::format,
-        |cx, meta, _action| {
-            cx.editor.switch(meta.id, Action::Replace);
+        |cx, meta, action| {
+            cx.editor.switch(meta.id, action);
         },
         |editor, meta| {
             let doc = &editor.documents.get(&meta.id)?;
@@ -3119,7 +3450,7 @@ fn symbol_picker(cx: &mut Context) {
             nested_to_flat(list, file, child);
         }
     }
-    let (_, doc) = current!(cx.editor);
+    let doc = doc!(cx.editor);
 
     let language_server = match doc.language_server() {
         Some(language_server) => language_server,
@@ -3140,7 +3471,7 @@ fn symbol_picker(cx: &mut Context) {
                 let symbols = match symbols {
                     lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
                     lsp::DocumentSymbolResponse::Nested(symbols) => {
-                        let (_view, doc) = current!(editor);
+                        let doc = doc!(editor);
                         let mut flat_symbols = Vec::new();
                         for symbol in symbols {
                             nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
@@ -3182,17 +3513,15 @@ fn symbol_picker(cx: &mut Context) {
 }
 
 fn workspace_symbol_picker(cx: &mut Context) {
-    let (_, doc) = current!(cx.editor);
-
+    let doc = doc!(cx.editor);
+    let current_path = doc.path().cloned();
     let language_server = match doc.language_server() {
         Some(language_server) => language_server,
         None => return,
     };
     let offset_encoding = language_server.offset_encoding();
-
     let future = language_server.workspace_symbols("".to_string());
 
-    let current_path = doc_mut!(cx.editor).path().cloned();
     cx.callback(
         future,
         move |_editor: &mut Editor,
@@ -3243,6 +3572,15 @@ fn workspace_symbol_picker(cx: &mut Context) {
     )
 }
 
+impl ui::menu::Item for lsp::CodeActionOrCommand {
+    fn label(&self) -> &str {
+        match self {
+            lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
+            lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
+        }
+    }
+}
+
 pub fn code_action(cx: &mut Context) {
     let (view, doc) = current!(cx.editor);
 
@@ -3262,49 +3600,85 @@ pub fn code_action(cx: &mut Context) {
 
     cx.callback(
         future,
-        move |_editor: &mut Editor,
+        move |editor: &mut Editor,
               compositor: &mut Compositor,
               response: Option<lsp::CodeActionResponse>| {
-            if let Some(actions) = response {
-                let picker = Picker::new(
-                    true,
-                    actions,
-                    |action| match action {
-                        lsp::CodeActionOrCommand::CodeAction(action) => {
-                            action.title.as_str().into()
-                        }
-                        lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
-                    },
-                    move |cx, code_action, _action| match code_action {
-                        lsp::CodeActionOrCommand::Command(command) => {
-                            log::debug!("code action command: {:?}", command);
-                            cx.editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
-                        }
-                        lsp::CodeActionOrCommand::CodeAction(code_action) => {
-                            log::debug!("code action: {:?}", code_action);
-                            if let Some(ref workspace_edit) = code_action.edit {
-                                apply_workspace_edit(cx.editor, offset_encoding, workspace_edit)
-                            }
-                        }
-                    },
-                );
-                compositor.push(Box::new(picker))
+            let actions = match response {
+                Some(a) => a,
+                None => return,
+            };
+            if actions.is_empty() {
+                editor.set_status("No code actions available".to_owned());
+                return;
             }
+
+            let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
+                if event != PromptEvent::Validate {
+                    return;
+                }
+
+                // always present here
+                let code_action = code_action.unwrap();
+
+                match code_action {
+                    lsp::CodeActionOrCommand::Command(command) => {
+                        log::debug!("code action command: {:?}", command);
+                        execute_lsp_command(editor, command.clone());
+                    }
+                    lsp::CodeActionOrCommand::CodeAction(code_action) => {
+                        log::debug!("code action: {:?}", code_action);
+                        if let Some(ref workspace_edit) = code_action.edit {
+                            log::debug!("edit: {:?}", workspace_edit);
+                            apply_workspace_edit(editor, offset_encoding, workspace_edit);
+                        }
+
+                        // if code action provides both edit and command first the edit
+                        // should be applied and then the command
+                        if let Some(command) = &code_action.command {
+                            execute_lsp_command(editor, command.clone());
+                        }
+                    }
+                }
+            });
+            picker.move_down(); // pre-select the first item
+
+            let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
+                vertical: 1,
+                horizontal: 1,
+            });
+            compositor.replace_or_push("code-action", Box::new(popup));
         },
     )
 }
 
+pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
+    let doc = doc!(editor);
+    let language_server = match doc.language_server() {
+        Some(language_server) => language_server,
+        None => return,
+    };
+
+    // the command is executed on the server and communicated back
+    // to the client asynchronously using workspace edits
+    let command_future = language_server.command(cmd);
+    tokio::spawn(async move {
+        let res = command_future.await;
+
+        if let Err(e) = res {
+            log::error!("execute LSP command: {}", e);
+        }
+    });
+}
+
 pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
     use lsp::ResourceOp;
     use std::fs;
     match op {
         ResourceOp::Create(op) => {
             let path = op.uri.to_file_path().unwrap();
-            let ignore_if_exists = if let Some(options) = &op.options {
+            let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
                 !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
-            } else {
-                false
-            };
+            });
             if ignore_if_exists && path.exists() {
                 Ok(())
             } else {
@@ -3314,11 +3688,12 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
         ResourceOp::Delete(op) => {
             let path = op.uri.to_file_path().unwrap();
             if path.is_dir() {
-                let recursive = if let Some(options) = &op.options {
-                    options.recursive.unwrap_or(false)
-                } else {
-                    false
-                };
+                let recursive = op
+                    .options
+                    .as_ref()
+                    .and_then(|options| options.recursive)
+                    .unwrap_or(false);
+
                 if recursive {
                     fs::remove_dir_all(&path)
                 } else {
@@ -3333,11 +3708,9 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
         ResourceOp::Rename(op) => {
             let from = op.old_uri.to_file_path().unwrap();
             let to = op.new_uri.to_file_path().unwrap();
-            let ignore_if_exists = if let Some(options) = &op.options {
+            let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
                 !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
-            } else {
-                false
-            };
+            });
             if ignore_if_exists && to.exists() {
                 Ok(())
             } else {
@@ -3347,7 +3720,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
     }
 }
 
-fn apply_workspace_edit(
+pub fn apply_workspace_edit(
     editor: &mut Editor,
     offset_encoding: OffsetEncoding,
     workspace_edit: &lsp::WorkspaceEdit,
@@ -3454,7 +3827,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);
         }
@@ -3538,22 +3911,22 @@ fn open(cx: &mut Context, open: Open) {
     let mut offs = 0;
 
     let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
-        let line = range.cursor_line(text);
+        let cursor_line = range.cursor_line(text);
 
-        let line = match open {
+        let new_line = match open {
             // adjust position to the end of the line (next line - 1)
-            Open::Below => line + 1,
+            Open::Below => cursor_line + 1,
             // adjust position to the end of the previous line (current line - 1)
-            Open::Above => line,
+            Open::Above => cursor_line,
         };
 
         // Index to insert newlines after, as well as the char width
         // to use to compensate for those inserted newlines.
-        let (line_end_index, line_end_offset_width) = if line == 0 {
+        let (line_end_index, line_end_offset_width) = if new_line == 0 {
             (0, 0)
         } else {
             (
-                line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)),
+                line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
                 doc.line_ending.len_chars(),
             )
         };
@@ -3564,8 +3937,10 @@ fn open(cx: &mut Context, open: Open) {
             doc.syntax(),
             text,
             line_end_index,
+            new_line.saturating_sub(1),
             true,
-        );
+        )
+        .unwrap_or_else(|| indent::indent_level_for_line(text.line(cursor_line), doc.tab_width()));
         let indent = doc.indent_unit().repeat(indent_level);
         let indent_len = indent.len();
         let mut text = String::with_capacity(1 + indent_len);
@@ -3611,7 +3986,7 @@ fn normal_mode(cx: &mut Context) {
 
     doc.mode = Mode::Normal;
 
-    doc.append_changes_to_history(view.id);
+    try_restore_indent(doc, view.id);
 
     // if leaving append mode, move cursor back by 1
     if doc.restore_cursor {
@@ -3628,6 +4003,40 @@ fn normal_mode(cx: &mut Context) {
     }
 }
 
+fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
+    use helix_core::chars::char_is_whitespace;
+    use helix_core::Operation;
+
+    fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
+        if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
+            changes
+        {
+            move_pos + inserted_str.len() == pos
+                && inserted_str.starts_with('\n')
+                && inserted_str.chars().skip(1).all(char_is_whitespace)
+                && pos == line_end_pos // ensure no characters exists after current position
+        } else {
+            false
+        }
+    }
+
+    let doc_changes = doc.changes().changes();
+    let text = doc.text().slice(..);
+    let range = doc.selection(view_id).primary();
+    let pos = range.cursor(text);
+    let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
+
+    if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
+        // Removes tailing whitespaces.
+        let transaction =
+            Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
+                let line_start_pos = text.line_to_char(range.cursor_line(text));
+                (line_start_pos, pos, None)
+            });
+        doc.apply(&transaction, view_id);
+    }
+}
+
 // Store a jump on the jumplist.
 fn push_jump(editor: &mut Editor) {
     let (view, doc) = current!(editor);
@@ -3636,7 +4045,7 @@ fn push_jump(editor: &mut Editor) {
 }
 
 fn goto_line(cx: &mut Context) {
-    goto_line_impl(&mut cx.editor, cx.count)
+    goto_line_impl(cx.editor, cx.count)
 }
 
 fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
@@ -3702,6 +4111,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(..);
@@ -3979,27 +4402,21 @@ fn goto_pos(editor: &mut Editor, pos: usize) {
 }
 
 fn goto_first_diag(cx: &mut Context) {
-    let editor = &mut cx.editor;
-    let (_, doc) = current!(editor);
-
+    let doc = doc!(cx.editor);
     let pos = match doc.diagnostics().first() {
         Some(diag) => diag.range.start,
         None => return,
     };
-
-    goto_pos(editor, pos);
+    goto_pos(cx.editor, pos);
 }
 
 fn goto_last_diag(cx: &mut Context) {
-    let editor = &mut cx.editor;
-    let (_, doc) = current!(editor);
-
+    let doc = doc!(cx.editor);
     let pos = match doc.diagnostics().last() {
         Some(diag) => diag.range.start,
         None => return,
     };
-
-    goto_pos(editor, pos);
+    goto_pos(cx.editor, pos);
 }
 
 fn goto_next_diag(cx: &mut Context) {
@@ -4089,7 +4506,6 @@ fn signature_help(cx: &mut Context) {
     );
 }
 
-// NOTE: Transactions in this module get appended to history when we switch back to normal mode.
 pub mod insert {
     use super::*;
     pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
@@ -4184,8 +4600,10 @@ 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 t = Tendril::from_char(ch);
-        let transaction = Transaction::insert(doc, selection, t);
+        let cursors = selection.clone().cursors(doc.slice(..));
+        let mut t = Tendril::new();
+        t.push(ch);
+        let transaction = Transaction::insert(doc, &cursors, t);
         Some(transaction)
     }
 
@@ -4200,11 +4618,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;
             }
@@ -4254,48 +4672,48 @@ pub mod insert {
             };
             let curr = contents.get_char(pos).unwrap_or(' ');
 
-            // TODO: offset range.head by 1? when calculating?
+            let current_line = text.char_to_line(pos);
             let indent_level = indent::suggested_indent_for_pos(
                 doc.language_config(),
                 doc.syntax(),
                 text,
-                pos.saturating_sub(1),
+                pos,
+                current_line,
                 true,
-            );
-            let indent = doc.indent_unit().repeat(indent_level);
-            let mut text = String::with_capacity(1 + indent.len());
-            text.push_str(doc.line_ending.as_str());
-            text.push_str(&indent);
+            )
+            .unwrap_or_else(|| {
+                indent::indent_level_for_line(text.line(current_line), doc.tab_width())
+            });
 
-            let head = pos + offs + text.chars().count();
+            let indent = doc.indent_unit().repeat(indent_level);
+            let mut text = String::new();
+            // If we are between pairs (such as brackets), we want to insert an additional line which is indented one level more and place the cursor there
+            let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
+                let inner_indent = doc.indent_unit().repeat(indent_level + 1);
+                text.reserve_exact(2 + indent.len() + inner_indent.len());
+                text.push_str(doc.line_ending.as_str());
+                text.push_str(&inner_indent);
+                let new_head_pos = pos + offs + text.chars().count();
+                text.push_str(doc.line_ending.as_str());
+                text.push_str(&indent);
+                new_head_pos
+            } else {
+                text.reserve_exact(1 + indent.len());
+                text.push_str(doc.line_ending.as_str());
+                text.push_str(&indent);
+                pos + offs + text.chars().count()
+            };
 
             // TODO: range replace or extend
             // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
             // can be used with cx.mode to do replace or extend on most changes
-            ranges.push(Range::new(
-                if range.is_empty() {
-                    head
-                } else {
-                    range.anchor + offs
-                },
-                head,
-            ));
-
-            // if between a bracket pair
-            if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
-                // another newline, indent the end bracket one level less
-                let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
-                text.push_str(doc.line_ending.as_str());
-                text.push_str(&indent);
-            }
-
+            ranges.push(Range::new(new_head_pos, new_head_pos));
             offs += text.chars().count();
 
             (pos, pos, Some(text.into()))
         });
 
         transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
-        //
 
         doc.apply(&transaction, view.id);
     }
@@ -4519,11 +4937,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);
 }
 
@@ -4548,20 +4963,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);
 }
 
@@ -4576,11 +4988,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(),
     );
 
@@ -4595,7 +5008,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();
@@ -4615,7 +5028,7 @@ fn paste_impl(
             // paste append
             (Paste::After, false) => range.to(),
         };
-        (pos, pos, Some(values.next().unwrap()))
+        (pos, pos, values.next())
     });
 
     Some(transaction)
@@ -4625,13 +5038,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);
@@ -4644,22 +5058,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;
@@ -4669,12 +5104,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(&value.repeat(count)))
                     .unwrap(),
             );
             let mut values = values
                 .iter()
-                .map(|value| Tendril::from_slice(value))
+                .map(|value| Tendril::from(&value.repeat(count)))
                 .chain(repeat);
             let selection = doc.selection(view.id);
             let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@@ -4686,7 +5121,6 @@ fn replace_with_yanked(cx: &mut Context) {
             });
 
             doc.apply(&transaction, view.id);
-            doc.append_changes_to_history(view.id);
         }
     }
 }
@@ -4694,6 +5128,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);
 
@@ -4701,7 +5136,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);
@@ -4713,38 +5152,38 @@ 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);
     }
 }
 
 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);
     }
 }
 
@@ -4780,7 +5219,6 @@ fn indent(cx: &mut Context) {
         }),
     );
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
 }
 
 fn unindent(cx: &mut Context) {
@@ -4820,7 +5258,6 @@ fn unindent(cx: &mut Context) {
     let transaction = Transaction::change(doc.text(), changes.into_iter());
 
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
 }
 
 fn format_selections(cx: &mut Context) {
@@ -4867,8 +5304,6 @@ fn format_selections(cx: &mut Context) {
 
         // doc.apply(&transaction, view.id);
     }
-
-    doc.append_changes_to_history(view.id);
 }
 
 fn join_selections(cx: &mut Context) {
@@ -4911,7 +5346,6 @@ fn join_selections(cx: &mut Context) {
     // .with_selection(selection);
 
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
 }
 
 fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
@@ -5039,7 +5473,7 @@ pub fn completion(cx: &mut Context) {
         move |editor: &mut Editor,
               compositor: &mut Compositor,
               response: Option<lsp::CompletionResponse>| {
-            let (_, doc) = current!(editor);
+            let doc = doc!(editor);
             if doc.mode() != Mode::Insert {
                 // we're not in insert mode anymore
                 return;
@@ -5136,9 +5570,10 @@ 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 contents =
+                    ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover");
+                let popup = Popup::new("hover", contents);
+                compositor.replace_or_push("hover", Box::new(popup));
             }
         },
     );
@@ -5154,7 +5589,6 @@ fn toggle_comments(cx: &mut Context) {
     let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
 
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
     exit_select_mode(cx);
 }
 
@@ -5185,7 +5619,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
     let selection = doc.selection(view.id);
     let mut fragments: Vec<_> = selection
         .fragments(text)
-        .map(|fragment| Tendril::from_slice(&fragment))
+        .map(|fragment| Tendril::from(fragment.as_ref()))
         .collect();
 
     let group = count
@@ -5211,8 +5645,8 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
     );
 
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
 }
+
 fn rotate_selection_contents_forward(cx: &mut Context) {
     rotate_selection_contents(cx, Direction::Forward)
 }
@@ -5228,14 +5662,73 @@ fn expand_selection(cx: &mut Context) {
 
         if let Some(syntax) = doc.syntax() {
             let text = doc.text().slice(..);
-            let selection = object::expand_selection(syntax, text, doc.selection(view.id));
+
+            let current_selection = doc.selection(view.id);
+
+            // save current selection so it can be restored using shrink_selection
+            view.object_selections.push(current_selection.clone());
+
+            let selection = object::expand_selection(syntax, text, current_selection.clone());
             doc.set_selection(view.id, selection);
         }
     };
-    motion(&mut cx.editor);
+    motion(cx.editor);
     cx.editor.last_motion = Some(Motion(Box::new(motion)));
 }
 
+fn shrink_selection(cx: &mut Context) {
+    let motion = |editor: &mut Editor| {
+        let (view, doc) = current!(editor);
+        let current_selection = doc.selection(view.id);
+        // try to restore previous selection
+        if let Some(prev_selection) = view.object_selections.pop() {
+            if current_selection.contains(&prev_selection) {
+                // allow shrinking the selection only if current selection contains the previous object selection
+                doc.set_selection(view.id, prev_selection);
+                return;
+            } else {
+                // clear existing selection as they can't be shrinked to anyway
+                view.object_selections.clear();
+            }
+        }
+        // if not previous selection, shrink to first child
+        if let Some(syntax) = doc.syntax() {
+            let text = doc.text().slice(..);
+            let selection = object::shrink_selection(syntax, text, current_selection.clone());
+            doc.set_selection(view.id, selection);
+        }
+    };
+    motion(cx.editor);
+    cx.editor.last_motion = Some(Motion(Box::new(motion)));
+}
+
+fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
+where
+    F: Fn(Node) -> Option<Node>,
+{
+    let motion = |editor: &mut Editor| {
+        let (view, doc) = current!(editor);
+
+        if let Some(syntax) = doc.syntax() {
+            let text = doc.text().slice(..);
+            let current_selection = doc.selection(view.id);
+            let selection =
+                object::select_sibling(syntax, text, current_selection.clone(), sibling_fn);
+            doc.set_selection(view.id, selection);
+        }
+    };
+    motion(cx.editor);
+    cx.editor.last_motion = Some(Motion(Box::new(motion)));
+}
+
+fn select_next_sibling(cx: &mut Context) {
+    select_sibling_impl(cx, &|node| Node::next_sibling(&node))
+}
+
+fn select_prev_sibling(cx: &mut Context) {
+    select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
+}
+
 fn match_brackets(cx: &mut Context) {
     let (view, doc) = current!(cx.editor);
 
@@ -5288,6 +5781,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()
 }
@@ -5358,8 +5857,10 @@ fn wonly(cx: &mut Context) {
 }
 
 fn select_register(cx: &mut Context) {
+    cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers));
     cx.on_next_key(move |cx, event| {
         if let Some(ch) = event.char() {
+            cx.editor.autoinfo = None;
             cx.editor.selected_register = Some(ch);
         }
     })
@@ -5464,7 +5965,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)));
         }
     })
@@ -5479,13 +5980,16 @@ fn surround_add(cx: &mut Context) {
 
             let mut changes = Vec::with_capacity(selection.len() * 2);
             for range in selection.iter() {
-                changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
-                changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
+                let mut o = Tendril::new();
+                o.push(open);
+                let mut c = Tendril::new();
+                c.push(close);
+                changes.push((range.from(), range.from(), Some(o)));
+                changes.push((range.to(), range.to(), Some(c)));
             }
 
             let transaction = Transaction::change(doc.text(), changes.into_iter());
             doc.apply(&transaction, view.id);
-            doc.append_changes_to_history(view.id);
         }
     })
 }
@@ -5510,15 +6014,12 @@ fn surround_replace(cx: &mut Context) {
                     let transaction = Transaction::change(
                         doc.text(),
                         change_pos.iter().enumerate().map(|(i, &pos)| {
-                            (
-                                pos,
-                                pos + 1,
-                                Some(Tendril::from_char(if i % 2 == 0 { open } else { close })),
-                            )
+                            let mut t = Tendril::new();
+                            t.push(if i % 2 == 0 { open } else { close });
+                            (pos, pos + 1, Some(t))
                         }),
                     );
                     doc.apply(&transaction, view.id);
-                    doc.append_changes_to_history(view.id);
                 }
             });
         }
@@ -5541,7 +6042,6 @@ fn surround_delete(cx: &mut Context) {
             let transaction =
                 Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
             doc.apply(&transaction, view.id);
-            doc.append_changes_to_history(view.id);
         }
     })
 }
@@ -5630,9 +6130,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..])
@@ -5658,8 +6156,9 @@ fn shell_impl(
         log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
     }
 
-    let tendril = Tendril::try_from_byte_slice(&output.stdout)
+    let str = std::str::from_utf8(&output.stdout)
         .map_err(|_| anyhow!("Process did not output valid UTF-8"))?;
+    let tendril = Tendril::from(str);
     Ok((tendril, output.status.success()))
 }
 
@@ -5714,7 +6213,6 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
             if behavior != ShellBehavior::Ignore {
                 let transaction = Transaction::change(doc.text(), changes.into_iter());
                 doc.apply(&transaction, view.id);
-                doc.append_changes_to_history(view.id);
             }
 
             // after replace cursor may be out of bounds, do this to
@@ -5762,7 +6260,6 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
 
     let transaction = Transaction::change(text, changes);
     doc.apply(&transaction, view.id);
-    doc.append_changes_to_history(view.id);
 }
 
 fn rename_symbol(cx: &mut Context) {
@@ -5796,7 +6293,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));
@@ -5816,16 +6313,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 {
@@ -5833,6 +6359,58 @@ fn increment_impl(cx: &mut Context, amount: i64) {
         let transaction = transaction.with_selection(selection.clone());
 
         doc.apply(&transaction, view.id);
-        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| {
+                let s = key.to_string();
+                if s.chars().count() == 1 {
+                    s
+                } else {
+                    format!("<{}>", s)
+                }
+            })
+            .collect::<String>();
+        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 replay_macro(cx: &mut Context) {
+    let reg = cx.register.unwrap_or('@');
+    let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
+        match helix_view::input::parse_macro(keys_str) {
+            Ok(keys) => keys,
+            Err(err) => {
+                cx.editor.set_error(format!("Invalid macro: {}", err));
+                return;
+            }
+        }
+    } else {
+        cx.editor.set_error(format!("Register [{}] empty", reg));
+        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/commands/dap.rs b/helix-term/src/commands/dap.rs
index 58ef99f52..c73f9611b 100644
--- a/helix-term/src/commands/dap.rs
+++ b/helix-term/src/commands/dap.rs
@@ -194,7 +194,7 @@ pub fn dap_start_impl(
     cx: &mut compositor::Context,
     name: Option<&str>,
     socket: Option<std::net::SocketAddr>,
-    params: Option<Vec<&str>>,
+    params: Option<Vec<std::borrow::Cow<str>>>,
 ) -> Result<(), anyhow::Error> {
     let doc = doc!(cx.editor);
 
@@ -242,7 +242,7 @@ pub fn dap_start_impl(
                 let mut param = x.to_string();
                 if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) {
                     if matches!(cfg.completion.as_deref(), Some("filename" | "directory")) {
-                        param = std::fs::canonicalize(x)
+                        param = std::fs::canonicalize(x.as_ref())
                             .ok()
                             .and_then(|pb| pb.into_os_string().into_string().ok())
                             .unwrap_or_else(|| x.to_string());
@@ -408,7 +408,7 @@ fn debug_parameter_prompt(
                 cx,
                 Some(&config_name),
                 None,
-                Some(params.iter().map(|x| x.as_str()).collect()),
+                Some(params.iter().map(|x| x.into()).collect()),
             ) {
                 cx.editor.set_error(e.to_string());
             }
@@ -651,7 +651,7 @@ pub fn dap_variables(cx: &mut Context) {
     }
 
     let contents = Text::from(tui::text::Text::from(variables));
-    let popup = Popup::new(contents);
+    let popup = Popup::new("dap-variables", contents);
     cx.push_layer(Box::new(popup));
 }
 
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 3a644750e..dd7ebe1d8 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;
@@ -121,17 +126,32 @@ impl Compositor {
         self.layers.push(layer);
     }
 
+    /// Replace a component that has the given `id` with the new layer and if
+    /// no component is found, push the layer normally.
+    pub fn replace_or_push(&mut self, id: &'static str, layer: Box<dyn Component>) {
+        if let Some(component) = self.find_id(id) {
+            *component = layer;
+        } else {
+            self.push(layer)
+        }
+    }
+
     pub fn pop(&mut self) -> Option<Box<dyn Component>> {
         self.layers.pop()
     }
 
     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 +204,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/config.rs b/helix-term/src/config.rs
index 3745f871a..6b8bbc1b8 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -20,14 +20,18 @@ pub struct LspConfig {
     pub display_messages: bool,
 }
 
-#[test]
-fn parsing_keymaps_config_file() {
-    use crate::keymap;
-    use crate::keymap::Keymap;
-    use helix_core::hashmap;
-    use helix_view::document::Mode;
+#[cfg(test)]
+mod tests {
+    use super::*;
 
-    let sample_keymaps = r#"
+    #[test]
+    fn parsing_keymaps_config_file() {
+        use crate::keymap;
+        use crate::keymap::Keymap;
+        use helix_core::hashmap;
+        use helix_view::document::Mode;
+
+        let sample_keymaps = r#"
             [keys.insert]
             y = "move_line_down"
             S-C-a = "delete_selection"
@@ -36,19 +40,20 @@ fn parsing_keymaps_config_file() {
             A-F12 = "move_next_word_end"
         "#;
 
-    assert_eq!(
-        toml::from_str::<Config>(sample_keymaps).unwrap(),
-        Config {
-            keys: Keymaps(hashmap! {
-                Mode::Insert => Keymap::new(keymap!({ "Insert mode"
-                    "y" => move_line_down,
-                    "S-C-a" => delete_selection,
-                })),
-                Mode::Normal => Keymap::new(keymap!({ "Normal mode"
-                    "A-F12" => move_next_word_end,
-                })),
-            }),
-            ..Default::default()
-        }
-    );
+        assert_eq!(
+            toml::from_str::<Config>(sample_keymaps).unwrap(),
+            Config {
+                keys: Keymaps(hashmap! {
+                    Mode::Insert => Keymap::new(keymap!({ "Insert mode"
+                        "y" => move_line_down,
+                        "S-C-a" => delete_selection,
+                    })),
+                    Mode::Normal => Keymap::new(keymap!({ "Normal mode"
+                        "A-F12" => move_next_word_end,
+                    })),
+                }),
+                ..Default::default()
+            }
+        );
+    }
 }
diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs
index 4fa381748..a6a770211 100644
--- a/helix-term/src/job.rs
+++ b/helix-term/src/job.rs
@@ -22,8 +22,8 @@ pub struct Jobs {
 }
 
 impl Job {
-    pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Job {
-        Job {
+    pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Self {
+        Self {
             future: f.map(|r| r.map(|()| None)).boxed(),
             wait: false,
         }
@@ -31,22 +31,22 @@ impl Job {
 
     pub fn with_callback<F: Future<Output = anyhow::Result<Callback>> + Send + 'static>(
         f: F,
-    ) -> Job {
-        Job {
+    ) -> Self {
+        Self {
             future: f.map(|r| r.map(Some)).boxed(),
             wait: false,
         }
     }
 
-    pub fn wait_before_exiting(mut self) -> Job {
+    pub fn wait_before_exiting(mut self) -> Self {
         self.wait = true;
         self
     }
 }
 
 impl Jobs {
-    pub fn new() -> Jobs {
-        Jobs::default()
+    pub fn new() -> Self {
+        Self::default()
     }
 
     pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
@@ -93,8 +93,8 @@ impl Jobs {
     }
 
     /// Blocks until all the jobs that need to be waited on are done.
-    pub fn finish(&mut self) {
+    pub async fn finish(&mut self) {
         let wait_futures = std::mem::take(&mut self.wait_futures);
-        helix_lsp::block_on(wait_futures.for_each(|_| future::ready(())));
+        wait_futures.for_each(|_| future::ready(())).await
     }
 }
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index b317242da..e08d7e448 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);
                 )+
             )*
@@ -222,9 +222,8 @@ impl KeyTrieNode {
                 .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
                 .collect();
         }
-        Info::new(self.name(), body)
+        Info::from_keymap(self.name(), body)
     }
-
     /// Get a reference to the key trie node's order.
     pub fn order(&self) -> &[KeyEvent] {
         self.order.as_slice()
@@ -260,8 +259,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 +303,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
@@ -344,7 +343,7 @@ pub struct Keymap {
 
 impl Keymap {
     pub fn new(root: KeyTrie) -> Self {
-        Keymap {
+        Self {
             root,
             state: Vec::new(),
             sticky: None,
@@ -368,7 +367,7 @@ impl Keymap {
     /// key cancels pending keystrokes. If there are no pending keystrokes but a
     /// sticky node is in use, it will be cleared.
     pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
-        if let key!(Esc) = key {
+        if key!(Esc) == key {
             if !self.state.is_empty() {
                 return KeymapResult::new(
                     // Note that Esc is not included here
@@ -386,10 +385,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 +407,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();
@@ -477,7 +476,7 @@ impl DerefMut for Keymaps {
 }
 
 impl Default for Keymaps {
-    fn default() -> Keymaps {
+    fn default() -> Self {
         let normal = keymap!({ "Normal mode"
             "h" | "left" => move_char_left,
             "j" | "down" => move_line_down,
@@ -521,9 +520,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,
@@ -551,6 +551,11 @@ impl Default for Keymaps {
             "S" => split_selection,
             ";" => collapse_selection,
             "A-;" => flip_selections,
+            "A-k" => expand_selection,
+            "A-j" => shrink_selection,
+            "A-h" => select_prev_sibling,
+            "A-l" => select_next_sibling,
+
             "%" => select_all,
             "x" => extend_line,
             "X" => extend_to_line_bounds,
@@ -592,6 +597,9 @@ impl Default for Keymaps {
             // paste_all
             "P" => paste_before,
 
+            "Q" => record_macro,
+            "q" => replay_macro,
+
             ">" => indent,
             "<" => unindent,
             "=" => format_selections,
@@ -613,6 +621,8 @@ impl Default for Keymaps {
             "A-(" => rotate_selection_contents_backward,
             "A-)" => rotate_selection_contents_forward,
 
+            "A-:" => ensure_selections_forward,
+
             "esc" => normal_mode,
             "C-b" | "pageup" => page_up,
             "C-f" | "pagedown" => page_down,
@@ -640,7 +650,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,
@@ -763,8 +773,10 @@ impl Default for Keymaps {
             "del" => delete_char_forward,
             "C-d" => delete_char_forward,
             "ret" => insert_newline,
+            "C-j" => insert_newline,
             "tab" => insert_tab,
             "C-w" => delete_word_backward,
+            "A-backspace" => delete_word_backward,
             "A-d" => delete_word_forward,
 
             "left" => move_char_left,
@@ -779,6 +791,8 @@ impl Default for Keymaps {
             "A-left" => move_prev_word_end,
             "A-f" => move_next_word_start,
             "A-right" => move_next_word_start,
+            "A-<" => goto_file_start,
+            "A->" => goto_file_end,
             "pageup" => page_up,
             "pagedown" => page_down,
             "home" => goto_line_start,
@@ -792,7 +806,7 @@ impl Default for Keymaps {
             "C-x" => completion,
             "C-r" => insert_register,
         });
-        Keymaps(hashmap!(
+        Self(hashmap!(
             Mode::Normal => Keymap::new(normal),
             Mode::Select => Keymap::new(select),
             Mode::Insert => Keymap::new(insert),
@@ -852,36 +866,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"
         );
 
@@ -915,7 +929,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 f5e3a8cdd..58cb139c7 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/main.rs b/helix-term/src/main.rs
index 881401304..0f504046f 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -56,7 +56,7 @@ USAGE:
     hx [FLAGS] [files]...
 
 ARGS:
-    <files>...    Sets the input file to use
+    <files>...    Sets the input file to use, position can also be specified via file[:row[:col]]
 
 FLAGS:
     -h, --help       Prints help information
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index dd782d29d..35afe81e9 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -154,8 +154,19 @@ impl Completion {
                     );
                     doc.apply(&transaction, view.id);
 
-                    if let Some(additional_edits) = &item.additional_text_edits {
-                        // gopls uses this to add extra imports
+                    // apply additional edits, mostly used to auto import unqualified types
+                    let resolved_additional_text_edits = if item.additional_text_edits.is_some() {
+                        None
+                    } else {
+                        Self::resolve_completion_item(doc, item.clone())
+                            .and_then(|item| item.additional_text_edits)
+                    };
+
+                    if let Some(additional_edits) = item
+                        .additional_text_edits
+                        .as_ref()
+                        .or_else(|| resolved_additional_text_edits.as_ref())
+                    {
                         if !additional_edits.is_empty() {
                             let transaction = util::generate_transaction_from_edits(
                                 doc.text(),
@@ -168,7 +179,7 @@ impl Completion {
                 }
             };
         });
-        let popup = Popup::new(menu);
+        let popup = Popup::new("completion", menu);
         let mut completion = Self {
             popup,
             start_offset,
@@ -181,6 +192,31 @@ impl Completion {
         completion
     }
 
+    fn resolve_completion_item(
+        doc: &Document,
+        completion_item: lsp::CompletionItem,
+    ) -> Option<CompletionItem> {
+        let language_server = doc.language_server()?;
+        let completion_resolve_provider = language_server
+            .capabilities()
+            .completion_provider
+            .as_ref()?
+            .resolve_provider;
+        if completion_resolve_provider != Some(true) {
+            return None;
+        }
+
+        let future = language_server.resolve_completion_item(completion_item);
+        let response = helix_lsp::block_on(future);
+        match response {
+            Ok(completion_item) => Some(completion_item),
+            Err(err) => {
+                log::error!("execute LSP command: {}", err);
+                None
+            }
+        }
+    }
+
     pub fn recompute_filter(&mut self, editor: &Editor) {
         // recompute menu based on matches
         let menu = self.popup.contents_mut();
@@ -268,6 +304,9 @@ impl Component for Completion {
             let cursor_pos = doc.selection(view.id).primary().cursor(text);
             let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
             let cursor_pos = (coords.row - view.offset.row) as u16;
+
+            let markdown_ui =
+                |content, syn_loader| Markdown::new(content, syn_loader).style_group("completion");
             let mut markdown_doc = match &option.documentation {
                 Some(lsp::Documentation::String(contents))
                 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
@@ -275,7 +314,7 @@ impl Component for Completion {
                     value: contents,
                 })) => {
                     // TODO: convert to wrapped text
-                    Markdown::new(
+                    markdown_ui(
                         format!(
                             "```{}\n{}\n```\n{}",
                             language,
@@ -290,7 +329,7 @@ impl Component for Completion {
                     value: contents,
                 })) => {
                     // TODO: set language based on doc scope
-                    Markdown::new(
+                    markdown_ui(
                         format!(
                             "```{}\n{}\n```\n{}",
                             language,
@@ -304,7 +343,7 @@ impl Component for Completion {
                     // TODO: copied from above
 
                     // TODO: set language based on doc scope
-                    Markdown::new(
+                    markdown_ui(
                         format!(
                             "```{}\n{}\n```",
                             language,
@@ -328,8 +367,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 ac11d298b..a2131abe3 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -7,8 +7,10 @@ use crate::{
 };
 
 use helix_core::{
-    coords_at_pos,
-    graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
+    coords_at_pos, encoding,
+    graphemes::{
+        ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
+    },
     movement::Direction,
     syntax::{self, HighlightEvent},
     unicode::segmentation::UnicodeSegmentation,
@@ -17,8 +19,8 @@ use helix_core::{
 };
 use helix_view::{
     document::{Mode, SCRATCH_BUFFER_NAME},
+    editor::CursorShapeConfig,
     graphics::{CursorKind, Modifier, Rect, Style},
-    info::Info,
     input::KeyEvent,
     keyboard::{KeyCode, KeyModifiers},
     Document, Editor, Theme, View,
@@ -31,10 +33,9 @@ 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>,
 }
 
 impl Default for EditorView {
@@ -48,10 +49,9 @@ 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,
         }
     }
 
@@ -106,13 +106,12 @@ impl EditorView {
             }
         }
 
-        let highlights =
-            Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, &editor.syn_loader);
+        let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
         let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
         let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
             Box::new(syntax::merge(
                 highlights,
-                Self::doc_selection_highlights(doc, view, theme),
+                Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape),
             ))
         } else {
             Box::new(highlights)
@@ -130,8 +129,7 @@ impl EditorView {
             let x = area.right();
             let border_style = theme.get("ui.window");
             for y in area.top()..area.bottom() {
-                surface
-                    .get_mut(x, y)
+                surface[(x, y)]
                     .set_symbol(tui::symbols::line::VERTICAL)
                     //.set_symbol(" ")
                     .set_style(border_style);
@@ -154,8 +152,7 @@ impl EditorView {
         doc: &'doc Document,
         offset: Position,
         height: u16,
-        theme: &Theme,
-        loader: &syntax::Loader,
+        _theme: &Theme,
     ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
         let text = doc.text().slice(..);
         let last_line = std::cmp::min(
@@ -172,48 +169,34 @@ impl EditorView {
             start..end
         };
 
-        // TODO: range doesn't actually restrict source, just highlight range
-        let highlights = match doc.syntax() {
+        match doc.syntax() {
             Some(syntax) => {
-                let scopes = theme.scopes();
-                syntax
-                    .highlight_iter(text.slice(..), Some(range), None, |language| {
-                        loader.language_configuration_for_injection_string(language)
-                            .and_then(|language_config| {
-                                let config = language_config.highlight_config(scopes)?;
-                                let config_ref = config.as_ref();
-                                // SAFETY: the referenced `HighlightConfiguration` behind
-                                // the `Arc` is guaranteed to remain valid throughout the
-                                // duration of the highlight.
-                                let config_ref = unsafe {
-                                    std::mem::transmute::<
-                                        _,
-                                        &'static syntax::HighlightConfiguration,
-                                    >(config_ref)
-                                };
-                                Some(config_ref)
-                            })
-                    })
+                let iter = syntax
+                    // TODO: range doesn't actually restrict source, just highlight range
+                    .highlight_iter(text.slice(..), Some(range), None)
                     .map(|event| event.unwrap())
-                    .collect() // TODO: we collect here to avoid holding the lock, fix later
-            }
-            None => vec![HighlightEvent::Source {
-                start: range.start,
-                end: range.end,
-            }],
-        }
-        .into_iter()
-        .map(move |event| match event {
-            // convert byte offsets to char offset
-            HighlightEvent::Source { start, end } => {
-                let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
-                let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end));
-                HighlightEvent::Source { start, end }
-            }
-            event => event,
-        });
+                    .map(move |event| match event {
+                        // convert byte offsets to char offset
+                        HighlightEvent::Source { start, end } => {
+                            let start =
+                                text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start));
+                            let end =
+                                text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end));
+                            HighlightEvent::Source { start, end }
+                        }
+                        event => event,
+                    });
 
-        Box::new(highlights)
+                Box::new(iter)
+            }
+            None => Box::new(
+                [HighlightEvent::Source {
+                    start: text.byte_to_char(range.start),
+                    end: text.byte_to_char(range.end),
+                }]
+                .into_iter(),
+            ),
+        }
     }
 
     /// Get highlight spans for document diagnostics
@@ -245,11 +228,16 @@ impl EditorView {
         doc: &Document,
         view: &View,
         theme: &Theme,
+        cursor_shape_config: &CursorShapeConfig,
     ) -> Vec<(usize, std::ops::Range<usize>)> {
         let text = doc.text().slice(..);
         let selection = doc.selection(view.id);
         let primary_idx = selection.primary_index();
 
+        let mode = doc.mode();
+        let cursorkind = cursor_shape_config.from_mode(mode);
+        let cursor_is_block = cursorkind == CursorKind::Block;
+
         let selection_scope = theme
             .find_scope_index("ui.selection")
             .expect("could not find `ui.selection` scope in the theme!");
@@ -257,7 +245,7 @@ impl EditorView {
             .find_scope_index("ui.cursor")
             .unwrap_or(selection_scope);
 
-        let cursor_scope = match doc.mode() {
+        let cursor_scope = match mode {
             Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
             Mode::Select => theme.find_scope_index("ui.cursor.select"),
             Mode::Normal => Some(base_cursor_scope),
@@ -273,7 +261,8 @@ impl EditorView {
 
         let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
         for (i, range) in selection.iter().enumerate() {
-            let (cursor_scope, selection_scope) = if i == primary_idx {
+            let selection_is_primary = i == primary_idx;
+            let (cursor_scope, selection_scope) = if selection_is_primary {
                 (primary_cursor_scope, primary_selection_scope)
             } else {
                 (cursor_scope, selection_scope)
@@ -281,7 +270,14 @@ impl EditorView {
 
             // Special-case: cursor at end of the rope.
             if range.head == range.anchor && range.head == text.len_chars() {
-                spans.push((cursor_scope, range.head..range.head + 1));
+                if !selection_is_primary || cursor_is_block {
+                    // Bar and underline cursors are drawn by the terminal
+                    // BUG: If the editor area loses focus while having a bar or
+                    // underline cursor (eg. when a regex prompt has focus) then
+                    // the primary cursor will be invisible. This doesn't happen
+                    // with block cursors since we manually draw *all* cursors.
+                    spans.push((cursor_scope, range.head..range.head + 1));
+                }
                 continue;
             }
 
@@ -290,11 +286,15 @@ impl EditorView {
                 // Standard case.
                 let cursor_start = prev_grapheme_boundary(text, range.head);
                 spans.push((selection_scope, range.anchor..cursor_start));
-                spans.push((cursor_scope, cursor_start..range.head));
+                if !selection_is_primary || cursor_is_block {
+                    spans.push((cursor_scope, cursor_start..range.head));
+                }
             } else {
                 // Reverse case.
                 let cursor_end = next_grapheme_boundary(text, range.head);
-                spans.push((cursor_scope, range.head..cursor_end));
+                if !selection_is_primary || cursor_is_block {
+                    spans.push((cursor_scope, range.head..cursor_end));
+                }
                 spans.push((selection_scope, cursor_end..range.anchor));
             }
         }
@@ -320,6 +320,10 @@ impl EditorView {
 
         let text_style = theme.get("ui.text");
 
+        // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
+        // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
+        let text = text.slice(..);
+
         'outer: for event in highlights {
             match event {
                 HighlightEvent::HighlightStart(span) => {
@@ -336,17 +340,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,
@@ -377,6 +380,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,
@@ -422,8 +429,7 @@ impl EditorView {
                             .add_modifier(Modifier::DIM)
                     });
 
-                    surface
-                        .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
+                    surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
                         .set_style(style);
                 }
             }
@@ -453,6 +459,8 @@ impl EditorView {
 
         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);
 
@@ -468,7 +476,7 @@ impl EditorView {
                         viewport.y + i as u16,
                         &text,
                         *width,
-                        style,
+                        gutter_style.patch(style),
                     );
                 }
                 text.clear();
@@ -574,21 +582,6 @@ impl EditorView {
         }
         surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
 
-        let rel_path = doc.relative_path();
-        let path = rel_path
-            .as_ref()
-            .map(|p| p.to_string_lossy())
-            .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
-
-        let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
-        surface.set_stringn(
-            viewport.x + 8,
-            viewport.y,
-            title,
-            viewport.width.saturating_sub(6) as usize,
-            base_style,
-        );
-
         //-------------------------------
         // Right side of the status line.
         //-------------------------------
@@ -662,6 +655,13 @@ impl EditorView {
             base_style,
         ));
 
+        let enc = doc.encoding();
+        if enc != encoding::UTF_8 {
+            right_side_text
+                .0
+                .push(Span::styled(format!(" {} ", enc.name()), base_style));
+        }
+
         // Render to the statusline.
         surface.set_spans(
             viewport.x
@@ -672,6 +672,31 @@ impl EditorView {
             &right_side_text,
             right_side_text.width() as u16,
         );
+
+        //-------------------------------
+        // Middle / File path / Title
+        //-------------------------------
+        let title = {
+            let rel_path = doc.relative_path();
+            let path = rel_path
+                .as_ref()
+                .map(|p| p.to_string_lossy())
+                .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
+            format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
+        };
+
+        surface.set_string_truncated(
+            viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
+            viewport.y,
+            title,
+            viewport
+                .width
+                .saturating_sub(6)
+                .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
+            base_style,
+            true,
+            true,
+        );
     }
 
     /// Handle events by looking them up in `self.keymaps`. Returns None
@@ -684,12 +709,13 @@ impl EditorView {
         cxt: &mut commands::Context,
         event: KeyEvent,
     ) -> Option<KeymapResult> {
+        cxt.editor.autoinfo = None;
         let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
-        self.autoinfo = key_result.sticky.map(|node| node.infobox());
+        cxt.editor.autoinfo = key_result.sticky.map(|node| node.infobox());
 
         match &key_result.kind {
             KeymapResultKind::Matched(command) => command.execute(cxt),
-            KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
+            KeymapResultKind::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()),
             KeymapResultKind::MatchedSequence(commands) => {
                 for command in commands {
                     command.execute(cxt);
@@ -789,8 +815,9 @@ impl EditorView {
 
     pub fn clear_completion(&mut self, editor: &mut Editor) {
         self.completion = None;
+
         // Clear any savepoints
-        let (_, doc) = current!(editor);
+        let doc = doc_mut!(editor);
         doc.savepoint = None;
         editor.clear_idle_timer(); // don't retrigger
     }
@@ -927,7 +954,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)
             }
@@ -953,9 +980,9 @@ impl EditorView {
                     if let Ok(pos) = doc.text().try_line_to_char(line) {
                         doc.set_selection(view_id, Selection::point(pos));
                         if modifiers == crossterm::event::KeyModifiers::ALT {
-                            commands::Command::dap_edit_log.execute(cxt);
+                            commands::MappableCommand::dap_edit_log.execute(cxt);
                         } else {
-                            commands::Command::dap_edit_condition.execute(cxt);
+                            commands::MappableCommand::dap_edit_condition.execute(cxt);
                         }
 
                         return EventResult::Consumed(None);
@@ -977,7 +1004,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);
                 }
@@ -991,7 +1019,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);
                 }
 
@@ -1004,14 +1032,18 @@ 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,
+    fn handle_event(
+        &mut self,
+        event: Event,
+        context: &mut crate::compositor::Context,
+    ) -> EventResult {
+        let mut cx = commands::Context {
+            editor: context.editor,
             count: None,
             register: None,
             callback: None,
             on_next_key_callback: None,
-            jobs: cx.jobs,
+            jobs: context.jobs,
         };
 
         match event {
@@ -1021,18 +1053,19 @@ impl Component for EditorView {
                 EventResult::Consumed(None)
             }
             Event::Key(key) => {
-                cxt.editor.reset_idle_timer();
+                cx.editor.reset_idle_timer();
                 let mut key = KeyEvent::from(key);
                 canonicalize_key(&mut key);
-                // clear status
-                cxt.editor.status_msg = None;
 
-                let (_, doc) = current!(cxt.editor);
+                // clear status
+                cx.editor.status_msg = None;
+
+                let doc = doc!(cx.editor);
                 let mode = doc.mode();
 
                 if let Some(on_next_key) = self.on_next_key.take() {
                     // if there's a command waiting input, do that first
-                    on_next_key(&mut cxt, key);
+                    on_next_key(&mut cx, key);
                 } else {
                     match mode {
                         Mode::Insert => {
@@ -1044,8 +1077,8 @@ impl Component for EditorView {
                             if let Some(completion) = &mut self.completion {
                                 // use a fake context here
                                 let mut cx = Context {
-                                    editor: cxt.editor,
-                                    jobs: cxt.jobs,
+                                    editor: cx.editor,
+                                    jobs: cx.jobs,
                                     scroll: None,
                                 };
                                 let res = completion.handle_event(event, &mut cx);
@@ -1055,40 +1088,46 @@ impl Component for EditorView {
 
                                     if callback.is_some() {
                                         // assume close_fn
-                                        self.clear_completion(cxt.editor);
+                                        self.clear_completion(cx.editor);
                                     }
                                 }
                             }
 
                             // if completion didn't take the event, we pass it onto commands
                             if !consumed {
-                                self.insert_mode(&mut cxt, key);
+                                self.insert_mode(&mut cx, key);
 
                                 // lastly we recalculate completion
                                 if let Some(completion) = &mut self.completion {
-                                    completion.update(&mut cxt);
+                                    completion.update(&mut cx);
                                     if completion.is_empty() {
-                                        self.clear_completion(cxt.editor);
+                                        self.clear_completion(cx.editor);
                                     }
                                 }
                             }
                         }
-                        mode => self.command_mode(mode, &mut cxt, key),
+                        mode => self.command_mode(mode, &mut cx, key),
                     }
                 }
 
-                self.on_next_key = cxt.on_next_key_callback.take();
+                self.on_next_key = cx.on_next_key_callback.take();
                 // appease borrowck
-                let callback = cxt.callback.take();
+                let callback = cx.callback.take();
 
                 // if the command consumed the last view, skip the render.
                 // on the next loop cycle the Application will then terminate.
-                if cxt.editor.should_close() {
+                if cx.editor.should_close() {
                     return EventResult::Ignored;
                 }
 
-                let (view, doc) = current!(cxt.editor);
-                view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff);
+                let (view, doc) = current!(cx.editor);
+                view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
+
+                // Store a history state if not in insert mode. This also takes care of
+                // commiting changes when leaving insert mode.
+                if doc.mode() != Mode::Insert {
+                    doc.append_changes_to_history(view.id);
+                }
 
                 // mode transitions
                 match (mode, doc.mode()) {
@@ -1117,7 +1156,7 @@ impl Component for EditorView {
                 EventResult::Consumed(callback)
             }
 
-            Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt),
+            Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
         }
     }
 
@@ -1134,8 +1173,9 @@ impl Component for EditorView {
         }
 
         if cx.editor.config.auto_info {
-            if let Some(ref mut info) = self.autoinfo {
+            if let Some(mut info) = cx.editor.autoinfo.take() {
                 info.render(area, surface, cx);
+                cx.editor.autoinfo = Some(info)
             }
         }
 
@@ -1173,13 +1213,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() {
@@ -1188,11 +1246,11 @@ impl Component for EditorView {
     }
 
     fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
-        // match view.doc.mode() {
-        //     Mode::Insert => write!(stdout, "\x1B[6 q"),
-        //     mode => write!(stdout, "\x1B[2 q"),
-        // };
-        editor.cursor()
+        match editor.cursor() {
+            // All block cursors are drawn manually
+            (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
+            cursor => cursor,
+        }
     }
 }
 
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index ca8303dd9..6a7b641ad 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -21,6 +21,9 @@ pub struct Markdown {
     contents: String,
 
     config_loader: Arc<syntax::Loader>,
+
+    block_style: String,
+    heading_style: String,
 }
 
 // TODO: pre-render and self reference via Pin
@@ -31,120 +34,137 @@ impl Markdown {
         Self {
             contents,
             config_loader,
+            block_style: "markup.raw.inline".into(),
+            heading_style: "markup.heading".into(),
         }
     }
-}
 
-fn parse<'a>(
-    contents: &'a str,
-    theme: Option<&Theme>,
-    loader: &syntax::Loader,
-) -> tui::text::Text<'a> {
-    // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
-    // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n        Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n                         .map(|&x| x * 2)\n                         .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n    .map(|&x| x as u8)\n    .map(|x| (x + 1) as char)\n    .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
-
-    let mut options = Options::empty();
-    options.insert(Options::ENABLE_STRIKETHROUGH);
-    let parser = Parser::new_ext(contents, options);
-
-    // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
-    let mut tags = Vec::new();
-    let mut spans = Vec::new();
-    let mut lines = Vec::new();
-
-    fn to_span(text: pulldown_cmark::CowStr) -> Span {
-        use std::ops::Deref;
-        Span::raw::<std::borrow::Cow<_>>(match text {
-            CowStr::Borrowed(s) => s.into(),
-            CowStr::Boxed(s) => s.to_string().into(),
-            CowStr::Inlined(s) => s.deref().to_owned().into(),
-        })
+    pub fn style_group(mut self, suffix: &str) -> Self {
+        self.block_style = format!("markup.raw.inline.{}", suffix);
+        self.heading_style = format!("markup.heading.{}", suffix);
+        self
     }
 
-    let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
+    fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
+        // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
+        // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n        Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n                         .map(|&x| x * 2)\n                         .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n    .map(|&x| x as u8)\n    .map(|x| (x + 1) as char)\n    .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
 
-    // TODO: use better scopes for these, `markup.raw.block`, `markup.heading`
-    let code_style = theme
-        .map(|theme| theme.get("ui.text.focus"))
-        .unwrap_or_default(); // white
-    let heading_style = theme
-        .map(|theme| theme.get("ui.linenr.selected"))
-        .unwrap_or_default(); // lilac
+        let mut options = Options::empty();
+        options.insert(Options::ENABLE_STRIKETHROUGH);
+        let parser = Parser::new_ext(&self.contents, options);
 
-    for event in parser {
-        match event {
-            Event::Start(tag) => tags.push(tag),
-            Event::End(tag) => {
-                tags.pop();
-                match tag {
-                    Tag::Heading(_) | Tag::Paragraph | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => {
-                        // whenever code block or paragraph closes, new line
-                        let spans = std::mem::take(&mut spans);
-                        if !spans.is_empty() {
-                            lines.push(Spans::from(spans));
+        // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+        let mut tags = Vec::new();
+        let mut spans = Vec::new();
+        let mut lines = Vec::new();
+
+        fn to_span(text: pulldown_cmark::CowStr) -> Span {
+            use std::ops::Deref;
+            Span::raw::<std::borrow::Cow<_>>(match text {
+                CowStr::Borrowed(s) => s.into(),
+                CowStr::Boxed(s) => s.to_string().into(),
+                CowStr::Inlined(s) => s.deref().to_owned().into(),
+            })
+        }
+
+        macro_rules! get_theme {
+            ($s1: expr) => {
+                theme
+                    .map(|theme| theme.try_get($s1.as_str()))
+                    .flatten()
+                    .unwrap_or_default()
+            };
+        }
+        let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
+        let code_style = get_theme!(self.block_style);
+        let heading_style = get_theme!(self.heading_style);
+
+        for event in parser {
+            match event {
+                Event::Start(tag) => tags.push(tag),
+                Event::End(tag) => {
+                    tags.pop();
+                    match tag {
+                        Tag::Heading(_, _, _)
+                        | Tag::Paragraph
+                        | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => {
+                            // whenever code block or paragraph closes, new line
+                            let spans = std::mem::take(&mut spans);
+                            if !spans.is_empty() {
+                                lines.push(Spans::from(spans));
+                            }
+                            lines.push(Spans::default());
                         }
-                        lines.push(Spans::default());
+                        _ => (),
                     }
-                    _ => (),
                 }
-            }
-            Event::Text(text) => {
-                // TODO: temp workaround
-                if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
-                    if let Some(theme) = theme {
-                        let rope = Rope::from(text.as_ref());
-                        let syntax = loader
-                            .language_configuration_for_injection_string(language)
-                            .and_then(|config| config.highlight_config(theme.scopes()))
-                            .map(|config| Syntax::new(&rope, config));
+                Event::Text(text) => {
+                    // TODO: temp workaround
+                    if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
+                        if let Some(theme) = theme {
+                            let rope = Rope::from(text.as_ref());
+                            let syntax = self
+                                .config_loader
+                                .language_configuration_for_injection_string(language)
+                                .and_then(|config| config.highlight_config(theme.scopes()))
+                                .map(|config| {
+                                    Syntax::new(&rope, config, self.config_loader.clone())
+                                });
 
-                        if let Some(syntax) = syntax {
-                            // if we have a syntax available, highlight_iter and generate spans
-                            let mut highlights = Vec::new();
+                            if let Some(syntax) = syntax {
+                                // if we have a syntax available, highlight_iter and generate spans
+                                let mut highlights = Vec::new();
 
-                            for event in syntax.highlight_iter(rope.slice(..), None, None, |_| None)
-                            {
-                                match event.unwrap() {
-                                    HighlightEvent::HighlightStart(span) => {
-                                        highlights.push(span);
-                                    }
-                                    HighlightEvent::HighlightEnd => {
-                                        highlights.pop();
-                                    }
-                                    HighlightEvent::Source { start, end } => {
-                                        let style = match highlights.first() {
-                                            Some(span) => theme.get(&theme.scopes()[span.0]),
-                                            None => text_style,
-                                        };
-
-                                        // TODO: replace tabs with indentation
-
-                                        let mut slice = &text[start..end];
-                                        // TODO: do we need to handle all unicode line endings
-                                        // here, or is just '\n' okay?
-                                        while let Some(end) = slice.find('\n') {
-                                            // emit span up to newline
-                                            let text = &slice[..end];
-                                            let text = text.replace('\t', "    "); // replace tabs
-                                            let span = Span::styled(text, style);
-                                            spans.push(span);
-
-                                            // truncate slice to after newline
-                                            slice = &slice[end + 1..];
-
-                                            // make a new line
-                                            let spans = std::mem::take(&mut spans);
-                                            lines.push(Spans::from(spans));
+                                for event in syntax.highlight_iter(rope.slice(..), None, None) {
+                                    match event.unwrap() {
+                                        HighlightEvent::HighlightStart(span) => {
+                                            highlights.push(span);
                                         }
+                                        HighlightEvent::HighlightEnd => {
+                                            highlights.pop();
+                                        }
+                                        HighlightEvent::Source { start, end } => {
+                                            let style = match highlights.first() {
+                                                Some(span) => theme.get(&theme.scopes()[span.0]),
+                                                None => text_style,
+                                            };
 
-                                        // if there's anything left, emit it too
-                                        if !slice.is_empty() {
-                                            let span =
-                                                Span::styled(slice.replace('\t', "    "), style);
-                                            spans.push(span);
+                                            // TODO: replace tabs with indentation
+
+                                            let mut slice = &text[start..end];
+                                            // TODO: do we need to handle all unicode line endings
+                                            // here, or is just '\n' okay?
+                                            while let Some(end) = slice.find('\n') {
+                                                // emit span up to newline
+                                                let text = &slice[..end];
+                                                let text = text.replace('\t', "    "); // replace tabs
+                                                let span = Span::styled(text, style);
+                                                spans.push(span);
+
+                                                // truncate slice to after newline
+                                                slice = &slice[end + 1..];
+
+                                                // make a new line
+                                                let spans = std::mem::take(&mut spans);
+                                                lines.push(Spans::from(spans));
+                                            }
+
+                                            // if there's anything left, emit it too
+                                            if !slice.is_empty() {
+                                                let span = Span::styled(
+                                                    slice.replace('\t', "    "),
+                                                    style,
+                                                );
+                                                spans.push(span);
+                                            }
                                         }
                                     }
                                 }
+                            } else {
+                                for line in text.lines() {
+                                    let span = Span::styled(line.to_string(), code_style);
+                                    lines.push(Spans::from(span));
+                                }
                             }
                         } else {
                             for line in text.lines() {
@@ -152,64 +172,60 @@ fn parse<'a>(
                                 lines.push(Spans::from(span));
                             }
                         }
+                    } else if let Some(Tag::Heading(_, _, _)) = tags.last() {
+                        let mut span = to_span(text);
+                        span.style = heading_style;
+                        spans.push(span);
                     } else {
-                        for line in text.lines() {
-                            let span = Span::styled(line.to_string(), code_style);
-                            lines.push(Spans::from(span));
-                        }
+                        let mut span = to_span(text);
+                        span.style = text_style;
+                        spans.push(span);
                     }
-                } else if let Some(Tag::Heading(_)) = tags.last() {
+                }
+                Event::Code(text) | Event::Html(text) => {
                     let mut span = to_span(text);
-                    span.style = heading_style;
-                    spans.push(span);
-                } else {
-                    let mut span = to_span(text);
-                    span.style = text_style;
+                    span.style = code_style;
                     spans.push(span);
                 }
+                Event::SoftBreak | Event::HardBreak => {
+                    // let spans = std::mem::replace(&mut spans, Vec::new());
+                    // lines.push(Spans::from(spans));
+                    spans.push(Span::raw(" "));
+                }
+                Event::Rule => {
+                    let mut span = Span::raw("---");
+                    span.style = code_style;
+                    lines.push(Spans::from(span));
+                    lines.push(Spans::default());
+                }
+                // TaskListMarker(bool) true if checked
+                _ => {
+                    log::warn!("unhandled markdown event {:?}", event);
+                }
             }
-            Event::Code(text) | Event::Html(text) => {
-                let mut span = to_span(text);
-                span.style = code_style;
-                spans.push(span);
-            }
-            Event::SoftBreak | Event::HardBreak => {
-                // let spans = std::mem::replace(&mut spans, Vec::new());
-                // lines.push(Spans::from(spans));
-                spans.push(Span::raw(" "));
-            }
-            Event::Rule => {
-                let mut span = Span::raw("---");
-                span.style = code_style;
-                lines.push(Spans::from(span));
-                lines.push(Spans::default());
-            }
-            // TaskListMarker(bool) true if checked
-            _ => {
-                log::warn!("unhandled markdown event {:?}", event);
+            // build up a vec of Paragraph tui widgets
+        }
+
+        if !spans.is_empty() {
+            lines.push(Spans::from(spans));
+        }
+
+        // if last line is empty, remove it
+        if let Some(line) = lines.last() {
+            if line.0.is_empty() {
+                lines.pop();
             }
         }
-        // build up a vec of Paragraph tui widgets
-    }
 
-    if !spans.is_empty() {
-        lines.push(Spans::from(spans));
+        Text::from(lines)
     }
-
-    // if last line is empty, remove it
-    if let Some(line) = lines.last() {
-        if line.0.is_empty() {
-            lines.pop();
-        }
-    }
-
-    Text::from(lines)
 }
+
 impl Component for Markdown {
     fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
         use tui::widgets::{Paragraph, Widget, Wrap};
 
-        let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
+        let text = self.parse(Some(&cx.editor.theme));
 
         let par = Paragraph::new(text)
             .wrap(Wrap { trim: false })
@@ -227,7 +243,8 @@ impl Component for Markdown {
         if padding >= viewport.1 || padding >= viewport.0 {
             return None;
         }
-        let contents = parse(&self.contents, None, &self.config_loader);
+        let contents = self.parse(None);
+
         // TODO: account for tab width
         let max_text_width = (viewport.0 - padding).min(120);
         let mut text_width = 0;
@@ -241,11 +258,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 e891c1492..f9a0438c5 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -14,11 +14,18 @@ use helix_view::{graphics::Rect, Editor};
 use tui::layout::Constraint;
 
 pub trait Item {
-    fn sort_text(&self) -> &str;
-    fn filter_text(&self) -> &str;
-
     fn label(&self) -> &str;
-    fn row(&self) -> Row;
+
+    fn sort_text(&self) -> &str {
+        self.label()
+    }
+    fn filter_text(&self) -> &str {
+        self.label()
+    }
+
+    fn row(&self) -> Row {
+        Row::new(vec![Cell::from(self.label())])
+    }
 }
 
 pub struct Menu<T: Item> {
@@ -132,7 +139,17 @@ impl<T: Item> Menu<T> {
 
             acc
         });
-        let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
+
+        let height = self.matches.len().min(10).min(viewport.1 as usize);
+        // do all the matches fit on a single screen?
+        let fits = self.matches.len() <= height;
+
+        let mut len = max_lens.iter().sum::<usize>() + n;
+
+        if !fits {
+            len += 1; // +1: reserve some space for scrollbar
+        }
+
         let width = len.min(viewport.0 as usize);
 
         self.widths = max_lens
@@ -140,8 +157,6 @@ impl<T: Item> Menu<T> {
             .map(|len| Constraint::Length(len as u16))
             .collect();
 
-        let height = self.matches.len().min(10).min(viewport.1 as usize);
-
         self.size = (width as u16, height as u16);
 
         // adjust scroll offsets if size changed
@@ -190,7 +205,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 +217,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);
@@ -297,12 +312,14 @@ impl<T: Item + 'static> Component for Menu<T> {
             },
         );
 
+        let fits = len <= win_height;
+
         for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
             let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
 
-            if is_marked {
-                let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16);
-                cell.set_symbol("▐ ");
+            if !fits && is_marked {
+                let cell = &mut surface[(area.x + area.width - 2, area.y + i as u16)];
+                cell.set_symbol("▐");
                 // cell.set_style(selected);
                 // cell.set_style(if is_marked { selected } else { style });
             }
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 3c203326c..49f7b2fa3 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -2,7 +2,7 @@ mod completion;
 pub(crate) mod editor;
 mod info;
 mod markdown;
-mod menu;
+pub mod menu;
 mod picker;
 mod popup;
 mod prompt;
@@ -65,7 +65,7 @@ pub fn regex_prompt(
                         return;
                     }
 
-                    let case_insensitive = if cx.editor.config.smart_case {
+                    let case_insensitive = if cx.editor.config.search.smart_case {
                         !input.chars().any(char::is_uppercase)
                     } else {
                         false
@@ -174,7 +174,9 @@ pub mod completers {
     use crate::ui::prompt::Completion;
     use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
     use fuzzy_matcher::FuzzyMatcher;
+    use helix_view::editor::Config;
     use helix_view::theme;
+    use once_cell::sync::Lazy;
     use std::borrow::Cow;
     use std::cmp::Reverse;
 
@@ -186,6 +188,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()
@@ -207,6 +210,31 @@ pub mod completers {
         names
     }
 
+    pub fn setting(input: &str) -> Vec<Completion> {
+        static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
+            serde_json::to_value(Config::default())
+                .unwrap()
+                .as_object()
+                .unwrap()
+                .keys()
+                .cloned()
+                .collect()
+        });
+
+        let matcher = Matcher::default();
+
+        let mut matches: Vec<_> = KEYS
+            .iter()
+            .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
+            .collect();
+
+        matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
+        matches
+            .into_iter()
+            .map(|(name, _)| ((0..), name.into()))
+            .collect()
+    }
+
     pub fn filename(input: &str) -> Vec<Completion> {
         filename_impl(input, |entry| {
             let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
@@ -255,7 +283,7 @@ pub mod completers {
         let is_tilde = input.starts_with('~') && input.len() == 1;
         let path = helix_core::path::expand_tilde(Path::new(input));
 
-        let (dir, file_name) = if input.ends_with('/') {
+        let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
             (path, None)
         } else {
             let file_name = path
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index eaca470e9..2c7db7f2b 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,
@@ -139,8 +139,8 @@ impl<T> FilePicker<T> {
                     (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
                     _ => {
                         // TODO: enable syntax highlighting; blocked by async rendering
-                        Document::open(path, None, Some(&editor.theme), None)
-                            .map(CachedPreview::Document)
+                        Document::open(path, None, None)
+                            .map(|doc| CachedPreview::Document(Box::new(doc)))
                             .unwrap_or(CachedPreview::NotFound)
                     }
                 },
@@ -159,6 +159,7 @@ impl<T: 'static> Component for FilePicker<T> {
         // |picker   | |         |
         // |         | |         |
         // +---------+ +---------+
+
         let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
         let area = inner_rect(area);
         // -- Render the frame:
@@ -220,13 +221,8 @@ impl<T: 'static> Component for FilePicker<T> {
 
             let offset = Position::new(first_line, 0);
 
-            let highlights = EditorView::doc_syntax_highlights(
-                doc,
-                offset,
-                area.height,
-                &cx.editor.theme,
-                &cx.editor.syn_loader,
-            );
+            let highlights =
+                EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme);
             EditorView::render_text_highlights(
                 doc,
                 offset,
@@ -397,6 +393,16 @@ fn inner_rect(area: Rect) -> Rect {
 }
 
 impl<T: 'static> Component for Picker<T> {
+    fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+        let max_width = 50.min(viewport.0);
+        let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
+        let height = (self.options.len() as u16 + 4) // add some spacing for input + padding
+            .min(max_height);
+        let width = max_width;
+        Some((width, height))
+    }
+
     fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
         let key_event = match event {
             Event::Key(event) => event,
@@ -404,13 +410,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') => {
@@ -492,10 +498,9 @@ impl<T: 'static> Component for Picker<T> {
         let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
         let borders = BorderType::line_symbols(BorderType::Plain);
         for x in inner.left()..inner.right() {
-            surface
-                .get_mut(x, inner.y + 1)
-                .set_symbol(borders.horizontal)
-                .set_style(sep_style);
+            if let Some(cell) = surface.get_mut(x, inner.y + 1) {
+                cell.set_symbol(borders.horizontal).set_style(sep_style);
+            }
         }
 
         // -- Render the contents:
@@ -505,7 +510,7 @@ impl<T: 'static> Component for Picker<T> {
         let selected = cx.editor.theme.get("ui.text.focus");
 
         let rows = inner.height;
-        let offset = self.cursor / (rows as usize) * (rows as usize);
+        let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
 
         let files = self.matches.iter().skip(offset).map(|(index, _score)| {
             (index, self.options.get(*index).unwrap()) // get_unchecked
@@ -513,7 +518,7 @@ impl<T: 'static> Component for Picker<T> {
 
         for (i, (_index, option)) in files.take(rows as usize).enumerate() {
             if i == (self.cursor - offset) {
-                surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected);
+                surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
             }
 
             surface.set_string_truncated(
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 8f7921a11..4d319423a 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -6,7 +6,7 @@ use crossterm::event::Event;
 use tui::buffer::Buffer as Surface;
 
 use helix_core::Position;
-use helix_view::graphics::Rect;
+use helix_view::graphics::{Margin, Rect};
 
 // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
 // a width/height hint. maybe Popup(Box<Component>)
@@ -14,17 +14,26 @@ use helix_view::graphics::Rect;
 pub struct Popup<T: Component> {
     contents: T,
     position: Option<Position>,
+    margin: Margin,
     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,
+            margin: Margin {
+                vertical: 0,
+                horizontal: 0,
+            },
             size: (0, 0),
+            child_size: (0, 0),
             scroll: 0,
+            id,
         }
     }
 
@@ -32,6 +41,11 @@ impl<T: Component> Popup<T> {
         self.position = pos;
     }
 
+    pub fn margin(mut self, margin: Margin) -> Self {
+        self.margin = margin;
+        self
+    }
+
     pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
         let position = self
             .position
@@ -68,6 +82,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 +110,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 +132,26 @@ 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 inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin);
+
         let (width, height) = self
             .contents
-            .required_size((120, 26)) // max width, max height
+            .required_size((inner.width, inner.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 + self.margin.horizontal * 2).min(max_width),
+            (height + self.margin.vertical * 2).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)
     }
@@ -141,6 +171,11 @@ impl<T: Component> Component for Popup<T> {
         let background = cx.editor.theme.get("ui.popup");
         surface.clear_with(area, background);
 
-        self.contents.render(area, surface, cx);
+        let inner = area.inner(&self.margin);
+        self.contents.render(inner, 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 e90b07727..4c4fef268 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -127,7 +127,7 @@ impl Prompt {
                 let mut char_position = char_indices
                     .iter()
                     .position(|(idx, _)| *idx == self.cursor)
-                    .unwrap_or_else(|| char_indices.len());
+                    .unwrap_or(char_indices.len());
 
                 for _ in 0..rep {
                     // Skip any non-whitespace characters
@@ -330,7 +330,7 @@ impl Prompt {
             .max(BASE_WIDTH);
 
         let cols = std::cmp::max(1, area.width / max_len);
-        let col_width = (area.width - (cols)) / cols;
+        let col_width = (area.width.saturating_sub(cols)) / cols;
 
         let height = ((self.completion.len() as u16 + cols - 1) / cols)
             .min(10) // at most 10 rows (or less)
@@ -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();
         })));
@@ -473,7 +473,7 @@ impl Component for Prompt {
                 }
             }
             key!(Enter) => {
-                if self.selection.is_some() && self.line.ends_with('/') {
+                if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) {
                     self.completion = (self.completion_fn)(&self.line);
                     self.exit_selection();
                 } else {
@@ -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-term/src/ui/spinner.rs b/helix-term/src/ui/spinner.rs
index e8a43b48d..68965469d 100644
--- a/helix-term/src/ui/spinner.rs
+++ b/helix-term/src/ui/spinner.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, time::SystemTime};
+use std::{collections::HashMap, time::Instant};
 
 #[derive(Default, Debug)]
 pub struct ProgressSpinners {
@@ -25,7 +25,7 @@ impl Default for Spinner {
 pub struct Spinner {
     frames: Vec<&'static str>,
     count: usize,
-    start: Option<SystemTime>,
+    start: Option<Instant>,
     interval: u64,
 }
 
@@ -50,14 +50,13 @@ impl Spinner {
     }
 
     pub fn start(&mut self) {
-        self.start = Some(SystemTime::now());
+        self.start = Some(Instant::now());
     }
 
     pub fn frame(&self) -> Option<&str> {
         let idx = (self
             .start
-            .map(|time| SystemTime::now().duration_since(time))?
-            .ok()?
+            .map(|time| Instant::now().duration_since(time))?
             .as_millis()
             / self.interval as u128) as usize
             % self.count;
diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml
index 6df65d360..e4cfbe4cd 100644
--- a/helix-tui/Cargo.toml
+++ b/helix-tui/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "helix-tui"
-version = "0.5.0"
+version = "0.6.0"
 authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
 description = """
 A library to build rich terminal user interfaces or dashboards
@@ -18,8 +18,8 @@ default = ["crossterm"]
 [dependencies]
 bitflags = "1.3"
 cassowary = "0.3"
-unicode-segmentation = "1.8"
-crossterm = { version = "0.22", optional = true }
+unicode-segmentation = "1.9"
+crossterm = { version = "0.23", optional = true }
 serde = { version = "1", "optional" = true, features = ["derive"]}
-helix-view = { version = "0.5", path = "../helix-view", features = ["term"] }
-helix-core = { version = "0.5", path = "../helix-core" }
+helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
+helix-core = { version = "0.6", path = "../helix-core" }
diff --git a/helix-tui/README.md b/helix-tui/README.md
index 97b3d1d9e..5cc80aa65 100644
--- a/helix-tui/README.md
+++ b/helix-tui/README.md
@@ -2,5 +2,5 @@
 
 This library is a fork of the great library
 [tui-rs](https://github.com/fdehau/tui-rs/). We've mainly relied on the double
-buffer implementation and render diffing, side-stepping it's widget and
+buffer implementation and render diffing, side-stepping its widget and
 layouting.
diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs
index 3f56b49c8..52474148e 100644
--- a/helix-tui/src/backend/test.rs
+++ b/helix-tui/src/backend/test.rs
@@ -111,8 +111,7 @@ impl Backend for TestBackend {
         I: Iterator<Item = (u16, u16, &'a Cell)>,
     {
         for (x, y, c) in content {
-            let cell = self.buffer.get_mut(x, y);
-            *cell = c.clone();
+            self.buffer[(x, y)] = c.clone();
         }
         Ok(())
     }
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index f480bc2f3..f8673e436 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -90,19 +90,19 @@ impl Default for Cell {
 /// use helix_view::graphics::{Rect, Color, Style, Modifier};
 ///
 /// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
-/// buf.get_mut(0, 2).set_symbol("x");
-/// assert_eq!(buf.get(0, 2).symbol, "x");
+/// buf[(0, 2)].set_symbol("x");
+/// assert_eq!(buf[(0, 2)].symbol, "x");
 /// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
-/// assert_eq!(buf.get(5, 0), &Cell{
+/// assert_eq!(buf[(5, 0)], Cell{
 ///     symbol: String::from("r"),
 ///     fg: Color::Red,
 ///     bg: Color::White,
 ///     modifier: Modifier::empty()
 /// });
-/// buf.get_mut(5, 0).set_char('x');
-/// assert_eq!(buf.get(5, 0).symbol, "x");
+/// buf[(5, 0)].set_char('x');
+/// assert_eq!(buf[(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 {
@@ -171,15 +162,38 @@ impl Buffer {
     }
 
     /// Returns a reference to Cell at the given coordinates
-    pub fn get(&self, x: u16, y: u16) -> &Cell {
-        let i = self.index_of(x, y);
-        &self.content[i]
+    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
+        self.index_of_opt(x, y).map(|i| &self.content[i])
     }
 
     /// Returns a mutable reference to Cell at the given coordinates
-    pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
-        let i = self.index_of(x, y);
-        &mut self.content[i]
+    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
+        self.index_of_opt(x, y).map(|i| &mut self.content[i])
+    }
+
+    /// Tells whether the global (x, y) coordinates are inside the Buffer's area.
+    ///
+    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use helix_tui::buffer::Buffer;
+    /// # use helix_view::graphics::Rect;
+    /// let rect = Rect::new(200, 100, 10, 10);
+    /// let buffer = Buffer::empty(rect);
+    /// // Global coordinates inside the Buffer's area
+    /// assert!(buffer.in_bounds(209, 100));
+    /// // Global coordinates outside the Buffer's area
+    /// assert!(!buffer.in_bounds(210, 100));
+    /// ```
+    ///
+    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
+    pub fn in_bounds(&self, x: u16, y: u16) -> bool {
+        x >= self.area.left()
+            && x < self.area.right()
+            && y >= self.area.top()
+            && y < self.area.bottom()
     }
 
     /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
@@ -193,7 +207,7 @@ impl Buffer {
     /// # use helix_view::graphics::Rect;
     /// let rect = Rect::new(200, 100, 10, 10);
     /// let buffer = Buffer::empty(rect);
-    /// // Global coordinates to the top corner of this buffer's area
+    /// // Global coordinates to the top corner of this Buffer's area
     /// assert_eq!(buffer.index_of(200, 100), 0);
     /// ```
     ///
@@ -202,10 +216,7 @@ impl Buffer {
     /// Panics when given an coordinate that is outside of this Buffer's area.
     pub fn index_of(&self, x: u16, y: u16) -> usize {
         debug_assert!(
-            x >= self.area.left()
-                && x < self.area.right()
-                && y >= self.area.top()
-                && y < self.area.bottom(),
+            self.in_bounds(x, y),
             "Trying to access position outside the buffer: x={}, y={}, area={:?}",
             x,
             y,
@@ -214,6 +225,16 @@ impl Buffer {
         ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
     }
 
+    /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates,
+    /// or `None` if the coordinates are outside the buffer's area.
+    fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> {
+        if self.in_bounds(x, y) {
+            Some(self.index_of(x, y))
+        } else {
+            None
+        }
+    }
+
     /// Returns the (global) coordinates of a cell given its index
     ///
     /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
@@ -287,6 +308,11 @@ impl Buffer {
     where
         S: AsRef<str>,
     {
+        // prevent panic if out of range
+        if !self.in_bounds(x, y) || width == 0 {
+            return (x, y);
+        }
+
         let mut index = self.index_of(x, y);
         let mut x_offset = x as usize;
         let width = if ellipsis { width - 1 } else { width };
@@ -381,7 +407,7 @@ impl Buffer {
     pub fn set_background(&mut self, area: Rect, color: Color) {
         for y in area.top()..area.bottom() {
             for x in area.left()..area.right() {
-                self.get_mut(x, y).set_bg(color);
+                self[(x, y)].set_bg(color);
             }
         }
     }
@@ -389,7 +415,7 @@ impl Buffer {
     pub fn set_style(&mut self, area: Rect, style: Style) {
         for y in area.top()..area.bottom() {
             for x in area.left()..area.right() {
-                self.get_mut(x, y).set_style(style);
+                self[(x, y)].set_style(style);
             }
         }
     }
@@ -417,7 +443,7 @@ impl Buffer {
     pub fn clear(&mut self, area: Rect) {
         for x in area.left()..area.right() {
             for y in area.top()..area.bottom() {
-                self.get_mut(x, y).reset();
+                self[(x, y)].reset();
             }
         }
     }
@@ -426,7 +452,7 @@ impl Buffer {
     pub fn clear_with(&mut self, area: Rect, style: Style) {
         for x in area.left()..area.right() {
             for y in area.top()..area.bottom() {
-                let cell = self.get_mut(x, y);
+                let cell = &mut self[(x, y)];
                 cell.reset();
                 cell.set_style(style);
             }
@@ -509,15 +535,32 @@ impl Buffer {
                 updates.push((x, y, &next_buffer[i]));
             }
 
-            to_skip = current.symbol.width().saturating_sub(1);
+            let current_width = current.symbol.width();
+            to_skip = current_width.saturating_sub(1);
 
-            let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
+            let affected_width = std::cmp::max(current_width, previous.symbol.width());
             invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
         }
         updates
     }
 }
 
+impl std::ops::Index<(u16, u16)> for Buffer {
+    type Output = Cell;
+
+    fn index(&self, (x, y): (u16, u16)) -> &Self::Output {
+        let i = self.index_of(x, y);
+        &self.content[i]
+    }
+}
+
+impl std::ops::IndexMut<(u16, u16)> for Buffer {
+    fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output {
+        let i = self.index_of(x, y);
+        &mut self.content[i]
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs
index b8e52479f..8a974ddba 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/block.rs b/helix-tui/src/widgets/block.rs
index 648c2d7ee..26223c3eb 100644
--- a/helix-tui/src/widgets/block.rs
+++ b/helix-tui/src/widgets/block.rs
@@ -15,12 +15,12 @@ pub enum BorderType {
 }
 
 impl BorderType {
-    pub fn line_symbols(border_type: BorderType) -> line::Set {
+    pub fn line_symbols(border_type: Self) -> line::Set {
         match border_type {
-            BorderType::Plain => line::NORMAL,
-            BorderType::Rounded => line::ROUNDED,
-            BorderType::Double => line::DOUBLE,
-            BorderType::Thick => line::THICK,
+            Self::Plain => line::NORMAL,
+            Self::Rounded => line::ROUNDED,
+            Self::Double => line::DOUBLE,
+            Self::Thick => line::THICK,
         }
     }
 }
@@ -140,14 +140,14 @@ impl<'a> Widget for Block<'a> {
         // Sides
         if self.borders.intersects(Borders::LEFT) {
             for y in area.top()..area.bottom() {
-                buf.get_mut(area.left(), y)
+                buf[(area.left(), y)]
                     .set_symbol(symbols.vertical)
                     .set_style(self.border_style);
             }
         }
         if self.borders.intersects(Borders::TOP) {
             for x in area.left()..area.right() {
-                buf.get_mut(x, area.top())
+                buf[(x, area.top())]
                     .set_symbol(symbols.horizontal)
                     .set_style(self.border_style);
             }
@@ -155,7 +155,7 @@ impl<'a> Widget for Block<'a> {
         if self.borders.intersects(Borders::RIGHT) {
             let x = area.right() - 1;
             for y in area.top()..area.bottom() {
-                buf.get_mut(x, y)
+                buf[(x, y)]
                     .set_symbol(symbols.vertical)
                     .set_style(self.border_style);
             }
@@ -163,7 +163,7 @@ impl<'a> Widget for Block<'a> {
         if self.borders.intersects(Borders::BOTTOM) {
             let y = area.bottom() - 1;
             for x in area.left()..area.right() {
-                buf.get_mut(x, y)
+                buf[(x, y)]
                     .set_symbol(symbols.horizontal)
                     .set_style(self.border_style);
             }
@@ -171,22 +171,22 @@ impl<'a> Widget for Block<'a> {
 
         // Corners
         if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
-            buf.get_mut(area.right() - 1, area.bottom() - 1)
+            buf[(area.right() - 1, area.bottom() - 1)]
                 .set_symbol(symbols.bottom_right)
                 .set_style(self.border_style);
         }
         if self.borders.contains(Borders::RIGHT | Borders::TOP) {
-            buf.get_mut(area.right() - 1, area.top())
+            buf[(area.right() - 1, area.top())]
                 .set_symbol(symbols.top_right)
                 .set_style(self.border_style);
         }
         if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
-            buf.get_mut(area.left(), area.bottom() - 1)
+            buf[(area.left(), area.bottom() - 1)]
                 .set_symbol(symbols.bottom_left)
                 .set_style(self.border_style);
         }
         if self.borders.contains(Borders::LEFT | Borders::TOP) {
-            buf.get_mut(area.left(), area.top())
+            buf[(area.left(), area.top())]
                 .set_symbol(symbols.top_left)
                 .set_style(self.border_style);
         }
diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs
index fee35d250..4e8391621 100644
--- a/helix-tui/src/widgets/paragraph.rs
+++ b/helix-tui/src/widgets/paragraph.rs
@@ -166,7 +166,7 @@ impl<'a> Widget for Paragraph<'a> {
             Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
         } else {
             let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
-            if let Alignment::Left = self.alignment {
+            if self.alignment == Alignment::Left {
                 line_composer.set_horizontal_offset(self.scroll.1);
             }
             line_composer
@@ -176,7 +176,7 @@ impl<'a> Widget for Paragraph<'a> {
             if y >= self.scroll.0 {
                 let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
                 for StyledGrapheme { symbol, style } in current_line {
-                    buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
+                    buf[(text_area.left() + x, text_area.top() + y - self.scroll.0)]
                         .set_symbol(if symbol.is_empty() {
                             // If the symbol is empty, the last char which rendered last time will
                             // leave on the line. It's a quick fix.
diff --git a/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs
index 21847783b..33e52bb4e 100644
--- a/helix-tui/src/widgets/reflow.rs
+++ b/helix-tui/src/widgets/reflow.rs
@@ -404,8 +404,8 @@ mod test {
         let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
                     では、";
         let (word_wrapper, word_wrapper_width) =
-            run_composer(Composer::WordWrapper { trim: true }, &text, width);
-        let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
+            run_composer(Composer::WordWrapper { trim: true }, text, width);
+        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
         assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
         let wrapped = vec![
             "コンピュータ上で文字",
diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs
index d7caa0b0a..6aee5988c 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/Cargo.toml b/helix-view/Cargo.toml
index ffe6a111c..932c33216 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "helix-view"
-version = "0.5.0"
+version = "0.6.0"
 authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
 edition = "2021"
 license = "MPL-2.0"
@@ -16,13 +16,13 @@ term = ["crossterm"]
 [dependencies]
 bitflags = "1.3"
 anyhow = "1"
-helix-core = { version = "0.5", path = "../helix-core" }
-helix-lsp = { version = "0.5", path = "../helix-lsp"}
-helix-dap = { version = "0.5", path = "../helix-dap"}
-crossterm = { version = "0.22", optional = true }
+helix-core = { version = "0.6", path = "../helix-core" }
+helix-lsp = { version = "0.6", path = "../helix-lsp"}
+helix-dap = { version = "0.6", path = "../helix-dap"}
+crossterm = { version = "0.23", optional = true }
 
 # Conversion traits
-once_cell = "1.8"
+once_cell = "1.9"
 url = "2"
 
 tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
@@ -31,7 +31,6 @@ futures-util = { version = "0.3", features = ["std", "async-await"], default-fea
 
 slotmap = "1"
 
-encoding_rs = "0.8"
 chardetng = "0.1"
 
 serde = { version = "1.0", features = ["derive"] }
@@ -41,7 +40,7 @@ log = "~0.4"
 which = "4.2"
 
 [target.'cfg(windows)'.dependencies]
-clipboard-win = { version = "4.2", features = ["std"] }
+clipboard-win = { version = "4.4", features = ["std"] }
 
 [dev-dependencies]
 helix-tui = { path = "../helix-tui" }
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 76b19a07d..c0186ee53 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,5 +1,6 @@
-use anyhow::{anyhow, Context, Error};
+use anyhow::{anyhow, bail, Context, Error};
 use serde::de::{self, Deserialize, Deserializer};
+use serde::Serialize;
 use std::cell::Cell;
 use std::collections::HashMap;
 use std::fmt::Display;
@@ -9,7 +10,8 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use helix_core::{
-    history::History,
+    encoding,
+    history::{History, UndoKind},
     indent::{auto_detect_indent_style, IndentStyle},
     line_ending::auto_detect_line_ending,
     syntax::{self, LanguageConfiguration},
@@ -18,7 +20,7 @@ use helix_core::{
 };
 use helix_lsp::util::LspFormatting;
 
-use crate::{DocumentId, Theme, ViewId};
+use crate::{DocumentId, ViewId};
 
 /// 8kB of buffer space for encoding and decoding `Rope`s.
 const BUF_SIZE: usize = 8192;
@@ -29,9 +31,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 pub enum Mode {
-    Normal,
-    Select,
-    Insert,
+    Normal = 0,
+    Select = 1,
+    Insert = 2,
 }
 
 impl Display for Mode {
@@ -52,7 +54,7 @@ impl FromStr for Mode {
             "normal" => Ok(Mode::Normal),
             "select" => Ok(Mode::Select),
             "insert" => Ok(Mode::Insert),
-            _ => Err(anyhow!("Invalid mode '{}'", s)),
+            _ => bail!("Invalid mode '{}'", s),
         }
     }
 }
@@ -68,13 +70,22 @@ impl<'de> Deserialize<'de> for Mode {
     }
 }
 
+impl Serialize for Mode {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.collect_str(self)
+    }
+}
+
 pub struct Document {
     pub(crate) id: DocumentId,
     text: Rope,
     pub(crate) selections: HashMap<ViewId, Selection>,
 
     path: Option<PathBuf>,
-    encoding: &'static encoding_rs::Encoding,
+    encoding: &'static encoding::Encoding,
 
     /// Current editing mode.
     pub mode: Mode,
@@ -104,6 +115,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 +139,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()
@@ -141,8 +154,8 @@ impl fmt::Debug for Document {
 /// be used to override encoding auto-detection.
 pub fn from_reader<R: std::io::Read + ?Sized>(
     reader: &mut R,
-    encoding: Option<&'static encoding_rs::Encoding>,
-) -> Result<(Rope, &'static encoding_rs::Encoding), Error> {
+    encoding: Option<&'static encoding::Encoding>,
+) -> Result<(Rope, &'static encoding::Encoding), Error> {
     // These two buffers are 8192 bytes in size each and are used as
     // intermediaries during the decoding process. Text read into `buf`
     // from `reader` is decoded into `buf_out` as UTF-8. Once either
@@ -210,11 +223,11 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
             total_read += read;
             total_written += written;
             match result {
-                encoding_rs::CoderResult::InputEmpty => {
+                encoding::CoderResult::InputEmpty => {
                     debug_assert_eq!(slice.len(), total_read);
                     break;
                 }
-                encoding_rs::CoderResult::OutputFull => {
+                encoding::CoderResult::OutputFull => {
                     debug_assert!(slice.len() > total_read);
                     builder.append(&buf_str[..total_written]);
                     total_written = 0;
@@ -249,7 +262,7 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
 /// replacement characters may appear in the encoded text.
 pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
     writer: &'a mut W,
-    encoding: &'static encoding_rs::Encoding,
+    encoding: &'static encoding::Encoding,
     rope: &'a Rope,
 ) -> Result<(), Error> {
     // Text inside a `Rope` is stored as non-contiguous blocks of data called
@@ -284,12 +297,12 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
             total_read += read;
             total_written += written;
             match result {
-                encoding_rs::CoderResult::InputEmpty => {
+                encoding::CoderResult::InputEmpty => {
                     debug_assert_eq!(chunk.len(), total_read);
                     debug_assert!(buf.len() >= total_written);
                     break;
                 }
-                encoding_rs::CoderResult::OutputFull => {
+                encoding::CoderResult::OutputFull => {
                     debug_assert!(chunk.len() > total_read);
                     writer.write_all(&buf[..total_written]).await?;
                     total_written = 0;
@@ -320,8 +333,8 @@ use helix_lsp::lsp;
 use url::Url;
 
 impl Document {
-    pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self {
-        let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+    pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self {
+        let encoding = encoding.unwrap_or(encoding::UTF_8);
         let changes = ChangeSet::new(&text);
         let old_state = None;
 
@@ -344,6 +357,7 @@ impl Document {
             history: Cell::new(History::default()),
             savepoint: None,
             last_saved_revision: 0,
+            modified_since_accessed: false,
             language_server: None,
         }
     }
@@ -353,9 +367,8 @@ impl Document {
     /// overwritten with the `encoding` parameter.
     pub fn open(
         path: &Path,
-        encoding: Option<&'static encoding_rs::Encoding>,
-        theme: Option<&Theme>,
-        config_loader: Option<&syntax::Loader>,
+        encoding: Option<&'static encoding::Encoding>,
+        config_loader: Option<Arc<syntax::Loader>>,
     ) -> Result<Self, Error> {
         // Open the file if it exists, otherwise assume it is a new file (and thus empty).
         let (rope, encoding) = if path.exists() {
@@ -363,7 +376,7 @@ impl Document {
                 std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
             from_reader(&mut file, encoding)?
         } else {
-            let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+            let encoding = encoding.unwrap_or(encoding::UTF_8);
             (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
         };
 
@@ -372,7 +385,7 @@ impl Document {
         // set the path and try detecting the language
         doc.set_path(Some(path))?;
         if let Some(loader) = config_loader {
-            doc.detect_language(theme, loader);
+            doc.detect_language(loader);
         }
 
         doc.detect_indent_and_line_ending();
@@ -383,7 +396,7 @@ impl Document {
     /// The same as [`format`], but only returns formatting changes if auto-formatting
     /// is configured.
     pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
-        if self.language_config().map(|c| c.auto_format) == Some(true) {
+        if self.language_config()?.auto_format {
             self.format()
         } else {
             None
@@ -393,30 +406,27 @@ impl Document {
     /// If supported, returns the changes that should be applied to this document in order
     /// to format it nicely.
     pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
-        if let Some(language_server) = self.language_server() {
-            let text = self.text.clone();
-            let offset_encoding = language_server.offset_encoding();
-            let request = language_server.text_document_formatting(
-                self.identifier(),
-                lsp::FormattingOptions::default(),
-                None,
-            )?;
+        let language_server = self.language_server()?;
+        let text = self.text.clone();
+        let offset_encoding = language_server.offset_encoding();
+        let request = language_server.text_document_formatting(
+            self.identifier(),
+            lsp::FormattingOptions::default(),
+            None,
+        )?;
 
-            let fut = async move {
-                let edits = request.await.unwrap_or_else(|e| {
-                    log::warn!("LSP formatting failed: {}", e);
-                    Default::default()
-                });
-                LspFormatting {
-                    doc: text,
-                    edits,
-                    offset_encoding,
-                }
-            };
-            Some(fut)
-        } else {
-            None
-        }
+        let fut = async move {
+            let edits = request.await.unwrap_or_else(|e| {
+                log::warn!("LSP formatting failed: {}", e);
+                Default::default()
+            });
+            LspFormatting {
+                doc: text,
+                edits,
+                offset_encoding,
+            }
+        };
+        Some(fut)
     }
 
     pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -460,9 +470,7 @@ impl Document {
             if let Some(parent) = path.parent() {
                 // TODO: display a prompt asking the user if the directories should be created
                 if !parent.exists() {
-                    return Err(Error::msg(
-                        "can't save file, parent directory does not exist",
-                    ));
+                    bail!("can't save file, parent directory does not exist");
                 }
             }
 
@@ -494,12 +502,12 @@ impl Document {
     }
 
     /// Detect the programming language based on the file type.
-    pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
+    pub fn detect_language(&mut self, config_loader: Arc<syntax::Loader>) {
         if let Some(path) = &self.path {
             let language_config = config_loader
                 .language_config_for_file_name(path)
                 .or_else(|| config_loader.language_config_for_shebang(self.text()));
-            self.set_language(theme, language_config);
+            self.set_language(language_config, Some(config_loader));
         }
     }
 
@@ -509,8 +517,7 @@ impl Document {
     /// line ending.
     pub fn detect_indent_and_line_ending(&mut self) {
         self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
-            self.language
-                .as_ref()
+            self.language_config()
                 .and_then(|config| config.indent.as_ref())
                 .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
         });
@@ -524,7 +531,7 @@ impl Document {
 
         // If there is no path or the path no longer exists.
         if path.is_none() {
-            return Err(anyhow!("can't find file to reload from"));
+            bail!("can't find file to reload from");
         }
 
         let mut file = std::fs::File::open(path.unwrap())?;
@@ -545,15 +552,13 @@ impl Document {
 
     /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
     pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
-        match encoding_rs::Encoding::for_label(label.as_bytes()) {
-            Some(encoding) => self.encoding = encoding,
-            None => return Err(anyhow::anyhow!("unknown encoding")),
-        }
+        self.encoding = encoding::Encoding::for_label(label.as_bytes())
+            .ok_or_else(|| anyhow!("unknown encoding"))?;
         Ok(())
     }
 
     /// Returns the [`Document`]'s current encoding.
-    pub fn encoding(&self) -> &'static encoding_rs::Encoding {
+    pub fn encoding(&self) -> &'static encoding::Encoding {
         self.encoding
     }
 
@@ -573,15 +578,13 @@ impl Document {
     /// if it exists.
     pub fn set_language(
         &mut self,
-        theme: Option<&Theme>,
         language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
+        loader: Option<Arc<helix_core::syntax::Loader>>,
     ) {
-        if let Some(language_config) = language_config {
-            let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
-            if let Some(highlight_config) = language_config.highlight_config(scopes) {
-                let syntax = Syntax::new(&self.text, highlight_config);
+        if let (Some(language_config), Some(loader)) = (language_config, loader) {
+            if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) {
+                let syntax = Syntax::new(&self.text, highlight_config, loader);
                 self.syntax = Some(syntax);
-                // TODO: config.configure(scopes) is now delayed, is that ok?
             }
 
             self.language = Some(language_config);
@@ -593,15 +596,10 @@ impl Document {
 
     /// Set the programming language for the file if you know the name (scope) but don't have the
     /// [`syntax::LanguageConfiguration`] for it.
-    pub fn set_language2(
-        &mut self,
-        scope: &str,
-        theme: Option<&Theme>,
-        config_loader: Arc<syntax::Loader>,
-    ) {
+    pub fn set_language2(&mut self, scope: &str, config_loader: Arc<syntax::Loader>) {
         let language_config = config_loader.language_config_for_scope(scope);
 
-        self.set_language(theme, language_config);
+        self.set_language(language_config, Some(config_loader));
     }
 
     /// Set the LSP.
@@ -639,6 +637,8 @@ impl Document {
                     selection.clone().ensure_invariants(self.text.slice(..)),
                 );
             }
+
+            self.modified_since_accessed = true;
         }
 
         if !transaction.changes().is_empty() {
@@ -680,7 +680,7 @@ impl Document {
 
                 if let Some(notify) = notify {
                     tokio::spawn(notify);
-                } //.expect("failed to emit textDocument/didChange");
+                }
             }
         }
         success
@@ -708,11 +708,11 @@ impl Document {
         success
     }
 
-    /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
-    pub fn undo(&mut self, view_id: ViewId) -> bool {
+    fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool {
         let mut history = self.history.take();
-        let success = if let Some(transaction) = history.undo() {
-            self.apply_impl(transaction, view_id)
+        let txn = if undo { history.undo() } else { history.redo() };
+        let success = if let Some(txn) = txn {
+            self.apply_impl(txn, view_id)
         } else {
             false
         };
@@ -725,21 +725,14 @@ impl Document {
         success
     }
 
+    /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
+    pub fn undo(&mut self, view_id: ViewId) -> bool {
+        self.undo_redo_impl(view_id, true)
+    }
+
     /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
     pub fn redo(&mut self, view_id: ViewId) -> bool {
-        let mut history = self.history.take();
-        let success = if let Some(transaction) = history.redo() {
-            self.apply_impl(transaction, view_id)
-        } else {
-            false
-        };
-        self.history.set(history);
-
-        if success {
-            // reset changeset to fix len
-            self.changes = ChangeSet::new(self.text());
-        }
-        success
+        self.undo_redo_impl(view_id, false)
     }
 
     pub fn savepoint(&mut self) {
@@ -752,9 +745,12 @@ impl Document {
         }
     }
 
-    /// Undo modifications to the [`Document`] according to `uk`.
-    pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
-        let txns = self.history.get_mut().earlier(uk);
+    fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool {
+        let txns = if earlier {
+            self.history.get_mut().earlier(uk)
+        } else {
+            self.history.get_mut().later(uk)
+        };
         let mut success = false;
         for txn in txns {
             if self.apply_impl(&txn, view_id) {
@@ -768,20 +764,14 @@ impl Document {
         success
     }
 
+    /// Undo modifications to the [`Document`] according to `uk`.
+    pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+        self.earlier_later_impl(view_id, uk, true)
+    }
+
     /// Redo modifications to the [`Document`] according to `uk`.
-    pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
-        let txns = self.history.get_mut().later(uk);
-        let mut success = false;
-        for txn in txns {
-            if self.apply_impl(&txn, view_id) {
-                success = true;
-            }
-        }
-        if success {
-            // reset changeset to fix len
-            self.changes = ChangeSet::new(self.text());
-        }
-        success
+    pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+        self.earlier_later_impl(view_id, uk, false)
     }
 
     /// Commit pending changes to history
@@ -837,6 +827,16 @@ impl Document {
             .map(|language| language.scope.as_str())
     }
 
+    /// Language ID for the document. Either the `language-id` from the
+    /// `language-server` configuration, or the document language if no
+    /// `language-id` has been specified.
+    pub fn language_id(&self) -> Option<&str> {
+        self.language_config()
+            .and_then(|config| config.language_server.as_ref())
+            .and_then(|lsp_config| lsp_config.language_id.as_deref())
+            .or_else(|| Some(self.language()?.rsplit_once('.')?.1))
+    }
+
     /// Corresponding [`LanguageConfiguration`].
     pub fn language_config(&self) -> Option<&LanguageConfiguration> {
         self.language.as_deref()
@@ -847,18 +847,10 @@ impl Document {
         self.version
     }
 
+    /// Language server if it has been initialized.
     pub fn language_server(&self) -> Option<&helix_lsp::Client> {
-        let server = self.language_server.as_deref();
-        let initialized = server
-            .map(|server| server.is_initialized())
-            .unwrap_or(false);
-
-        // only resolve language_server if it's initialized
-        if initialized {
-            server
-        } else {
-            None
-        }
+        let server = self.language_server.as_deref()?;
+        server.is_initialized().then(|| server)
     }
 
     #[inline]
@@ -869,8 +861,7 @@ impl Document {
 
     /// Tab size in columns.
     pub fn tab_width(&self) -> usize {
-        self.language
-            .as_ref()
+        self.language_config()
             .and_then(|config| config.indent.as_ref())
             .map_or(4, |config| config.tab_width) // fallback to 4 columns
     }
@@ -883,6 +874,10 @@ impl Document {
         self.indent_style.as_str()
     }
 
+    pub fn changes(&self) -> &ChangeSet {
+        &self.changes
+    }
+
     #[inline]
     /// File path on disk.
     pub fn path(&self) -> Option<&PathBuf> {
@@ -891,7 +886,7 @@ impl Document {
 
     /// File path as a URL.
     pub fn url(&self) -> Option<Url> {
-        self.path().map(|path| Url::from_file_path(path).unwrap())
+        Url::from_file_path(self.path()?).ok()
     }
 
     #[inline]
@@ -914,10 +909,6 @@ impl Document {
             .map(helix_core::path::get_relative_path)
     }
 
-    // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
-    //     self.state.doc.slice
-    // }
-
     // transact(Fn) ?
 
     // -- LSP methods
@@ -938,7 +929,6 @@ impl Document {
 
     pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
         self.diagnostics = diagnostics;
-        // sort by range
         self.diagnostics
             .sort_unstable_by_key(|diagnostic| diagnostic.range);
     }
@@ -1113,7 +1103,7 @@ mod test {
 
     macro_rules! test_decode {
         ($label:expr, $label_override:expr) => {
-            let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+            let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
             let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
             let path = base_path.join(format!("{}_in.txt", $label));
             let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
@@ -1132,7 +1122,7 @@ mod test {
 
     macro_rules! test_encode {
         ($label:expr, $label_override:expr) => {
-            let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+            let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
             let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
             let path = base_path.join(format!("{}_out.txt", $label));
             let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index c7b3baefb..2e6121bc4 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -1,7 +1,9 @@
 use crate::{
     clipboard::{get_clipboard_provider, ClipboardProvider},
-    document::SCRATCH_BUFFER_NAME,
+    document::{Mode, SCRATCH_BUFFER_NAME},
     graphics::{CursorKind, Rect},
+    info::Info,
+    input::KeyEvent,
     theme::{self, Theme},
     tree::{self, Tree},
     Document, DocumentId, View, ViewId,
@@ -22,7 +24,7 @@ use std::{
 
 use tokio::time::{sleep, Duration, Instant, Sleep};
 
-use anyhow::{bail, Context, Error};
+use anyhow::{bail, Error};
 
 pub use helix_core::diagnostic::Severity;
 pub use helix_core::register::Registers;
@@ -30,7 +32,7 @@ use helix_core::syntax;
 use helix_core::{Position, Selection};
 use helix_dap as dap;
 
-use serde::Deserialize;
+use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
 
 fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
 where
@@ -40,7 +42,7 @@ where
     Ok(Duration::from_millis(millis))
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
 pub struct FilePickerConfig {
     /// IgnoreOptions
@@ -80,7 +82,7 @@ impl Default for FilePickerConfig {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
 pub struct Config {
     /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
@@ -95,8 +97,6 @@ pub struct Config {
     pub line_number: LineNumber,
     /// Middle click paste support. Defaults to true.
     pub middle_click_paste: bool,
-    /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
-    pub smart_case: bool,
     /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
     pub auto_pairs: bool,
     /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
@@ -108,18 +108,101 @@ pub struct Config {
     /// Whether to display infoboxes. Defaults to true.
     pub auto_info: bool,
     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,
+    /// Search configuration.
+    #[serde(default)]
+    pub search: SearchConfig,
 }
 
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+pub struct SearchConfig {
+    /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
+    pub smart_case: bool,
+    /// Whether the search should wrap after depleting the matches. Default to true.
+    pub wrap_around: bool,
+}
+
+// Cursor shape is read and used on every rendered frame and so needs
+// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
+#[derive(Debug, Clone, PartialEq)]
+pub struct CursorShapeConfig([CursorKind; 3]);
+
+impl CursorShapeConfig {
+    pub fn from_mode(&self, mode: Mode) -> CursorKind {
+        self.get(mode as usize).copied().unwrap_or_default()
+    }
+}
+
+impl<'de> Deserialize<'de> for CursorShapeConfig {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
+        let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
+        Ok(CursorShapeConfig([
+            into_cursor(Mode::Normal),
+            into_cursor(Mode::Select),
+            into_cursor(Mode::Insert),
+        ]))
+    }
+}
+
+impl Serialize for CursorShapeConfig {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut map = serializer.serialize_map(Some(self.len()))?;
+        let modes = [Mode::Normal, Mode::Select, Mode::Insert];
+        for mode in modes {
+            map.serialize_entry(&mode, &self.from_mode(mode))?;
+        }
+        map.end()
+    }
+}
+
+impl std::ops::Deref for CursorShapeConfig {
+    type Target = [CursorKind; 3];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl Default for CursorShapeConfig {
+    fn default() -> Self {
+        Self([CursorKind::Block; 3])
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub enum LineNumber {
     /// Show absolute line number
     Absolute,
 
-    /// Show relative line number to the primary cursor
+    /// If focused and in normal/select mode, show relative line number to the primary cursor.
+    /// If unfocused or in insert mode, show absolute line number.
     Relative,
 }
 
+impl std::str::FromStr for LineNumber {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.to_lowercase().as_str() {
+            "absolute" | "abs" => Ok(Self::Absolute),
+            "relative" | "rel" => Ok(Self::Relative),
+            _ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
+        }
+    }
+}
+
 impl Default for Config {
     fn default() -> Self {
         Self {
@@ -133,13 +216,24 @@ impl Default for Config {
             },
             line_number: LineNumber::Absolute,
             middle_click_paste: true,
-            smart_case: true,
             auto_pairs: true,
             auto_completion: true,
             idle_timeout: Duration::from_millis(400),
             completion_trigger_len: 2,
             auto_info: true,
             file_picker: FilePickerConfig::default(),
+            cursor_shape: CursorShapeConfig::default(),
+            true_color: false,
+            search: SearchConfig::default(),
+        }
+    }
+}
+
+impl Default for SearchConfig {
+    fn default() -> Self {
+        Self {
+            wrap_around: true,
+            smart_case: true,
         }
     }
 }
@@ -177,6 +271,7 @@ pub struct Editor {
     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,
 
@@ -190,6 +285,7 @@ pub struct Editor {
     pub theme_loader: Arc<theme::Loader>,
 
     pub status_msg: Option<(String, Severity)>,
+    pub autoinfo: Option<Info>,
 
     pub config: Config,
 
@@ -225,6 +321,7 @@ impl Editor {
             documents: BTreeMap::new(),
             count: None,
             selected_register: None,
+            macro_recording: None,
             theme: theme_loader.default(),
             language_servers,
             debugger: None,
@@ -235,6 +332,7 @@ impl Editor {
             registers: Registers::default(),
             clipboard_provider: get_clipboard_provider(),
             status_msg: None,
+            autoinfo: None,
             idle_timer: Box::pin(sleep(config.idle_timeout)),
             last_motion: None,
             config,
@@ -275,31 +373,16 @@ impl Editor {
         }
 
         let scopes = theme.scopes();
-        for config in self
-            .syn_loader
-            .language_configs_iter()
-            .filter(|cfg| cfg.is_highlight_initialized())
-        {
-            config.reconfigure(scopes);
-        }
+        self.syn_loader.set_scopes(scopes.to_vec());
 
         self.theme = theme;
         self._refresh();
     }
 
-    pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
-        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);
+        doc.detect_language(self.syn_loader.clone());
         Self::launch_language_server(&mut self.language_servers, doc)
     }
 
@@ -323,11 +406,8 @@ impl Editor {
                 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();
+
+                let language_id = doc.language_id().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(
@@ -394,7 +474,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`.
@@ -403,7 +484,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(view.doc)
+                        {
+                            view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
+                        }
+                    }
                 }
 
                 let view_id = view.id;
@@ -471,7 +561,7 @@ impl Editor {
         let id = if let Some(id) = id {
             id
         } else {
-            let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
+            let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
 
             let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
 
@@ -629,9 +719,10 @@ impl Editor {
             let inner = view.inner_area();
             pos.col += inner.x as usize;
             pos.row += inner.y as usize;
-            (Some(pos), CursorKind::Hidden)
+            let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
+            (Some(pos), cursorkind)
         } else {
-            (None, CursorKind::Hidden)
+            (None, CursorKind::default())
         }
     }
 
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index 0bfca04aa..6d0a92928 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -1,10 +1,12 @@
 use bitflags::bitflags;
+use serde::{Deserialize, Serialize};
 use std::{
     cmp::{max, min},
     str::FromStr,
 };
 
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
 /// UNSTABLE
 pub enum CursorKind {
     /// █
@@ -17,6 +19,12 @@ pub enum CursorKind {
     Hidden,
 }
 
+impl Default for CursorKind {
+    fn default() -> Self {
+        Self::Block
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Margin {
     pub vertical: u16,
@@ -25,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,
@@ -33,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.
@@ -334,7 +331,7 @@ impl FromStr for Modifier {
 /// ];
 /// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
 /// for style in &styles {
-///   buffer.get_mut(0, 0).set_style(*style);
+///   buffer[(0, 0)].set_style(*style);
 /// }
 /// assert_eq!(
 ///     Style {
@@ -343,7 +340,7 @@ impl FromStr for Modifier {
 ///         add_modifier: Modifier::BOLD,
 ///         sub_modifier: Modifier::empty(),
 ///     },
-///     buffer.get(0, 0).style(),
+///     buffer[(0, 0)].style(),
 /// );
 /// ```
 ///
@@ -359,7 +356,7 @@ impl FromStr for Modifier {
 /// ];
 /// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
 /// for style in &styles {
-///   buffer.get_mut(0, 0).set_style(*style);
+///   buffer[(0, 0)].set_style(*style);
 /// }
 /// assert_eq!(
 ///     Style {
@@ -368,7 +365,7 @@ impl FromStr for Modifier {
 ///         add_modifier: Modifier::empty(),
 ///         sub_modifier: Modifier::empty(),
 ///     },
-///     buffer.get(0, 0).style(),
+///     buffer[(0, 0)].style(),
 /// );
 /// ```
 #[derive(Debug, Clone, Copy, PartialEq)]
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
index e156b9e55..6a77c41fc 100644
--- a/helix-view/src/gutter.rs
+++ b/helix-view/src/gutter.rs
@@ -25,7 +25,8 @@ pub fn diagnostic<'doc>(
 
     Box::new(move |line: usize, _selected: bool, out: &mut String| {
         use helix_core::diagnostic::Severity;
-        if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
+        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,
@@ -60,29 +61,31 @@ pub fn line_number<'doc>(
         .char_to_line(doc.selection(view.id).primary().cursor(text));
 
     let config = editor.config.line_number;
+    let mode = doc.mode;
 
     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)
-                    }
-                }
+            use crate::{document::Mode, editor::LineNumber};
+
+            let relative = config == LineNumber::Relative
+                && mode != Mode::Insert
+                && is_focused
+                && current_line != line;
+
+            let display_num = if relative {
+                abs_diff(current_line, line)
+            } else {
+                line + 1
             };
             let style = if selected && is_focused {
                 linenr_select
             } else {
                 linenr
             };
-            write!(out, "{:>1$}", line, width).unwrap();
+            write!(out, "{:>1$}", display_num, width).unwrap();
             Some(style)
         }
     })
diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs
index b5a002fa4..5ad6a60c7 100644
--- a/helix-view/src/info.rs
+++ b/helix-view/src/info.rs
@@ -1,5 +1,5 @@
 use crate::input::KeyEvent;
-use helix_core::unicode::width::UnicodeWidthStr;
+use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
 use std::{collections::BTreeSet, fmt::Write};
 
 #[derive(Debug)]
@@ -16,33 +16,60 @@ pub struct Info {
 }
 
 impl Info {
-    pub fn new(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Info {
-        let body = body
-            .into_iter()
-            .map(|(desc, events)| {
-                let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
-                (desc, events.join(", "))
-            })
-            .collect::<Vec<_>>();
-
-        let keymaps_width = body.iter().map(|r| r.1.len()).max().unwrap();
-        let mut text = String::new();
-
-        for (desc, keyevents) in &body {
-            let _ = writeln!(
-                text,
-                "{:width$}  {}",
-                keyevents,
-                desc,
-                width = keymaps_width
-            );
+    pub fn new(title: &str, body: Vec<(String, String)>) -> Self {
+        if body.is_empty() {
+            return Self {
+                title: title.to_string(),
+                height: 1,
+                width: title.len() as u16,
+                text: "".to_string(),
+            };
         }
 
-        Info {
+        let item_width = body.iter().map(|(item, _)| item.width()).max().unwrap();
+        let mut text = String::new();
+
+        for (item, desc) in &body {
+            let _ = writeln!(text, "{:width$}  {}", item, desc, width = item_width);
+        }
+
+        Self {
             title: title.to_string(),
             width: text.lines().map(|l| l.width()).max().unwrap() as u16,
             height: body.len() as u16,
             text,
         }
     }
+
+    pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Self {
+        let body = body
+            .into_iter()
+            .map(|(desc, events)| {
+                let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
+                (events.join(", "), desc.to_string())
+            })
+            .collect();
+
+        Self::new(title, body)
+    }
+
+    pub fn from_registers(registers: &Registers) -> Self {
+        let body = registers
+            .inner()
+            .iter()
+            .map(|(ch, reg)| {
+                let content = reg
+                    .read()
+                    .get(0)
+                    .and_then(|s| s.lines().next())
+                    .map(String::from)
+                    .unwrap_or_default();
+                (ch.to_string(), content)
+            })
+            .collect();
+
+        let mut infobox = Self::new("Registers", body);
+        infobox.width = 30; // copied content could be very long
+        infobox
+    }
 }
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index 580204ccc..14dadc3b9 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,16 +216,81 @@ 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(),
+            }
+        }
+    }
+}
+
+pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
+    use anyhow::Context;
+    let mut keys_res: anyhow::Result<_> = Ok(Vec::new());
+    let mut i = 0;
+    while let Ok(keys) = &mut keys_res {
+        if i >= keys_str.len() {
+            break;
+        }
+        if !keys_str.is_char_boundary(i) {
+            i += 1;
+            continue;
+        }
+
+        let s = &keys_str[i..];
+        let mut end_i = 1;
+        while !s.is_char_boundary(end_i) {
+            end_i += 1;
+        }
+        let c = &s[..end_i];
+        if c == ">" {
+            keys_res = Err(anyhow!("Unmatched '>'"));
+        } else if c != "<" {
+            keys.push(c);
+            i += end_i;
+        } else {
+            match s.find('>').context("'>' expected") {
+                Ok(end_i) => {
+                    keys.push(&s[1..end_i]);
+                    i += end_i + 1;
+                }
+                Err(err) => keys_res = Err(err),
+            }
+        }
+    }
+    keys_res.and_then(|keys| keys.into_iter().map(str::parse).collect())
+}
+
 #[cfg(test)]
 mod test {
     use super::*;
@@ -315,4 +376,120 @@ mod test {
         assert!(str::parse::<KeyEvent>("123").is_err());
         assert!(str::parse::<KeyEvent>("S--").is_err());
     }
+
+    #[test]
+    fn parsing_valid_macros() {
+        assert_eq!(
+            parse_macro("xdo").ok(),
+            Some(vec![
+                KeyEvent {
+                    code: KeyCode::Char('x'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('d'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('o'),
+                    modifiers: KeyModifiers::NONE,
+                },
+            ]),
+        );
+
+        assert_eq!(
+            parse_macro("<C-w>v<C-w>h<C-o>xx<A-s>").ok(),
+            Some(vec![
+                KeyEvent {
+                    code: KeyCode::Char('w'),
+                    modifiers: KeyModifiers::CONTROL,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('v'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('w'),
+                    modifiers: KeyModifiers::CONTROL,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('h'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('o'),
+                    modifiers: KeyModifiers::CONTROL,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('x'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('x'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('s'),
+                    modifiers: KeyModifiers::ALT,
+                },
+            ])
+        );
+
+        assert_eq!(
+            parse_macro(":o foo.bar<ret>").ok(),
+            Some(vec![
+                KeyEvent {
+                    code: KeyCode::Char(':'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('o'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char(' '),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('f'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('o'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('o'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('.'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('b'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('a'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Char('r'),
+                    modifiers: KeyModifiers::NONE,
+                },
+                KeyEvent {
+                    code: KeyCode::Enter,
+                    modifiers: KeyModifiers::NONE,
+                },
+            ])
+        );
+    }
+
+    #[test]
+    fn parsing_invalid_macros_fails() {
+        assert!(parse_macro("abc<C-").is_err());
+        assert!(parse_macro("abc>123").is_err());
+        assert!(parse_macro("wd<foo>").is_err());
+    }
 }
diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs
index 810aa0635..f17172091 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/theme.rs b/helix-view/src/theme.rs
index 757316bde..00c1bbbd9 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 {
@@ -257,53 +296,58 @@ impl TryFrom<Value> for ThemePalette {
     }
 }
 
-#[test]
-fn test_parse_style_string() {
-    let fg = Value::String("#ffffff".to_string());
+#[cfg(test)]
+mod tests {
+    use super::*;
 
-    let mut style = Style::default();
-    let palette = ThemePalette::default();
-    palette.parse_style(&mut style, fg).unwrap();
+    #[test]
+    fn test_parse_style_string() {
+        let fg = Value::String("#ffffff".to_string());
 
-    assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
-}
+        let mut style = Style::default();
+        let palette = ThemePalette::default();
+        palette.parse_style(&mut style, fg).unwrap();
 
-#[test]
-fn test_palette() {
-    use helix_core::hashmap;
-    let fg = Value::String("my_color".to_string());
-
-    let mut style = Style::default();
-    let palette =
-        ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
-    palette.parse_style(&mut style, fg).unwrap();
-
-    assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
-}
-
-#[test]
-fn test_parse_style_table() {
-    let table = toml::toml! {
-        "keyword" = {
-            fg = "#ffffff",
-            bg = "#000000",
-            modifiers = ["bold"],
-        }
-    };
-
-    let mut style = Style::default();
-    let palette = ThemePalette::default();
-    if let Value::Table(entries) = table {
-        for (_name, value) in entries {
-            palette.parse_style(&mut style, value).unwrap();
-        }
+        assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
     }
 
-    assert_eq!(
-        style,
-        Style::default()
-            .fg(Color::Rgb(255, 255, 255))
-            .bg(Color::Rgb(0, 0, 0))
-            .add_modifier(Modifier::BOLD)
-    );
+    #[test]
+    fn test_palette() {
+        use helix_core::hashmap;
+        let fg = Value::String("my_color".to_string());
+
+        let mut style = Style::default();
+        let palette =
+            ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
+        palette.parse_style(&mut style, fg).unwrap();
+
+        assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
+    }
+
+    #[test]
+    fn test_parse_style_table() {
+        let table = toml::toml! {
+            "keyword" = {
+                fg = "#ffffff",
+                bg = "#000000",
+                modifiers = ["bold"],
+            }
+        };
+
+        let mut style = Style::default();
+        let palette = ThemePalette::default();
+        if let Value::Table(entries) = table {
+            for (_name, value) in entries {
+                palette.parse_style(&mut style, value).unwrap();
+            }
+        }
+
+        assert_eq!(
+            style,
+            Style::default()
+                .fg(Color::Rgb(255, 255, 255))
+                .bg(Color::Rgb(0, 0, 0))
+                .add_modifier(Modifier::BOLD)
+        );
+    }
 }
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 9336742b7..6bc9435cc 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -78,6 +78,13 @@ 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],
+    /// used to store previous selections of tree-sitter objecs
+    pub object_selections: Vec<Selection>,
 }
 
 impl View {
@@ -89,6 +96,8 @@ 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],
+            object_selections: Vec::new(),
         }
     }
 
@@ -370,7 +379,7 @@ mod tests {
         let text = rope.slice(..);
 
         assert_eq!(
-            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
+            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
             Some(0)
         );
 
@@ -403,7 +412,7 @@ mod tests {
         let text = rope.slice(..);
 
         assert_eq!(
-            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
+            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
             Some(0)
         );
 
diff --git a/languages.toml b/languages.toml
index 2b5016518..b6ab1d0e1 100644
--- a/languages.toml
+++ b/languages.toml
@@ -3,14 +3,11 @@ name = "rust"
 scope = "source.rust"
 injection-regex = "rust"
 file-types = ["rs"]
-roots = []
+roots = ["Cargo.toml", "Cargo.lock"]
 auto-format = true
 comment-token = "//"
 language-server = { command = "rust-analyzer" }
 indent = { tab-width = 4, unit = "    " }
-[language.config]
-cargo = { loadOutDirsFromCheck = true }
-procMacro = { enable = false }
 
 [language.debugger]
 name = "lldb-vscode"
@@ -73,6 +70,17 @@ comment-token = "#"
 language-server = { command = "elixir-ls" }
 indent = { tab-width = 2, unit = "  " }
 
+[[language]]
+name = "fish"
+scope = "source.fish"
+injection-regex = "fish"
+file-types = ["fish"]
+shebangs = ["fish"]
+roots = []
+comment-token = "#"
+
+indent = { tab-width = 4, unit = "    " }
+
 [[language]]
 name = "mint"
 scope = "source.mint"
@@ -226,6 +234,7 @@ roots = []
 comment-token = "//"
 # TODO: highlights-jsx, highlights-params
 
+language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
 indent = { tab-width = 2, unit = "  " }
 
 [language.debugger]
@@ -249,7 +258,7 @@ shebangs = []
 roots = []
 # TODO: highlights-jsx, highlights-params
 
-language-server = { command = "typescript-language-server", args = ["--stdio"] }
+language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
 indent = { tab-width = 2, unit = "  " }
 
 [[language]]
@@ -260,14 +269,14 @@ file-types = ["tsx"]
 roots = []
 # TODO: highlights-jsx, highlights-params
 
-language-server = { command = "typescript-language-server", args = ["--stdio"] }
+language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
 indent = { tab-width = 2, unit = "  " }
 
 [[language]]
 name = "css"
 scope = "source.css"
 injection-regex = "css"
-file-types = ["css"]
+file-types = ["css", "scss"]
 roots = []
 
 indent = { tab-width = 2, unit = "  " }
@@ -322,7 +331,7 @@ indent = { tab-width = 2, unit = "  " }
 name = "bash"
 scope = "source.bash"
 injection-regex = "bash"
-file-types = ["sh", "bash"]
+file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile", ".bashrc", ".profile", ".zshenv", ".zlogin", ".zlogout", ".zprofile", ".zshrc"]
 shebangs = ["sh", "bash", "dash"]
 roots = []
 comment-token = "#"
@@ -340,6 +349,15 @@ roots = []
 
 indent = { tab-width = 4, unit = "    " }
 
+[[language]]
+name = "twig"
+scope = "source.twig"
+injection-regex = "twig"
+file-types = ["twig"]
+roots = []
+
+indent = { tab-width = 2, unit = "  " }
+
 [[language]]
 name = "latex"
 scope = "source.tex"
@@ -350,6 +368,17 @@ comment-token = "%"
 
 indent = { tab-width = 4, unit = "\t" }
 
+[[language]]
+name = "lean"
+scope = "source.lean"
+injection-regex = "lean"
+file-types = ["lean"]
+roots = [ "lakefile.lean" ]
+comment-token = "--"
+language-server = { command = "lean", args = [ "--server" ] }
+
+indent = { tab-width = 2, unit = "  " }
+
 [[language]]
 name = "julia"
 scope = "source.julia"
@@ -367,7 +396,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);
@@ -380,7 +408,7 @@ name = "java"
 scope = "source.java"
 injection-regex = "java"
 file-types = ["java"]
-roots = []
+roots = ["pom.xml"]
 indent = { tab-width = 4, unit = "    " }
 
 [[language]]
@@ -445,16 +473,17 @@ file-types = ["yml", "yaml"]
 roots = []
 comment-token = "#"
 indent = { tab-width = 2, unit = "  " }
+injection-regex = "yml|yaml"
 
-# [[language]]
-# name = "haskell"
-# scope = "source.haskell"
-# injection-regex = "haskell"
-# file-types = ["hs"]
-# roots = []
-# comment-token = "--"
-#
-# indent = { tab-width = 2, unit = "  " }
+[[language]]
+name = "haskell"
+scope = "source.haskell"
+injection-regex = "haskell"
+file-types = ["hs"]
+roots = []
+comment-token = "--"
+language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
+indent = { tab-width = 2, unit = "  " }
 
 [[language]]
 name = "zig"
@@ -487,6 +516,7 @@ scope = "source.tsq"
 file-types = ["scm"]
 roots = []
 comment-token = ";"
+injection-regex = "tsq"
 indent = { tab-width = 2, unit = "  " }
 
 [[language]]
@@ -497,6 +527,14 @@ roots = []
 comment-token = "#"
 indent = { tab-width = 2, unit = "  " }
 language-server = { command = "cmake-language-server" }
+injection-regex = "cmake"
+
+[[language]]
+name = "make"
+scope = "source.make"
+file-types = ["Makefile", "makefile", "justfile", ".justfile"]
+roots =[]
+comment-token = "#"
 
 [[language]]
 name = "glsl"
@@ -505,6 +543,7 @@ file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
 roots = []
 comment-token = "//"
 indent = { tab-width = 4, unit = "    " }
+injection-regex = "glsl"
 
 [[language]]
 name = "perl"
@@ -524,6 +563,13 @@ shebangs = ["racket"]
 comment-token = ";"
 language-server = { command = "racket", args = ["-l", "racket-langserver"] }
 
+[[language]]
+name = "comment"
+scope = "scope.comment"
+roots = []
+file-types = []
+injection-regex = "comment"
+
 [[language]]
 name = "wgsl"
 scope = "source.wgsl"
@@ -539,3 +585,149 @@ roots = []
 file-types = ["ll"]
 comment-token = ";"
 indent = { tab-width = 2, unit = "  " }
+injection-regex = "llvm"
+
+[[language]]
+name = "llvm-mir"
+scope = "source.llvm_mir"
+roots = []
+file-types = []
+comment-token = ";"
+indent = { tab-width = 2, unit = "  " }
+injection-regex = "mir"
+
+[[language]]
+name = "llvm-mir-yaml"
+tree-sitter-library = "yaml"
+scope = "source.yaml"
+roots = []
+file-types = ["mir"]
+comment-token = "#"
+indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "tablegen"
+scope = "source.tablegen"
+roots = []
+file-types = ["td"]
+comment-token = "//"
+indent = { tab-width = 2, unit = "  " }
+injection-regex = "tablegen"
+
+[[language]]
+name = "markdown"
+scope = "source.md"
+injection-regex = "md|markdown"
+file-types = ["md"]
+roots = []
+
+indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "dart"
+scope = "source.dart"
+file-types = ["dart"]
+roots = ["pubspec.yaml"]
+auto-format = true
+comment-token = "//"
+language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
+indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "scala"
+scope = "source.scala"
+roots = ["build.sbt", "pom.xml"]
+file-types = ["scala", "sbt"]
+comment-token = "//"
+indent = { tab-width = 2, unit = "  " }
+language-server = { command = "metals" }
+
+[[language]]
+name = "dockerfile"
+scope = "source.dockerfile"
+injection-regex = "docker|dockerfile"
+roots = ["Dockerfile"]
+file-types = ["Dockerfile", "dockerfile"]
+comment-token = "#"
+indent = { tab-width = 2, unit = "  " }
+language-server = { command = "docker-langserver", args = ["--stdio"] }
+
+[[language]]
+name = "git-commit"
+scope = "git.commitmsg"
+roots = []
+file-types = ["COMMIT_EDITMSG"]
+comment-token = "#"
+indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "git-diff"
+scope = "source.diff"
+roots = []
+file-types = ["diff"]
+injection-regex = "diff"
+comment-token = "#"
+indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "git-rebase"
+scope = "source.gitrebase"
+roots = []
+file-types = ["git-rebase-todo"]
+injection-regex = "git-rebase"
+comment-token = "#"
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "regex"
+scope = "source.regex"
+injection-regex = "regex"
+file-types = ["regex"]
+roots = []
+
+[[language]]
+name = "git-config"
+scope = "source.gitconfig"
+roots = []
+# TODO: allow specifying file-types as a regex so we can read directory names (e.g. `.git/config`)
+file-types = [".gitmodules", ".gitconfig"]
+injection-regex = "git-config"
+comment-token = "#"
+indent = { tab-width = 4, unit = "\t" }
+
+[[language]]
+name = "graphql"
+scope = "source.graphql"
+injection-regex = "graphql"
+file-types = ["gql", "graphql"]
+roots = []
+indent = { tab-width = 2, unit = "  " }
+
+[[language]]
+name = "elm"
+scope = "source.elm"
+injection-regex = "elm"
+file-types = ["elm"]
+roots = ["elm.json"]
+auto-format = true
+comment-token = "--"
+language-server = { command = "elm-language-server" }
+indent = { tab-width = 4, unit = "    " }
+
+[[language]]
+name = "iex"
+scope = "source.iex"
+injection-regex = "iex"
+file-types = ["iex"]
+roots = []
+
+[[language]]
+name = "rescript"
+scope = "source.rescript"
+injection-regex = "rescript"
+file-types = ["res"]
+roots = ["bsconfig.json"]
+auto-format = true
+comment-token = "//"
+language-server = { command = "rescript-language-server", args = ["--stdio"] }
+indent = { tab-width = 2, unit = "  " }
diff --git a/runtime/queries/bash/injections.scm b/runtime/queries/bash/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/bash/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/c-sharp/injections.scm b/runtime/queries/c-sharp/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/c-sharp/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/c/indents.toml b/runtime/queries/c/indents.toml
new file mode 100644
index 000000000..f4076e171
--- /dev/null
+++ b/runtime/queries/c/indents.toml
@@ -0,0 +1,16 @@
+indent = [
+  "compound_statement",
+  "field_declaration_list",
+  "enumerator_list",
+  "parameter_list",
+  "init_declarator",
+  "case_statement",
+  "condition_clause",
+  "expression_statement",
+]
+
+outdent = [
+  "case",
+  "}",
+  "]",
+]
diff --git a/runtime/queries/c/injections.scm b/runtime/queries/c/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/c/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/c/textobjects.scm b/runtime/queries/c/textobjects.scm
new file mode 100644
index 000000000..b0f036685
--- /dev/null
+++ b/runtime/queries/c/textobjects.scm
@@ -0,0 +1,13 @@
+(function_definition
+  body: (_) @function.inside) @function.around
+
+(struct_specifier
+  body: (_) @class.inside) @class.around
+
+(enum_specifier
+  body: (_) @class.inside) @class.around
+
+(union_specifier
+  body: (_) @class.inside) @class.around
+
+(parameter_declaration) @parameter.inside
diff --git a/runtime/queries/cmake/indents.toml b/runtime/queries/cmake/indents.toml
new file mode 100644
index 000000000..8b886a4fb
--- /dev/null
+++ b/runtime/queries/cmake/indents.toml
@@ -0,0 +1,12 @@
+indent = [
+  "if_condition",
+  "foreach_loop",
+  "while_loop",
+  "function_def",
+  "macro_def",
+  "normal_command",
+]
+
+outdent = [
+  ")"
+]
diff --git a/runtime/queries/cmake/injections.scm b/runtime/queries/cmake/injections.scm
new file mode 100644
index 000000000..6cb6c254b
--- /dev/null
+++ b/runtime/queries/cmake/injections.scm
@@ -0,0 +1,4 @@
+((line_comment) @injection.content
+ (#set! injection.language "comment"))
+((bracket_comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/cmake/textobjects.scm b/runtime/queries/cmake/textobjects.scm
new file mode 100644
index 000000000..b0d1b1083
--- /dev/null
+++ b/runtime/queries/cmake/textobjects.scm
@@ -0,0 +1,3 @@
+(macro_def) @function.around
+
+(argument) @parameter.inside
diff --git a/runtime/queries/comment/highlights.scm b/runtime/queries/comment/highlights.scm
new file mode 100644
index 000000000..88685d59a
--- /dev/null
+++ b/runtime/queries/comment/highlights.scm
@@ -0,0 +1,30 @@
+[
+ "("
+ ")"
+] @punctuation.bracket
+
+":" @punctuation.delimiter
+
+((tag (name) @warning)
+ (#match? @warning "^(TODO|HACK|WARNING)$"))
+
+("text" @warning
+ (#match? @warning "^(TODO|HACK|WARNING)$"))
+
+((tag (name) @error)
+ (match? @error "^(FIXME|XXX|BUG)$"))
+
+("text" @error
+ (match? @error "^(FIXME|XXX|BUG)$"))
+
+(tag
+ (name) @ui.text
+ (user)? @constant)
+
+; Issue number (#123)
+("text" @constant.numeric
+ (#match? @constant.numeric "^#[0-9]+$"))
+
+; User mention (@user)
+("text" @tag
+ (#match? @tag "^[@][a-zA-Z0-9_-]+$"))
diff --git a/runtime/queries/cpp/indents.toml b/runtime/queries/cpp/indents.toml
new file mode 100644
index 000000000..0ca2ed8bf
--- /dev/null
+++ b/runtime/queries/cpp/indents.toml
@@ -0,0 +1,17 @@
+indent = [
+  "compound_statement",
+  "field_declaration_list",
+  "enumerator_list",
+  "parameter_list",
+  "init_declarator",
+  "case_statement",
+  "condition_clause",
+  "expression_statement",
+]
+
+outdent = [
+  "case",
+  "access_specifier",
+  "}",
+  "]",
+]
diff --git a/runtime/queries/cpp/injections.scm b/runtime/queries/cpp/injections.scm
new file mode 100644
index 000000000..a5a5208ca
--- /dev/null
+++ b/runtime/queries/cpp/injections.scm
@@ -0,0 +1 @@
+; inherits: c
diff --git a/runtime/queries/cpp/textobjects.scm b/runtime/queries/cpp/textobjects.scm
new file mode 100644
index 000000000..6e3de1a2c
--- /dev/null
+++ b/runtime/queries/cpp/textobjects.scm
@@ -0,0 +1,7 @@
+; inherits: c
+
+(lambda_expression
+  body: (_) @function.inside) @function.around
+
+(class_specifier
+  body: (_) @class.inside) @class.around
diff --git a/runtime/queries/css/injections.scm b/runtime/queries/css/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/css/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/dart/highlights.scm b/runtime/queries/dart/highlights.scm
new file mode 100644
index 000000000..9f667d6be
--- /dev/null
+++ b/runtime/queries/dart/highlights.scm
@@ -0,0 +1,237 @@
+(dotted_identifier_list) @string
+
+; Methods
+; --------------------
+(super) @function.builtin
+
+(function_expression_body (identifier) @function.method)
+((identifier)(selector (argument_part)) @function.method)
+
+; Annotations
+; --------------------
+(annotation
+  name: (identifier) @attribute)
+(marker_annotation
+  name: (identifier) @attribute)
+
+; Types
+; --------------------
+(class_definition
+  name: (identifier) @type)
+  
+(constructor_signature
+  name: (identifier) @function.method)
+
+(function_signature
+  name: (identifier) @function.method)
+
+(getter_signature
+  (identifier) @function.builtin)
+
+(setter_signature
+  name: (identifier) @function.builtin)
+
+(enum_declaration
+  name: (identifier) @type)
+
+(enum_constant
+  name: (identifier) @type.builtin)
+
+(void_type) @type.builtin
+
+((scoped_identifier
+  scope: (identifier) @type)
+ (#match? @type "^[a-zA-Z]"))
+ 
+((scoped_identifier
+  scope: (identifier) @type
+  name: (identifier) @type)
+ (#match? @type "^[a-zA-Z]"))
+
+; the DisabledDrawerButtons in : const DisabledDrawerButtons(history: true),
+(type_identifier) @type.builtin
+
+; Variables
+; --------------------
+; the "File" in var file = File();
+((identifier) @namespace
+ (#match? @namespace "^_?[A-Z].*[a-z]")) ; catch Classes or IClasses not CLASSES
+
+("Function" @type.builtin)
+(inferred_type) @type.builtin
+
+; properties
+(unconditional_assignable_selector
+  (identifier) @variable.other.member)
+
+(conditional_assignable_selector
+  (identifier) @variable.other.member)
+
+; assignments
+; --------------------
+; the "strings" in : strings = "some string"
+(assignment_expression
+  left: (assignable_expression) @variable)
+
+(this) @variable.builtin
+
+; Parameters
+; --------------------
+(formal_parameter
+    name: (identifier) @variable)
+
+(named_argument
+  (label (identifier) @variable))
+
+; Literals
+; --------------------
+[
+  (hex_integer_literal)
+  (decimal_integer_literal)
+  (decimal_floating_point_literal)
+  ;(octal_integer_literal)
+  ;(hex_floating_point_literal)
+] @constant.numeric.integer
+
+(symbol_literal) @string.special.symbol
+(string_literal) @string
+
+[
+  (const_builtin)
+  (final_builtin)
+] @variable.builtin
+
+[
+  (true)
+  (false)
+] @constant.builtin.boolean
+
+(null_literal) @constant.builtin
+
+(comment) @comment.line
+(documentation_comment) @comment.block.documentation
+
+; Tokens
+; --------------------
+(template_substitution
+  "$" @punctuation.special
+  "{" @punctuation.special
+  "}" @punctuation.special
+) @embedded
+
+(template_substitution
+  "$" @punctuation.special
+  (identifier_dollar_escaped) @variable
+) @embedded
+
+(escape_sequence) @constant.character.escape
+
+; Punctuation
+;---------------------
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+]  @punctuation.bracket
+
+[
+  ";"
+  "."
+  ","
+  ":"
+] @punctuation.delimiter
+  
+; Operators
+;---------------------
+[
+ "@"
+ "?"
+ "=>"
+ ".."
+ "=="
+ "&&"
+ "%"
+ "<"
+ ">"
+ "="
+ ">="
+ "<="
+ "||"
+ (multiplicative_operator)
+ (increment_operator)
+ (is_operator)
+ (prefix_operator)
+ (equality_operator)
+ (additive_operator)
+] @operator
+
+; Keywords
+; --------------------
+["import" "library" "export"] @keyword.control.import
+["do" "while" "continue" "for"] @keyword.control.repeat
+["return" "yield"] @keyword.control.return
+["as" "in" "is"] @keyword.operator
+
+[
+  "?."
+  "??"
+  "if"
+  "else"
+  "switch"
+  "default"
+  "late"
+] @keyword.control.conditional
+
+[
+  "try"
+  "throw"
+  "catch"
+  "finally"
+  (break_statement)
+] @keyword.control.exception
+
+; Reserved words (cannot be used as identifiers)
+[
+    (case_builtin)
+    "abstract"
+    "async"
+    "async*"
+    "await"
+    "class"
+    "covariant"
+    "deferred"
+    "dynamic"
+    "enum"
+    "extends"
+    "extension"
+    "external"
+    "factory"
+    "Function"
+    "get"
+    "implements"
+    "interface"
+    "mixin"
+    "new"
+    "on"
+    "operator"
+    "part"
+    "required"
+    "set"
+    "show"
+    "static"
+    "super"
+    "sync*"
+    "typedef"
+    "with"
+] @keyword
+
+; when used as an identifier:
+((identifier) @variable.builtin
+ (#match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$"))
+
+; Error
+(ERROR) @error
+
diff --git a/runtime/queries/dart/indents.toml b/runtime/queries/dart/indents.toml
new file mode 100644
index 000000000..5c11e05dd
--- /dev/null
+++ b/runtime/queries/dart/indents.toml
@@ -0,0 +1,20 @@
+indent = [
+  "class_body",
+  "function_body",
+  "function_expression_body",
+  "declaration",
+  "initializers",
+  "switch_block",
+  "if_statement",
+  "formal_parameter_list",
+  "formal_parameter",
+  "list_literal",
+  "return_statement",
+  "arguments"
+]
+
+outdent = [
+ "}",
+ "]",
+ ")"
+]
diff --git a/runtime/queries/dart/injections.scm b/runtime/queries/dart/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/dart/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/dart/locals.scm b/runtime/queries/dart/locals.scm
new file mode 100644
index 000000000..629838e52
--- /dev/null
+++ b/runtime/queries/dart/locals.scm
@@ -0,0 +1,20 @@
+; Scopes
+;-------
+
+[
+ (block)
+ (try_statement)
+ (catch_clause)
+ (finally_clause)
+] @local.scope
+
+; Definitions
+;------------
+
+(class_definition
+ body: (_) @local.definition)
+
+; References
+;------------
+
+(identifier) @local.reference
diff --git a/runtime/queries/dockerfile/highlights.scm b/runtime/queries/dockerfile/highlights.scm
new file mode 100644
index 000000000..5a945fb9b
--- /dev/null
+++ b/runtime/queries/dockerfile/highlights.scm
@@ -0,0 +1,51 @@
+[
+	"FROM"
+	"AS"
+	"RUN"
+	"CMD"
+	"LABEL"
+	"EXPOSE"
+	"ENV"
+	"ADD"
+	"COPY"
+	"ENTRYPOINT"
+	"VOLUME"
+	"USER"
+	"WORKDIR"
+	"ARG"
+	"ONBUILD"
+	"STOPSIGNAL"
+	"HEALTHCHECK"
+	"SHELL"
+	"MAINTAINER"
+	"CROSS_BUILD"
+] @keyword
+
+[
+	":"
+	"@"
+] @operator
+
+(comment) @comment
+
+
+(image_spec
+	(image_tag
+		":" @punctuation.special)
+	(image_digest
+		"@" @punctuation.special))
+
+(double_quoted_string) @string
+
+(expansion
+  [
+	"$"
+	"{"
+	"}"
+  ] @punctuation.special
+) @none
+
+((variable) @constant
+ (#match? @constant "^[A-Z][A-Z_0-9]*$"))
+
+
diff --git a/runtime/queries/dockerfile/injections.scm b/runtime/queries/dockerfile/injections.scm
new file mode 100644
index 000000000..20396f1ae
--- /dev/null
+++ b/runtime/queries/dockerfile/injections.scm
@@ -0,0 +1,6 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
+([(shell_command) (shell_fragment)] @injection.content
+ (#set! injection.language "bash"))
+
diff --git a/runtime/queries/elixir/injections.scm b/runtime/queries/elixir/injections.scm
new file mode 100644
index 000000000..8370a0d8d
--- /dev/null
+++ b/runtime/queries/elixir/injections.scm
@@ -0,0 +1,9 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
+((sigil
+  (sigil_name) @_sigil_name
+  (quoted_content) @injection.content)
+ (#match? @_sigil_name "^(r|R)$")
+ (#set! injection.language "regex")
+ (#set! injection.combined))
diff --git a/runtime/queries/elm/highlights.scm b/runtime/queries/elm/highlights.scm
new file mode 100644
index 000000000..3c8fd12d8
--- /dev/null
+++ b/runtime/queries/elm/highlights.scm
@@ -0,0 +1,83 @@
+; Keywords
+[
+    "if"
+    "then"
+    "else"
+    "let"
+    "in"
+ ] @keyword.control
+(case) @keyword.control
+(of) @keyword.control
+
+(colon) @keyword.operator
+(backslash) @keyword
+(as) @keyword
+(port) @keyword
+(exposing) @keyword
+(alias) @keyword
+(infix) @keyword
+
+(arrow) @keyword.operator
+(dot) @keyword.operator
+
+(port) @keyword
+
+(type_annotation(lower_case_identifier) @function)
+(port_annotation(lower_case_identifier) @function)
+(file (value_declaration (function_declaration_left(lower_case_identifier) @function)))
+
+(field name: (lower_case_identifier) @attribute)
+(field_access_expr(lower_case_identifier) @attribute)
+
+(operator_identifier) @keyword.operator
+(eq) @keyword.operator.assignment
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+] @punctuation.bracket
+
+"|" @keyword
+"," @punctuation.delimiter
+
+[
+  "|>"
+] @keyword
+
+
+(import) @keyword.contol.import
+(module) @keyword.other
+
+(number_constant_expr) @constant.numeric
+
+(type) @type
+
+(type_declaration(upper_case_identifier) @type)
+(type_ref) @type
+(type_alias_declaration name: (upper_case_identifier) @type)
+
+(union_pattern constructor: (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member)) 
+(union_pattern constructor: (upper_case_qid (upper_case_identifier) @variable.other.member)) 
+
+(union_variant(upper_case_identifier) @variable.other.member)
+(value_expr name: (value_qid (upper_case_identifier) @label))
+(value_expr (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member))
+(value_expr(upper_case_qid(upper_case_identifier)) @variable.other.member)
+
+; comments
+(line_comment) @comment
+(block_comment) @comment
+
+; strings
+(string_escape) @constant.character.escape
+
+(open_quote) @string
+(close_quote) @string
+(regular_string_part) @string
+
+(open_char) @constant.character
+(close_char) @constant.character
diff --git a/runtime/queries/elm/injections.scm b/runtime/queries/elm/injections.scm
new file mode 100644
index 000000000..83f8245ca
--- /dev/null
+++ b/runtime/queries/elm/injections.scm
@@ -0,0 +1,4 @@
+; Parse glsl where defined
+
+((glsl_content) @injection.content
+ (#set! injection.language "glsl"))
diff --git a/runtime/queries/elm/locals.scm b/runtime/queries/elm/locals.scm
new file mode 100644
index 000000000..ab1031156
--- /dev/null
+++ b/runtime/queries/elm/locals.scm
@@ -0,0 +1,14 @@
+(value_declaration) @local.scope
+(type_alias_declaration) @local.scope
+(type_declaration) @local.scope
+(type_annotation) @local.scope
+(port_annotation) @local.scope
+(infix_declaration) @local.scope
+(let_in_expr) @local.scope
+
+(function_declaration_left (lower_pattern (lower_case_identifier)) @local.definition)
+(function_declaration_left (lower_case_identifier) @local.definition)
+
+(value_expr(value_qid(upper_case_identifier)) @local.reference)
+(value_expr(value_qid(lower_case_identifier)) @local.reference)
+(type_ref (upper_case_qid) @local.reference)
diff --git a/runtime/queries/elm/tags.scm b/runtime/queries/elm/tags.scm
new file mode 100644
index 000000000..03999fb17
--- /dev/null
+++ b/runtime/queries/elm/tags.scm
@@ -0,0 +1,19 @@
+(value_declaration (function_declaration_left (lower_case_identifier) @name)) @definition.function
+
+(function_call_expr (value_expr (value_qid) @name)) @reference.function
+(exposed_value (lower_case_identifier) @name) @reference.function
+(type_annotation ((lower_case_identifier) @name) (colon)) @reference.function
+
+(type_declaration ((upper_case_identifier) @name) ) @definition.type
+
+(type_ref (upper_case_qid (upper_case_identifier) @name)) @reference.type
+(exposed_type (upper_case_identifier) @name) @reference.type
+
+(type_declaration (union_variant (upper_case_identifier) @name)) @definition.union
+
+(value_expr (upper_case_qid (upper_case_identifier) @name)) @reference.union
+
+
+(module_declaration 
+    (upper_case_qid (upper_case_identifier)) @name
+) @definition.module
diff --git a/runtime/queries/fish/highlights.scm b/runtime/queries/fish/highlights.scm
new file mode 100644
index 000000000..def539319
--- /dev/null
+++ b/runtime/queries/fish/highlights.scm
@@ -0,0 +1,156 @@
+;; Operators
+
+[
+ "&&"
+ "||"
+ "|"
+ "&"
+ "="
+ "!="
+ ".."
+ "!"
+ (direction)
+ (stream_redirect)
+ (test_option)
+] @operator
+
+[
+ "not"
+ "and"
+ "or"
+] @keyword.operator
+
+;; Conditionals
+
+(if_statement
+[
+ "if"
+ "end"
+] @keyword.control.conditional)
+
+(switch_statement
+[
+ "switch"
+ "end"
+] @keyword.control.conditional)
+
+(case_clause
+[
+ "case"
+] @keyword.control.conditional)
+
+(else_clause 
+[
+ "else"
+] @keyword.control.conditional)
+
+(else_if_clause 
+[
+ "else"
+ "if"
+] @keyword.control.conditional)
+
+;; Loops/Blocks
+
+(while_statement
+[
+ "while"
+ "end"
+] @keyword.control.repeat)
+
+(for_statement
+[
+ "for"
+ "end"
+] @keyword.control.repeat)
+
+(begin_statement
+[
+ "begin"
+ "end"
+] @keyword.control.repeat)
+
+;; Keywords
+
+[
+ "in"
+ (break)
+ (continue)
+] @keyword
+
+"return" @keyword.control.return
+
+;; Punctuation
+
+[
+ "["
+ "]"
+ "{"
+ "}"
+ "("
+ ")"
+] @punctuation.bracket
+
+"," @punctuation.delimiter
+
+;; Commands
+
+(command
+  argument: [
+             (word) @variable.parameter (#match? @variable.parameter "^-")
+            ]
+)
+
+; non-bultin command names
+(command name: (word) @function)
+
+; derived from builtin -n (fish 3.2.2)
+(command
+  name: [
+        (word) @function.builtin
+        (#match? @function.builtin "^(\.|:|_|alias|argparse|bg|bind|block|breakpoint|builtin|cd|command|commandline|complete|contains|count|disown|echo|emit|eval|exec|exit|fg|functions|history|isatty|jobs|math|printf|pwd|random|read|realpath|set|set_color|source|status|string|test|time|type|ulimit|wait)$")
+        ]
+)
+
+(test_command "test" @function.builtin)
+
+;; Functions
+
+(function_definition ["function" "end"] @keyword.function)
+
+(function_definition
+  name: [
+        (word) (concatenation)
+        ] 
+@function)
+
+(function_definition
+  option: [
+          (word)
+          (concatenation (word))
+          ] @variable.parameter (#match? @variable.parameter "^-")
+)
+
+;; Strings
+
+[(double_quote_string) (single_quote_string)] @string
+(escape_sequence) @constant.character.escape
+
+;; Variables
+
+(variable_name) @variable
+(variable_expansion) @constant
+
+;; Nodes
+
+(integer) @constant.numeric.integer
+(float) @constant.numeric.float
+(comment) @comment
+(test_option) @string
+
+((word) @constant.builtin.boolean
+(#match? @constant.builtin.boolean "^(true|false)$"))
+
+;; Error
+
+(ERROR) @error
diff --git a/runtime/queries/fish/indents.toml b/runtime/queries/fish/indents.toml
new file mode 100644
index 000000000..6f1e563ae
--- /dev/null
+++ b/runtime/queries/fish/indents.toml
@@ -0,0 +1,12 @@
+indent = [
+  "function_definition",
+  "while_statement",
+  "for_statement",
+  "if_statement",
+  "begin_statement",
+  "switch_statement",
+]
+
+outdent = [
+  "end"
+]
diff --git a/runtime/queries/fish/injections.scm b/runtime/queries/fish/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/fish/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/fish/textobjects.scm b/runtime/queries/fish/textobjects.scm
new file mode 100644
index 000000000..67fd6614e
--- /dev/null
+++ b/runtime/queries/fish/textobjects.scm
@@ -0,0 +1 @@
+(function_definition) @function.around
diff --git a/runtime/queries/git-commit/highlights.scm b/runtime/queries/git-commit/highlights.scm
new file mode 100644
index 000000000..0b50d4190
--- /dev/null
+++ b/runtime/queries/git-commit/highlights.scm
@@ -0,0 +1,14 @@
+(subject) @markup.heading
+(path) @string.special.path
+(branch) @string.special.symbol
+(commit) @constant
+(item) @markup.link.url
+(header) @tag
+
+(change kind: "new file" @diff.plus)
+(change kind: "deleted" @diff.minus)
+(change kind: "modified" @diff.delta)
+(change kind: "renamed" @diff.delta.moved)
+
+[":" "->"] @punctuation.delimeter
+(comment) @comment
diff --git a/runtime/queries/git-commit/injections.scm b/runtime/queries/git-commit/injections.scm
new file mode 100644
index 000000000..cf0657f72
--- /dev/null
+++ b/runtime/queries/git-commit/injections.scm
@@ -0,0 +1,8 @@
+((comment (scissors))
+ (message) @injection.content
+ (#set! injection.include-children)
+ (#set! injection.language "diff"))
+
+((rebase_command) @injection.content
+ (#set! injection.include-children)
+ (#set! injection.language "git-rebase"))
diff --git a/runtime/queries/git-config/highlights.scm b/runtime/queries/git-config/highlights.scm
new file mode 100644
index 000000000..84767edc6
--- /dev/null
+++ b/runtime/queries/git-config/highlights.scm
@@ -0,0 +1,27 @@
+((section_name) @keyword.directive
+ (#eq? @keyword.directive "include"))
+
+((section_header
+   (section_name) @keyword.directive
+   (subsection_name))
+ (#eq? @keyword.directive "includeIf"))
+
+(section_name) @markup.heading
+(variable (name) @variable.other.member)
+[(true) (false)] @constant.builtin.boolean
+(integer) @constant.numeric.integer
+
+((string) @string.special.path
+ (#match? @string.special.path "^(~|./|/)"))
+
+[(string) (subsection_name)] @string
+
+[
+  "["
+  "]"
+  "\""
+] @punctuation.bracket
+
+"=" @punctuation.delimiter
+
+(comment) @comment
diff --git a/runtime/queries/git-diff/highlights.scm b/runtime/queries/git-diff/highlights.scm
new file mode 100644
index 000000000..1c1a8829f
--- /dev/null
+++ b/runtime/queries/git-diff/highlights.scm
@@ -0,0 +1,6 @@
+[(addition) (new_file)] @diff.plus
+[(deletion) (old_file)] @diff.minus
+
+(commit) @constant
+(location) @attribute
+(command) @markup.bold
diff --git a/runtime/queries/git-rebase/highlights.scm b/runtime/queries/git-rebase/highlights.scm
new file mode 100644
index 000000000..4f007037d
--- /dev/null
+++ b/runtime/queries/git-rebase/highlights.scm
@@ -0,0 +1,11 @@
+(operation operator: ["p" "pick" "r" "reword" "e" "edit" "s" "squash" "m" "merge" "d" "drop" "b" "break" "x" "exec"] @keyword)
+(operation operator: ["l" "label" "t" "reset"] @function)
+(operation operator: ["f" "fixup"] @function.special)
+
+(option) @operator
+(label) @string.special.symbol
+(commit) @constant
+"#" @punctuation.delimiter
+(comment) @comment
+
+(ERROR) @error
diff --git a/runtime/queries/git-rebase/injections.scm b/runtime/queries/git-rebase/injections.scm
new file mode 100644
index 000000000..070129b63
--- /dev/null
+++ b/runtime/queries/git-rebase/injections.scm
@@ -0,0 +1,4 @@
+((operation
+   operator: ["x" "exec"]
+   (command) @injection.content)
+ (#set! injection.language "bash"))
diff --git a/runtime/queries/glsl/injections.scm b/runtime/queries/glsl/injections.scm
index 7d3323b16..6330ea3e4 100644
--- a/runtime/queries/glsl/injections.scm
+++ b/runtime/queries/glsl/injections.scm
@@ -1,3 +1,4 @@
-(preproc_arg) @glsl
+; inherits: c
 
-(comment) @comment
+((preproc_arg) @injection.content
+ (#set! injection.language "glsl"))
diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm
index 56384d4d7..4ff8675b1 100644
--- a/runtime/queries/go/highlights.scm
+++ b/runtime/queries/go/highlights.scm
@@ -69,6 +69,7 @@
   "|"
   "|="
   "||"
+  "~"
 ] @operator
 
 ; Keywords
@@ -143,6 +144,9 @@
   (false)
 ] @constant.builtin.boolean
 
-(nil) @constant.builtin
+[
+  (nil)
+  (iota)
+] @constant.builtin
 
 (comment) @comment
diff --git a/runtime/queries/go/injections.scm b/runtime/queries/go/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/go/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/graphql/highlights.scm b/runtime/queries/graphql/highlights.scm
new file mode 100644
index 000000000..9fab40515
--- /dev/null
+++ b/runtime/queries/graphql/highlights.scm
@@ -0,0 +1,163 @@
+; Types
+;------
+
+(scalar_type_definition
+  (name) @type)
+
+(object_type_definition
+  (name) @type)
+
+(interface_type_definition
+  (name) @type)
+
+(union_type_definition
+  (name) @type)
+
+(enum_type_definition
+  (name) @type)
+
+(input_object_type_definition
+  (name) @type)
+
+(directive_definition
+  (name) @type)
+
+(directive_definition
+  "@" @type)
+
+(scalar_type_extension
+  (name) @type)
+
+(object_type_extension
+  (name) @type)
+
+(interface_type_extension
+  (name) @type)
+
+(union_type_extension
+  (name) @type)
+
+(enum_type_extension
+  (name) @type)
+
+(input_object_type_extension
+  (name) @type)
+
+(named_type
+  (name) @type)
+
+(directive) @type
+
+; Properties
+;-----------
+
+(field
+  (name) @variable.other.member)
+
+(field
+  (alias
+    (name) @variable.other.member))
+
+(field_definition
+  (name) @variable.other.member)
+
+(object_value
+  (object_field
+    (name) @variable.other.member))
+
+(enum_value
+  (name) @variable.other.member)
+
+; Variable Definitions and Arguments 
+;-----------------------------------
+
+(operation_definition
+  (name) @variable)
+
+(fragment_name
+  (name) @variable)
+
+(input_fields_definition
+  (input_value_definition
+    (name) @variable.parameter))
+
+(argument
+  (name) @variable.parameter)
+
+(arguments_definition
+  (input_value_definition
+    (name) @variable.parameter))
+
+(variable_definition
+  (variable) @variable.parameter)
+
+(argument
+  (value
+    (variable) @variable))
+
+; Constants
+;----------
+
+(string_value) @string
+
+(int_value) @constants.numeric.integer
+
+(float_value) @constants.numeric.float
+
+(boolean_value) @constants.builtin.boolean
+
+; Literals
+;---------
+
+(description) @comment
+
+(comment) @comment
+
+(directive_location
+  (executable_directive_location) @type.builtin)
+
+(directive_location
+  (type_system_directive_location) @type.builtin)
+
+; Keywords
+;----------
+
+[
+  "query"
+  "mutation"
+  "subscription"
+  "fragment"
+  "scalar"
+  "type"
+  "interface"
+  "union"
+  "enum"
+  "input"
+  "extend"
+  "directive"
+  "schema"
+  "on"
+  "repeatable"
+  "implements"
+] @keyword
+
+; Punctuation
+;------------
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+"=" @operator
+
+"|" @punctuation.delimiter
+"&" @punctuation.delimiter
+":" @punctuation.delimiter
+
+"..." @punctuation.special
+"!" @punctuation.special
diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm
index 721878769..5009f3b51 100644
--- a/runtime/queries/haskell/highlights.scm
+++ b/runtime/queries/haskell/highlights.scm
@@ -1,45 +1,125 @@
-(variable) @variable
-(operator) @operator
-(exp_name (constructor) @constructor)
-(constructor_operator) @operator
-(module) @namespace
-(type) @type
-(type) @class
-(constructor) @constructor
-(pragma) @pragma
-(comment) @comment
-(signature name: (variable) @fun_type_name)
-(function name: (variable) @function)
-(constraint class: (class_name (type)) @class)
-(class (class_head class: (class_name (type)) @class))
-(instance (instance_head class: (class_name (type)) @class))
+;; ----------------------------------------------------------------------------
+;; Literals and comments
+
 (integer) @constant.numeric.integer
+(exp_negation) @constant.numeric.integer
 (exp_literal (float)) @constant.numeric.float
 (char) @constant.character
-(con_unit) @literal
-(con_list) @literal
-(tycon_arrow) @operator
-(where) @keyword
-"module" @keyword
-"let" @keyword
-"in" @keyword
-"class" @keyword
-"instance" @keyword
-"data" @keyword
-"newtype" @keyword
-"family" @keyword
-"type" @keyword
-"import" @keyword
-"qualified" @keyword
-"as" @keyword
-"deriving" @keyword
-"via" @keyword
-"stock" @keyword
-"anyclass" @keyword
-"do" @keyword
-"mdo" @keyword
-"rec" @keyword
+(string) @string
+
+(con_unit) @constant.builtin ; unit, as in ()
+
+(comment) @comment
+
+
+;; ----------------------------------------------------------------------------
+;; Punctuation
+
 [
   "("
   ")"
+  "{"
+  "}"
+  "["
+  "]"
 ] @punctuation.bracket
+
+[
+  (comma)
+  ";"
+] @punctuation.delimiter
+
+
+;; ----------------------------------------------------------------------------
+;; Keywords, operators, includes
+
+(pragma) @constant.macro
+
+[
+  "if"
+  "then"
+  "else"
+  "case"
+  "of"
+] @keyword.control.conditional
+
+[
+  "import"
+  "qualified"
+  "module"
+] @keyword.control.import
+
+[
+  (operator)
+  (constructor_operator)
+  (type_operator)
+  (tycon_arrow)
+  (qualified_module)  ; grabs the `.` (dot), ex: import System.IO
+  (all_names)
+  (wildcard)
+  "="
+  "|"
+  "::"
+  "=>"
+  "->"
+  "<-"
+  "\\"
+  "`"
+  "@"
+] @operator
+
+(qualified_module (module) @constructor)
+(qualified_type (module) @namespace)
+(qualified_variable (module) @namespace)
+(import (module) @namespace)
+
+[
+  (where)
+  "let"
+  "in"
+  "class"
+  "instance"
+  "data"
+  "newtype"
+  "family"
+  "type"
+  "as"
+  "hiding"
+  "deriving"
+  "via"
+  "stock"
+  "anyclass"
+  "do"
+  "mdo"
+  "rec"
+  "forall"
+  "∀"
+  "infix"
+  "infixl"
+  "infixr"
+] @keyword
+
+
+;; ----------------------------------------------------------------------------
+;; Functions and variables
+
+(signature name: (variable) @type)
+(function name: (variable) @function)
+
+(variable) @variable
+"_" @variable.builtin
+
+(exp_infix (variable) @operator)  ; consider infix functions as operators
+
+("@" @namespace)  ; "as" pattern operator, e.g. x@Constructor
+
+
+;; ----------------------------------------------------------------------------
+;; Types
+
+(type) @type
+
+(constructor) @constructor
+
+; True or False
+((constructor) @_bool (#match? @_bool "(True|False)")) @constant.builtin.boolean
diff --git a/runtime/queries/haskell/injections.scm b/runtime/queries/haskell/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/haskell/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/html/injections.scm b/runtime/queries/html/injections.scm
index 71e7c3aed..ef58f4156 100644
--- a/runtime/queries/html/injections.scm
+++ b/runtime/queries/html/injections.scm
@@ -1,3 +1,6 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
 ((script_element
   (raw_text) @injection.content)
  (#set! injection.language "javascript"))
diff --git a/runtime/queries/iex/highlights.scm b/runtime/queries/iex/highlights.scm
new file mode 100644
index 000000000..2847fbff6
--- /dev/null
+++ b/runtime/queries/iex/highlights.scm
@@ -0,0 +1 @@
+(prompt) @comment
diff --git a/runtime/queries/iex/injections.scm b/runtime/queries/iex/injections.scm
new file mode 100644
index 000000000..48863d9db
--- /dev/null
+++ b/runtime/queries/iex/injections.scm
@@ -0,0 +1,6 @@
+((evaluation_block (prompt_line (expression) @injection.content))
+ (#set! injection.language "elixir")
+ (#set! injection.combined))
+
+((result) @injection.content
+ (#set! injection.language "elixir"))
diff --git a/runtime/queries/java/injections.scm b/runtime/queries/java/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/java/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/javascript/injections.scm b/runtime/queries/javascript/injections.scm
index 5539241a6..e84291115 100644
--- a/runtime/queries/javascript/injections.scm
+++ b/runtime/queries/javascript/injections.scm
@@ -9,6 +9,14 @@
   ]
   arguments: (template_string) @injection.content)
 
+; Parse the contents of gql template literals
+
+((call_expression
+   function: (identifier) @_template_function_name
+   arguments: (template_string) @injection.content)
+ (#eq? @_template_function_name "gql")
+ (#set! injection.language "graphql"))
+
 ; Parse regex syntax within regex literals
 
 ((regex_pattern) @injection.content
diff --git a/runtime/queries/julia/injections.scm b/runtime/queries/julia/injections.scm
index be2412c06..1c1e804ec 100644
--- a/runtime/queries/julia/injections.scm
+++ b/runtime/queries/julia/injections.scm
@@ -1,5 +1,5 @@
-; TODO: re-add when markdown is added.
-; ((triple_string) @markdown
-;   (#offset! @markdown 0 3 0 -3))
+((triple_string) @injection.content
+ (#set! injection.language "markdown"))
 
-(comment) @comment
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/julia/locals.scm b/runtime/queries/julia/locals.scm
index f8b34f71d..d5ac794e8 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 f045c82d1..0a030b31f 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.link.url)
  (#match? @_name "^(\\\\url|\\\\href)$"))
 
 (ERROR) @error
diff --git a/runtime/queries/latex/injections.scm b/runtime/queries/latex/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/latex/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/lean/folds.scm b/runtime/queries/lean/folds.scm
new file mode 100644
index 000000000..2c2bbb33a
--- /dev/null
+++ b/runtime/queries/lean/folds.scm
@@ -0,0 +1,15 @@
+[
+  (namespace)
+  (section)
+
+  (instance)
+  (def)
+  (theorem)
+  (example)
+
+  (product)
+  (array)
+  (list)
+
+  (string)
+] @fold
diff --git a/runtime/queries/lean/highlights.scm b/runtime/queries/lean/highlights.scm
new file mode 100644
index 000000000..a64feb1d3
--- /dev/null
+++ b/runtime/queries/lean/highlights.scm
@@ -0,0 +1,217 @@
+(open
+  namespace: (identifier) @namespace)
+(namespace
+  name: (identifier) @namespace)
+(section
+  name: (identifier) @namespace)
+
+;; Identifier naming conventions
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(arrow) @type
+(product) @type
+
+;; Declarations
+
+[
+  "abbrev"
+  "def"
+  "theorem"
+  "constant"
+  "instance"
+  "axiom"
+  "example"
+  "inductive"
+  "structure"
+  "class"
+
+  "deriving"
+
+  "section"
+  "namespace"
+] @keyword
+
+(attributes
+  (identifier) @function)
+
+(abbrev
+  name: (identifier) @type)
+(def
+  name: (identifier) @function)
+(theorem
+  name: (identifier) @function)
+(constant
+  name: (identifier) @type)
+(instance
+  name: (identifier) @function)
+(instance
+  type: (identifier) @type)
+(axiom
+  name: (identifier) @function)
+(structure
+  name: (identifier) @type)
+(structure
+  extends: (identifier) @type)
+
+(where_decl
+  type: (identifier) @type)
+
+(proj
+  name: (identifier) @field)
+
+(binders
+  type: (identifier) @type)
+
+["if" "then" "else"] @keyword.control.conditional
+
+["for" "in" "do"] @keyword.control.repeat
+
+(import) @include
+
+; Tokens
+
+[
+  "!"
+  "$"
+  "%"
+  "&&"
+  "*"
+  "*>"
+  "+"
+  "++"
+  "-"
+  "/"
+  "::"
+  ":="
+  "<"
+  "<$>"
+  "<*"
+  "<*>"
+  "<="
+  "<|"
+  "<|>"
+  "="
+  "=="
+  "=>"
+  ">"
+  ">"
+  ">="
+  ">>"
+  ">>="
+  "@"
+  "^"
+  "|>"
+  "|>."
+  "||"
+  "←"
+  "→"
+  "↔"
+  "∘"
+  "∧"
+  "∨"
+  "≠"
+  "≤"
+  "≥"
+] @operator
+
+[
+  "@&"
+] @operator
+
+[
+  "attribute"
+  "by"
+  "end"
+  "export"
+  "extends"
+  "fun"
+  "let"
+  "have"
+  "match"
+  "open"
+  "return"
+  "universe"
+  "variable"
+  "where"
+  "with"
+  "λ"
+  (hash_command)
+  (prelude)
+  (sorry)
+] @keyword
+
+[
+  "prefix"
+  "infix"
+  "infixl"
+  "infixr"
+  "postfix"
+  "notation"
+  "macro_rules"
+  "syntax"
+  "elab"
+  "builtin_initialize"
+] @keyword
+
+[
+  "noncomputable"
+  "partial"
+  "private"
+  "protected"
+  "unsafe"
+] @keyword
+
+[
+  "apply"
+  "exact"
+  "rewrite"
+  "rw"
+  "simp"
+  (trivial)
+] @keyword
+
+[
+  "catch"
+  "finally"
+  "try"
+] @exception
+
+((apply
+  name: (identifier) @exception)
+ (#match? @exception "throw"))
+
+[
+  "unless"
+  "mut"
+] @keyword
+
+[(true) (false)] @boolean
+
+(number) @constant.numeric.integer
+(float) @constant.numeric.float
+
+(comment) @comment
+(char) @character
+(string) @string
+(interpolated_string) @string
+; (escape_sequence) @string.escape
+
+; Reset highlighing in string interpolation
+(interpolation) @none
+
+(interpolation
+  "{" @punctuation.special
+  "}" @punctuation.special)
+
+["(" ")" "[" "]" "{" "}" "⟨" "⟩"] @punctuation.bracket
+
+["|" "," "." ":" ";"] @punctuation.delimiter
+
+(sorry) @error
+
+;; Error
+(ERROR) @error
+
+; Variables
+(identifier) @variable
diff --git a/runtime/queries/lean/injections.scm b/runtime/queries/lean/injections.scm
new file mode 100644
index 000000000..030714f1c
--- /dev/null
+++ b/runtime/queries/lean/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "markdown"))
diff --git a/runtime/queries/lean/locals.scm b/runtime/queries/lean/locals.scm
new file mode 100644
index 000000000..dd6c20363
--- /dev/null
+++ b/runtime/queries/lean/locals.scm
@@ -0,0 +1,5 @@
+[
+  (module)
+  (namespace)
+  (section)
+] @local.scope
diff --git a/runtime/queries/ledger/highlights.scm b/runtime/queries/ledger/highlights.scm
index bdf5f2dbb..02a9ea9a7 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/ledger/injections.scm b/runtime/queries/ledger/injections.scm
index 2d9481414..c1714786f 100644
--- a/runtime/queries/ledger/injections.scm
+++ b/runtime/queries/ledger/injections.scm
@@ -1,2 +1,2 @@
-(comment) @comment
-(note) @comment
+([(comment) (note)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/llvm-mir-yaml/highlights.scm b/runtime/queries/llvm-mir-yaml/highlights.scm
new file mode 100644
index 000000000..4ba254e82
--- /dev/null
+++ b/runtime/queries/llvm-mir-yaml/highlights.scm
@@ -0,0 +1 @@
+; inherits: yaml
diff --git a/runtime/queries/llvm-mir-yaml/indents.toml b/runtime/queries/llvm-mir-yaml/indents.toml
new file mode 100644
index 000000000..ddc3578b1
--- /dev/null
+++ b/runtime/queries/llvm-mir-yaml/indents.toml
@@ -0,0 +1,3 @@
+indent = [
+    "block_mapping_pair",
+]
diff --git a/runtime/queries/llvm-mir-yaml/injections.scm b/runtime/queries/llvm-mir-yaml/injections.scm
new file mode 100644
index 000000000..b3243022b
--- /dev/null
+++ b/runtime/queries/llvm-mir-yaml/injections.scm
@@ -0,0 +1,9 @@
+; inherits: yaml
+
+((document (block_node (block_scalar) @injection.content))
+ (#set! injection.language "llvm"))
+
+((document (block_node (block_mapping (block_mapping_pair
+  key: (flow_node (plain_scalar (string_scalar))) ; "body"
+  value: (block_node (block_scalar) @injection.content)))))
+  (#set! injection.language "mir"))
diff --git a/runtime/queries/llvm-mir/highlights.scm b/runtime/queries/llvm-mir/highlights.scm
new file mode 100644
index 000000000..792346122
--- /dev/null
+++ b/runtime/queries/llvm-mir/highlights.scm
@@ -0,0 +1,136 @@
+[
+  (label)
+  (bb_ref)
+] @label
+
+[
+  (comment)
+  (multiline_comment)
+] @comment
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "<"
+  ">"
+] @punctuation.bracket
+
+[
+  ","
+  ":"
+  "|"
+  "*"
+] @punctuation.delimiter
+
+[
+  "="
+  "x"
+] @operator
+
+[
+  "true"
+  "false"
+] @constant.builtin.boolean
+
+[
+  "null"
+  "_"
+  "unknown-address"
+] @constant.builtin
+
+[
+  (stack_object)
+  (constant_pool_index)
+  (jump_table_index)
+  (var)
+  (physical_register)
+  (ir_block)
+  (external_symbol)
+  (global_var)
+  (ir_local_var)
+  (metadata_ref)
+  (mnemonic)
+] @variable
+
+(low_level_type) @type
+
+[
+  (immediate_type)
+  (primitive_type)
+] @type.builtin
+
+(number) @constant.numeric.integer
+(float) @constant.numeric.float
+(string) @string
+
+(instruction name: _ @keyword.operator)
+
+[
+  "successors"
+  "liveins"
+  "pre-instr-symbol"
+  "post-instr-symbol"
+  "heap-alloc-marker"
+  "debug-instr-number"
+  "debug-location"
+  "mcsymbol"
+  "tied-def"
+  "target-flags"
+  "CustomRegMask"
+  "same_value"
+  "def_cfa_register"
+  "restore"
+  "undefined"
+  "offset"
+  "rel_offset"
+  "def_cfa"
+  "llvm_def_aspace_cfa"
+  "register"
+  "escape"
+  "remember_state"
+  "restore_state"
+  "window_save"
+  "negate_ra_sign_state"
+  "intpred"
+  "floatpred"
+  "shufflemask"
+  "liveout"
+  "target-index"
+  "blockaddress"
+  "intrinsic"
+  "load"
+  "store"
+  "unknown-size"
+  "on"
+  "from"
+  "into"
+  "align"
+  "basealign"
+  "addrspace"
+  "call-entry"
+  "custom"
+  "constant-pool"
+  "stack"
+  "got"
+  "jump-table"
+  "syncscope"
+  "address-taken"
+  "landing-pad"
+  "inlineasm-br-indirect-target"
+  "ehfunclet-entry"
+  "bbsections"
+
+  (intpred)
+  (floatpred)
+  (memory_operand_flag)
+  (atomic_ordering)
+  (register_flag)
+  (instruction_flag)
+  (float_keyword)
+] @keyword
+
+(ERROR) @error
diff --git a/runtime/queries/llvm-mir/indents.toml b/runtime/queries/llvm-mir/indents.toml
new file mode 100644
index 000000000..6a70e5adc
--- /dev/null
+++ b/runtime/queries/llvm-mir/indents.toml
@@ -0,0 +1,7 @@
+indent = [
+  "basic_block",
+]
+
+outdent = [
+  "label",
+]
diff --git a/runtime/queries/llvm-mir/injections.scm b/runtime/queries/llvm-mir/injections.scm
new file mode 100644
index 000000000..0b476f864
--- /dev/null
+++ b/runtime/queries/llvm-mir/injections.scm
@@ -0,0 +1,2 @@
+([ (comment) (multiline_comment)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/llvm-mir/textobjects.scm b/runtime/queries/llvm-mir/textobjects.scm
new file mode 100644
index 000000000..73f6f7728
--- /dev/null
+++ b/runtime/queries/llvm-mir/textobjects.scm
@@ -0,0 +1,3 @@
+(basic_block) @function.around
+
+(argument) @parameter.inside
diff --git a/runtime/queries/llvm/highlights.scm b/runtime/queries/llvm/highlights.scm
index 73afe85ed..cb705197e 100644
--- a/runtime/queries/llvm/highlights.scm
+++ b/runtime/queries/llvm/highlights.scm
@@ -1,14 +1,158 @@
 (type) @type
-(statement) @keyword.operator
+(type_keyword) @type.builtin
+
+(type [
+    (local_var)
+    (global_var)
+  ] @type)
+
+(argument) @variable.parameter
+
+(_ inst_name: _ @keyword.operator)
+
+[
+  "catch"
+  "filter"
+] @keyword.operator
+
+[
+  "to"
+  "nuw"
+  "nsw"
+  "exact"
+  "unwind"
+  "from"
+  "cleanup"
+  "swifterror"
+  "volatile"
+  "inbounds"
+  "inrange"
+  (icmp_cond)
+  (fcmp_cond)
+  (fast_math)
+] @keyword.control
+
+(_ callee: _ @function)
+(function_header name: _ @function)
+
+[
+  "declare"
+  "define"
+  (calling_conv)
+] @keyword.function
+
+[
+  "target"
+  "triple"
+  "datalayout"
+  "source_filename"
+  "addrspace"
+  "blockaddress"
+  "align"
+  "syncscope"
+  "within"
+  "uselistorder"
+  "uselistorder_bb"
+  "module"
+  "asm"
+  "sideeffect"
+  "alignstack"
+  "inteldialect"
+  "unwind"
+  "type"
+  "global"
+  "constant"
+  "externally_initialized"
+  "alias"
+  "ifunc"
+  "section"
+  "comdat"
+  "thread_local"
+  "localdynamic"
+  "initialexec"
+  "localexec"
+  "any"
+  "exactmatch"
+  "largest"
+  "nodeduplicate"
+  "samesize"
+  "distinct"
+  "attributes"
+  "vscale"
+  "no_cfi"
+  (linkage_aux)
+  (dso_local)
+  (visibility)
+  (dll_storage_class)
+  (unnamed_addr)
+  (attribute_name)
+] @keyword
+
+
+(function_header [
+    (linkage)
+    (calling_conv)
+    (unnamed_addr)
+  ] @keyword.function)
+
+[
+  (string)
+  (cstring)
+] @string
+
 (number) @constant.numeric.integer
 (comment) @comment
-(string) @string
 (label) @label
-(keyword) @keyword
-"ret" @keyword.control.return
-(boolean) @constant.builtin.boolean
+(_ inst_name: "ret" @keyword.control.return)
 (float) @constant.numeric.float
-(constant) @constant
-(identifier) @variable
-(symbol) @punctuation.delimiter
-(bracket) @punctuation.bracket
+
+[
+  (local_var)
+  (global_var)
+] @variable
+
+[
+  (struct_value)
+  (array_value)
+  (vector_value)
+] @constructor
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "<"
+  ">"
+  "<{"
+  "}>"
+] @punctuation.bracket
+
+[
+  ","
+  ":"
+] @punctuation.delimiter
+
+[
+  "="
+  "|"
+  "x"
+  "..."
+] @operator
+
+[
+  "true"
+  "false"
+] @constant.builtin.boolean
+
+[
+  "undef"
+  "poison"
+  "null"
+  "none"
+  "zeroinitializer"
+] @constant.builtin
+
+(ERROR) @error
diff --git a/runtime/queries/llvm/indents.toml b/runtime/queries/llvm/indents.toml
new file mode 100644
index 000000000..8cd603c8e
--- /dev/null
+++ b/runtime/queries/llvm/indents.toml
@@ -0,0 +1,8 @@
+indent = [
+ "function_body",
+ "instruction",
+]
+
+outdent = [
+ "}",
+]
diff --git a/runtime/queries/llvm/injections.scm b/runtime/queries/llvm/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/llvm/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/llvm/locals.scm b/runtime/queries/llvm/locals.scm
new file mode 100644
index 000000000..1946c2871
--- /dev/null
+++ b/runtime/queries/llvm/locals.scm
@@ -0,0 +1,14 @@
+; Scopes
+
+(function_body) @local.scope
+
+; Definitions
+
+(argument
+  (value (var (local_var) @local.definition)))
+
+(instruction
+  (local_var) @local.definition)
+
+; References
+(local_var) @local.reference
diff --git a/runtime/queries/llvm/textobjects.scm b/runtime/queries/llvm/textobjects.scm
new file mode 100644
index 000000000..3738a3bb9
--- /dev/null
+++ b/runtime/queries/llvm/textobjects.scm
@@ -0,0 +1,16 @@
+(define
+  body: (_) @function.inside) @function.around
+
+(struct_type
+  (struct_body) @class.inside) @class.around
+
+(packed_struct_type
+  (struct_body) @class.inside) @class.around
+
+(array_type
+  (array_vector_body) @class.inside) @class.around
+
+(vector_type
+  (array_vector_body) @class.inside) @class.around
+
+(argument) @parameter.inside
diff --git a/runtime/queries/lua/injections.scm b/runtime/queries/lua/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/lua/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/make/highlights.scm b/runtime/queries/make/highlights.scm
new file mode 100644
index 000000000..50380bafa
--- /dev/null
+++ b/runtime/queries/make/highlights.scm
@@ -0,0 +1,170 @@
+[
+ "("
+ ")"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ ":"
+ "&:"
+ "::"
+ "|"
+ ";"
+ "\""
+ "'"
+ ","
+] @punctuation.delimiter
+
+[
+ "$"
+ "$$"
+] @punctuation.special
+
+(automatic_variable
+ [ "@" "%" "<" "?" "^" "+" "/" "*" "D" "F"] @punctuation.special)
+
+(automatic_variable
+ "/" @error . ["D" "F"])
+
+[
+ "="
+ ":="
+ "::="
+ "?="
+ "+="
+ "!="
+ "@"
+ "-"
+ "+"
+] @operator
+
+[
+ (text)
+ (string)
+ (raw_text)
+] @string
+
+(variable_assignment (word) @string)
+
+[
+ "ifeq"
+ "ifneq"
+ "ifdef"
+ "ifndef"
+ "else"
+ "endif"
+ "if"
+ "or"  ; boolean functions are conditional in make grammar
+ "and"
+] @keyword.control.conditional
+
+"foreach" @keyword.control.repeat
+
+[
+ "define"
+ "endef"
+ "vpath"
+ "undefine"
+ "export"
+ "unexport"
+ "override"
+ "private"
+; "load"
+] @keyword
+
+[
+ "include"
+ "sinclude"
+ "-include"
+] @keyword.control.import
+
+[
+ "subst"
+ "patsubst"
+ "strip"
+ "findstring"
+ "filter"
+ "filter-out"
+ "sort"
+ "word"
+ "words"
+ "wordlist"
+ "firstword"
+ "lastword"
+ "dir"
+ "notdir"
+ "suffix"
+ "basename"
+ "addsuffix"
+ "addprefix"
+ "join"
+ "wildcard"
+ "realpath"
+ "abspath"
+ "call"
+ "eval"
+ "file"
+ "value"
+ "shell"
+] @keyword.function
+
+[
+ "error"
+ "warning"
+ "info"
+] @keyword.control.exception
+
+;; Variable
+(variable_assignment
+  name: (word) @variable)
+
+(variable_reference
+  (word) @variable)
+
+(comment) @comment
+
+((word) @clean @string.regexp
+ (#match? @clean "[%\*\?]"))
+
+(function_call
+  function: "error"
+  (arguments (text) @error))
+
+(function_call
+  function: "warning"
+  (arguments (text) @warning))
+
+(function_call
+  function: "info"
+  (arguments (text) @info))
+
+;; Install Command Categories
+;; Others special variables
+;; Variables Used by Implicit Rules
+[
+ "VPATH"
+ ".RECIPEPREFIX"
+] @constant.builtin
+
+(variable_assignment
+  name: (word) @clean @constant.builtin
+        (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS)$"))
+
+(variable_reference
+  (word) @clean @constant.builtin
+  (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS\.VARIABLES|\.FEATURES|\.INCLUDE_DIRS|\.LOADED)$"))
+
+;; Standart targets
+(targets
+  (word) @constant.macro
+  (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$"))
+
+(targets
+  (word) @constant.macro
+  (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$"))
+
+;; Builtin targets
+(targets
+  (word) @constant.macro
+  (#match? @constant.macro "^\.(PHONY|SUFFIXES|DEFAULT|PRECIOUS|INTERMEDIATE|SECONDARY|SECONDEXPANSION|DELETE_ON_ERROR|IGNORE|LOW_RESOLUTION_TIME|SILENT|EXPORT_ALL_VARIABLES|NOTPARALLEL|ONESHELL|POSIX)$"))
diff --git a/runtime/queries/make/injections.scm b/runtime/queries/make/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/make/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm
new file mode 100644
index 000000000..f12254e99
--- /dev/null
+++ b/runtime/queries/markdown/highlights.scm
@@ -0,0 +1,41 @@
+[
+  (atx_heading)
+  (setext_heading)
+] @markup.heading
+
+(code_fence_content) @none
+
+[
+  (indented_code_block)
+  (fenced_code_block)
+] @markup.raw.block
+
+(block_quote) @markup.quote
+
+(code_span) @markup.raw.inline
+
+(emphasis) @markup.italic
+
+(strong_emphasis) @markup.bold
+
+(link_destination) @markup.link.url
+(link_label) @markup.link.label
+
+[
+  (link_text)
+  (image_description)
+] @markup.link.text
+
+[
+  (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 000000000..10dcab0b9
--- /dev/null
+++ b/runtime/queries/markdown/injections.scm
@@ -0,0 +1,9 @@
+(fenced_code_block
+  (info_string) @injection.language
+  (code_fence_content) @injection.content
+  (#set! injection.include-children))
+
+((html_block) @injection.content
+ (#set! injection.language "html"))
+((html_tag) @injection.content
+ (#set! injection.language "html"))
diff --git a/runtime/queries/nix/highlights.scm b/runtime/queries/nix/highlights.scm
index 66719e876..f6682065e 100644
--- a/runtime/queries/nix/highlights.scm
+++ b/runtime/queries/nix/highlights.scm
@@ -13,7 +13,7 @@
 ] @keyword
 
 ((identifier) @variable.builtin
- (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$")
+ (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins)$")
  (#is-not? local))
 
 ((identifier) @function.builtin
@@ -33,6 +33,11 @@
 
 (uri) @string.special.uri
 
+; boolean
+((identifier) @constant.builtin.boolean (#match? @constant.builtin.boolean "^(true|false)$")) @constant.builtin.boolean
+; null
+((identifier) @constant.builtin (#eq? @constant.builtin "null")) @constant.builtin
+
 (integer) @constant.numeric.integer
 (float) @constant.numeric.float
 
diff --git a/runtime/queries/ocaml-interface/injections.scm b/runtime/queries/ocaml-interface/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/ocaml-interface/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm
index 15f46cc14..a08b12670 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/ocaml/indents.toml b/runtime/queries/ocaml/indents.toml
index 9b6462d82..7586b83a0 100644
--- a/runtime/queries/ocaml/indents.toml
+++ b/runtime/queries/ocaml/indents.toml
@@ -8,6 +8,6 @@ indent = [
     "match_case",
 ]
 
-oudent = [
+outdent = [
     "}",
 ]
diff --git a/runtime/queries/ocaml/injections.scm b/runtime/queries/ocaml/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/ocaml/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/perl/indents.toml b/runtime/queries/perl/indents.toml
new file mode 100644
index 000000000..365e06636
--- /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/perl/injections.scm b/runtime/queries/perl/injections.scm
new file mode 100644
index 000000000..cab5f53d5
--- /dev/null
+++ b/runtime/queries/perl/injections.scm
@@ -0,0 +1,2 @@
+((comments) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/php/highlights.scm b/runtime/queries/php/highlights.scm
index 46b5d26c2..5379fa1e3 100644
--- a/runtime/queries/php/highlights.scm
+++ b/runtime/queries/php/highlights.scm
@@ -5,7 +5,8 @@
 
 (primitive_type) @type.builtin
 (cast_type) @type.builtin
-(type_name (name) @type)
+(named_type (name) @type) @type
+(named_type (qualified_name) @type) @type
 
 ; Functions
 
@@ -85,10 +86,12 @@
 "endif" @keyword
 "endswitch" @keyword
 "endwhile" @keyword
+"enum" @keyword
 "extends" @keyword
 "final" @keyword
 "finally" @keyword
 "foreach" @keyword
+"fn" @keyword
 "function" @keyword
 "global" @keyword
 "if" @keyword
@@ -97,6 +100,7 @@
 "include" @keyword
 "insteadof" @keyword
 "interface" @keyword
+"match" @keyword
 "namespace" @keyword
 "new" @keyword
 "private" @keyword
diff --git a/runtime/queries/php/injections.scm b/runtime/queries/php/injections.scm
index 16d5736be..614a38509 100644
--- a/runtime/queries/php/injections.scm
+++ b/runtime/queries/php/injections.scm
@@ -1,3 +1,6 @@
 ((text) @injection.content
  (#set! injection.language "html")
  (#set! injection.combined))
+
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/php/textobjects.scm b/runtime/queries/php/textobjects.scm
new file mode 100644
index 000000000..04ffefd22
--- /dev/null
+++ b/runtime/queries/php/textobjects.scm
@@ -0,0 +1,30 @@
+(class_declaration
+  body: (_) @class.inside) @class.around
+
+(interface_declaration
+  body: (_) @class.inside) @class.around
+
+(trait_declaration
+  body: (_) @class.inside) @class.around
+
+(enum_declaration
+  body: (_) @class.inside) @class.around
+
+(function_definition
+  body: (_) @function.inside) @function.around
+
+(method_declaration
+  body: (_) @function.inside) @function.around
+
+(arrow_function 
+  body: (_) @function.inside) @function.around
+  
+(anonymous_function_creation_expression
+  body: (_) @function.inside) @function.around
+  
+(formal_parameters
+  [
+    (simple_parameter)
+    (variadic_parameter)
+    (property_promotion_parameter)
+  ] @parameter.inside)
diff --git a/runtime/queries/protobuf/injections.scm b/runtime/queries/protobuf/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/protobuf/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/python/injections.scm b/runtime/queries/python/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/python/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/regex/highlights.scm b/runtime/queries/regex/highlights.scm
new file mode 100644
index 000000000..9376caa92
--- /dev/null
+++ b/runtime/queries/regex/highlights.scm
@@ -0,0 +1,53 @@
+; upstream: https://github.com/tree-sitter/tree-sitter-regex/blob/e1cfca3c79896ff79842f057ea13e529b66af636/queries/highlights.scm
+
+[
+  "("
+  ")"
+  "(?"
+  "(?:"
+  "(?<"
+  ">"
+  "["
+  "]"
+  "{"
+  "}"
+] @punctuation.bracket
+
+[
+  "*"
+  "+"
+  "|"
+  "="
+  "<="
+  "!"
+  "<!"
+  "?"
+] @operator
+
+[
+  (identity_escape)
+  (control_letter_escape)
+  (character_class_escape)
+  (control_escape)
+  (start_assertion)
+  (end_assertion)
+  (boundary_assertion)
+  (non_boundary_assertion)
+] @constant.character.escape
+
+(group_name) @property
+
+(count_quantifier
+  [
+    (decimal_digits) @constant.numeric
+    "," @punctuation.delimiter
+  ])
+
+(character_class
+  [
+    "^" @operator
+    (class_range "-" @operator)
+  ])
+
+(class_character) @constant.character
+(pattern_character) @string
diff --git a/runtime/queries/rescript/highlights.scm b/runtime/queries/rescript/highlights.scm
new file mode 100644
index 000000000..b9ab8ea63
--- /dev/null
+++ b/runtime/queries/rescript/highlights.scm
@@ -0,0 +1,179 @@
+(comment) @comment
+
+; Identifiers
+;------------
+
+; Escaped identifiers like \"+."
+((value_identifier) @function.macro
+ (#match? @function.macro "^\\.*$"))
+
+[
+  (type_identifier)
+  (unit_type)
+  "list"
+] @type
+
+[
+  (variant_identifier)
+  (polyvar_identifier)
+] @constant
+
+(property_identifier) @variable.other.member
+(module_identifier) @namespace
+
+(jsx_identifier) @tag
+(jsx_attribute (property_identifier) @variable.parameter)
+
+; Parameters
+;----------------
+
+(list_pattern (value_identifier) @variable.parameter)
+(spread_pattern (value_identifier) @variable.parameter)
+
+; String literals
+;----------------
+
+[
+  (string)
+  (template_string)
+] @string
+
+(template_substitution
+  "${" @punctuation.bracket
+  "}" @punctuation.bracket) @embedded
+
+(character) @constant.character
+(escape_sequence) @constant.character.escape
+
+; Other literals
+;---------------
+
+[
+  (true)
+  (false)
+] @constant.builtin
+
+(number) @constant.numeric
+(polyvar) @constant
+(polyvar_string) @constant
+
+; Functions
+;----------
+
+[
+ (formal_parameters (value_identifier))
+ (positional_parameter (value_identifier))
+ (labeled_parameter (value_identifier))
+] @variable.parameter
+
+(function parameter: (value_identifier) @variable.parameter)
+
+; Meta
+;-----
+
+[
+ "@"
+ "@@"
+ (decorator_identifier)
+] @label
+
+(extension_identifier) @keyword
+("%") @keyword
+
+; Misc
+;-----
+
+(subscript_expression index: (string) @variable.other.member)
+(polyvar_type_pattern "#" @constant)
+
+[
+  ("include")
+  ("open")
+] @keyword
+
+[
+  "as"
+  "export"
+  "external"
+  "let"
+  "module"
+  "mutable"
+  "private"
+  "rec"
+  "type"
+  "and"
+] @keyword
+
+[
+  "if"
+  "else"
+  "switch"
+] @keyword
+
+[
+  "exception"
+  "try"
+  "catch"
+  "raise"
+] @keyword
+
+[
+  "."
+  ","
+  "|"
+] @punctuation.delimiter
+
+[
+  "++"
+  "+"
+  "+."
+  "-"
+  "-."
+  "*"
+  "*."
+  "/"
+  "/."
+  "<"
+  "<="
+  "=="
+  "==="
+  "!"
+  "!="
+  "!=="
+  ">"
+  ">="
+  "&&"
+  "||"
+  "="
+  ":="
+  "->"
+  "|>"
+  ":>"
+  (uncurry)
+] @operator
+
+[
+  "("
+  ")"
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket
+
+(polyvar_type
+  [
+   "["
+   "[>"
+   "[<"
+   "]"
+  ] @punctuation.bracket)
+
+[
+  "~"
+  "?"
+  "=>"
+  "..."
+] @punctuation
+
+(ternary_expression ["?" ":"] @operator)
diff --git a/runtime/queries/rescript/injections.scm b/runtime/queries/rescript/injections.scm
new file mode 100644
index 000000000..201cce757
--- /dev/null
+++ b/runtime/queries/rescript/injections.scm
@@ -0,0 +1,8 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
+((raw_js) @injection.content
+ (#set! injection.language "javascript"))
+
+((raw_gql) @injection.content
+ (#set! injection.language "graphql"))
diff --git a/runtime/queries/rescript/textobjects.scm b/runtime/queries/rescript/textobjects.scm
new file mode 100644
index 000000000..7ee8cd1a9
--- /dev/null
+++ b/runtime/queries/rescript/textobjects.scm
@@ -0,0 +1,9 @@
+; Classes (modules)
+;------------------
+
+(module_declaration definition: ((_) @class.inside)) @class.around
+
+; Functions
+;----------
+
+(function body: (_) @function.inside) @function.around
diff --git a/runtime/queries/ruby/indents.toml b/runtime/queries/ruby/indents.toml
new file mode 100644
index 000000000..b417751fc
--- /dev/null
+++ b/runtime/queries/ruby/indents.toml
@@ -0,0 +1,25 @@
+indent = [
+  "argument_list",
+  "array",
+  "begin",
+  "block",
+  "call",
+  "class",
+  "case",
+  "do_block",
+  "elsif",
+  "if",
+  "hash",
+  "method",
+  "module",
+  "singleton_class",
+  "singleton_method",
+]
+
+outdent = [
+  ")",
+  "}",
+  "]",
+  "end",
+  "when",
+]
diff --git a/runtime/queries/ruby/injections.scm b/runtime/queries/ruby/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/ruby/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm
index 539d95500..26496c66b 100644
--- a/runtime/queries/rust/highlights.scm
+++ b/runtime/queries/rust/highlights.scm
@@ -127,11 +127,16 @@
   "await"
 ] @keyword.control
 
+"use" @keyword.control.import
+(mod_item "mod" @keyword.control.import !body)
+(use_as_clause "as" @keyword.control.import)
+
+(type_cast_expression "as" @keyword.operator)
+
 [
   (crate)
   (super)
   "as"
-  "use"
   "pub"
   "mod"
   "extern"
@@ -242,10 +247,9 @@
 ; ---
 ; Macros
 ; ---
-
 (meta_item
-  (identifier) @attribute)
-(attribute_item) @attribute
+  (identifier) @function.macro)
+
 (inner_attribute_item) @attribute
 
 (macro_definition
@@ -259,7 +263,7 @@
   "!" @function.macro)
 
 (metavariable) @variable.parameter
-(fragment_specifier) @variable.parameter
+(fragment_specifier) @type
 
 
 
diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml
index 3900f0b91..51a0ceeaf 100644
--- a/runtime/queries/rust/indents.toml
+++ b/runtime/queries/rust/indents.toml
@@ -9,6 +9,7 @@ indent = [
  "field_initializer_list",
  "struct_pattern",
  "tuple_pattern",
+ "unit_expression",
  "enum_variant_list",
  "call_expression",
  "binary_expression",
diff --git a/runtime/queries/rust/injections.scm b/runtime/queries/rust/injections.scm
index 6035d4189..77c70805e 100644
--- a/runtime/queries/rust/injections.scm
+++ b/runtime/queries/rust/injections.scm
@@ -1,3 +1,6 @@
+([(line_comment) (block_comment)] @injection.content
+ (#set! injection.language "comment"))
+
 ((macro_invocation
   (token_tree) @injection.content)
  (#set! injection.language "rust")
@@ -7,3 +10,17 @@
   (token_tree) @injection.content)
  (#set! injection.language "rust")
  (#set! injection.include-children))
+
+(call_expression
+  function: (scoped_identifier
+    path: (identifier) @_regex (#eq? @_regex "Regex")
+    name: (identifier) @_new (#eq? @_new "new"))
+  arguments: (arguments (raw_string_literal) @injection.content)
+  (#set! injection.language "regex"))
+
+(call_expression
+  function: (scoped_identifier
+    path: (scoped_identifier (identifier) @_regex (#eq? @_regex "Regex") .)
+    name: (identifier) @_new (#eq? @_new "new"))
+  arguments: (arguments (raw_string_literal) @injection.content)
+  (#set! injection.language "regex"))
diff --git a/runtime/queries/scala/highlights.scm b/runtime/queries/scala/highlights.scm
new file mode 100644
index 000000000..50a6e18a6
--- /dev/null
+++ b/runtime/queries/scala/highlights.scm
@@ -0,0 +1,203 @@
+; CREDITS @stumash (stuart.mashaal@gmail.com)
+
+;; variables
+
+
+((identifier) @variable.builtin
+ (#match? @variable.builtin "^this$"))
+
+(interpolation) @none
+
+; Assume other uppercase names constants.
+; NOTE: In order to distinguish constants we highlight
+; all the identifiers that are uppercased. But this solution
+; is not suitable for all occurrences e.g. it will highlight
+; an uppercased method as a constant if used with no params.
+; Introducing highlighting for those specific cases, is probably
+; best way to resolve the issue.
+((identifier) @constant (#match? @constant "^[A-Z]"))
+
+;; types
+
+(type_identifier) @type
+
+(class_definition
+  name: (identifier) @type)
+
+(object_definition
+  name: (identifier) @type)
+
+(trait_definition
+  name: (identifier) @type)
+
+(type_definition
+  name: (type_identifier) @type)
+
+; method definition
+
+(class_definition
+  body: (template_body
+    (function_definition
+      name: (identifier) @function.method)))
+(object_definition
+  body: (template_body
+    (function_definition
+      name: (identifier) @function.method)))
+(trait_definition
+  body: (template_body
+    (function_definition
+      name: (identifier) @function.method)))
+
+; imports
+
+(import_declaration
+  path: (identifier) @namespace)
+((stable_identifier (identifier) @namespace))
+
+((import_declaration
+  path: (identifier) @type) (#match? @type "^[A-Z]"))
+((stable_identifier (identifier) @type) (#match? @type "^[A-Z]"))
+
+((import_selectors (identifier) @type) (#match? @type "^[A-Z]"))
+
+; method invocation
+
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (field_expression
+    field: (identifier) @function.method))
+
+((call_expression
+   function: (identifier) @variable.other.member)
+ (#match? @variable.other.member "^[A-Z]"))
+
+(generic_function
+  function: (identifier) @function)
+
+(
+  (identifier) @function.builtin
+  (#match? @function.builtin "^super$")
+)
+
+; function definitions
+
+(function_definition
+  name: (identifier) @function)
+
+(parameter
+  name: (identifier) @variable.parameter)
+
+; expressions
+
+
+(field_expression field: (identifier) @variable.other.member)
+(field_expression value: (identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(infix_expression operator: (identifier) @operator)
+(infix_expression operator: (operator_identifier) @operator)
+(infix_type operator: (operator_identifier) @operator)
+(infix_type operator: (operator_identifier) @operator)
+
+; literals
+(boolean_literal) @constant.builtin.boolean
+(integer_literal) @constant.numeric.integer
+(floating_point_literal) @constant.numeric.float
+
+
+(symbol_literal) @string.special.symbol
+ 
+[
+(string)
+(character_literal)
+(interpolated_string_expression)
+] @string
+
+(interpolation "$" @punctuation.special)
+
+;; keywords
+
+[
+  "abstract"
+  "case"
+  "class"
+  "extends"
+  "final"
+  "finally"
+;; `forSome` existential types not implemented yet
+  "implicit"
+  "lazy"
+;; `macro` not implemented yet
+  "object"
+  "override"
+  "package"
+  "private"
+  "protected"
+  "sealed"
+  "trait"
+  "type"
+  "val"
+  "var"
+  "with"
+] @keyword
+
+(null_literal) @constant.builtin
+(wildcard) @keyword
+
+;; special keywords
+
+"new" @keyword.operator
+
+[
+ "else"
+ "if"
+ "match"
+ "try"
+ "catch"
+ "throw"
+] @keyword.control.conditional
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "."
+ ","
+] @punctuation.delimiter
+
+[
+ "do"
+ "for"
+ "while"
+ "yield"
+] @keyword.control.repeat
+
+"def" @keyword.function
+
+[
+  "=>"
+  "<-"
+  "@"
+] @keyword.operator
+
+"import" @keyword.control.import
+
+"return" @keyword.control.return
+
+(comment) @comment
+
+;; `case` is a conditional keyword in case_block
+
+(case_block
+  (case_clause ("case") @keyword.control.conditional))
+
+(identifier) @variable
\ No newline at end of file
diff --git a/runtime/queries/scala/indents.toml b/runtime/queries/scala/indents.toml
new file mode 100644
index 000000000..6de548442
--- /dev/null
+++ b/runtime/queries/scala/indents.toml
@@ -0,0 +1,23 @@
+
+indent = [
+ "block",
+ "arguments",
+ "parameter",
+ "class_definition",
+ "trait_definition",
+ "object_definition",
+ "function_definition",
+ "val_definition",
+ "import_declaration",
+ "while_expression",
+ "do_while_expression",
+ "for_expression",
+ "try_expression",
+ "match_expression"
+]
+
+outdent = [
+ "}",
+ "]",
+ ")"
+]
diff --git a/runtime/queries/scala/injections.scm b/runtime/queries/scala/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/scala/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm
index 4fcdfd669..22b0c551e 100644
--- a/runtime/queries/svelte/highlights.scm
+++ b/runtime/queries/svelte/highlights.scm
@@ -20,12 +20,12 @@
 ((element (start_tag (tag_name) @_tag) (text) @markup.inline)
  (#match? @_tag "^(code|kbd)$"))
 
-((element (start_tag (tag_name) @_tag) (text) @markup.underline.link)
+((element (start_tag (tag_name) @_tag) (text) @markup.link.url)
  (#eq? @_tag "a"))
 
 ((attribute
    (attribute_name) @_attr
-   (quoted_attribute_value (attribute_value) @markup.undeline.link))
+   (quoted_attribute_value (attribute_value) @markup.link.url))
  (#match? @_attr "^(href|src)$"))
 
 (tag_name) @tag
diff --git a/runtime/queries/svelte/injections.scm b/runtime/queries/svelte/injections.scm
index 266f47016..04e860cf0 100644
--- a/runtime/queries/svelte/injections.scm
+++ b/runtime/queries/svelte/injections.scm
@@ -26,5 +26,5 @@
   (#set! injection.language "typescript")
 )
 
-(comment) @comment
-
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tablegen/highlights.scm b/runtime/queries/tablegen/highlights.scm
new file mode 100644
index 000000000..8ade5ba9a
--- /dev/null
+++ b/runtime/queries/tablegen/highlights.scm
@@ -0,0 +1,90 @@
+[
+  (comment)
+  (multiline_comment)
+] @comment
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "<"
+  ">"
+] @punctuation.bracket
+
+[
+  ","
+  ";"
+  "."
+] @punctuation.delimiter
+
+[
+  "#"
+  "-"
+  "..."
+  ":"
+] @operator
+
+[
+  "="
+  "!cond"
+  (operator_keyword)
+] @function
+
+[
+  "true"
+  "false"
+] @constant.builtin.boolean
+
+[
+  "?"
+] @constant.builtin
+
+(var) @variable
+
+(template_arg (identifier) @variable.parameter)
+
+(_ argument: (value (identifier) @variable.parameter))
+
+(type) @type
+
+"code" @type.builtin
+
+(number) @constant.numeric.integer
+[
+  (string_string)
+  (code_string)
+] @string
+
+(preprocessor) @keyword.directive
+
+[
+  "class"
+  "field"
+  "let"
+  "defvar"
+  "def"
+  "defset"
+  "defvar"
+  "assert"
+] @keyword
+
+[
+  "let"
+  "in"
+  "foreach"
+  "if"
+  "then"
+  "else"
+] @keyword.operator
+
+"include" @keyword.control.import
+
+[
+  "multiclass"
+  "defm"
+] @namespace
+
+(ERROR) @error
diff --git a/runtime/queries/tablegen/indents.toml b/runtime/queries/tablegen/indents.toml
new file mode 100644
index 000000000..43532f4d4
--- /dev/null
+++ b/runtime/queries/tablegen/indents.toml
@@ -0,0 +1,7 @@
+indent = [
+  "statement",
+]
+
+outdent = [
+  "}",
+]
diff --git a/runtime/queries/tablegen/injections.scm b/runtime/queries/tablegen/injections.scm
new file mode 100644
index 000000000..0b476f864
--- /dev/null
+++ b/runtime/queries/tablegen/injections.scm
@@ -0,0 +1,2 @@
+([ (comment) (multiline_comment)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tablegen/textobjects.scm b/runtime/queries/tablegen/textobjects.scm
new file mode 100644
index 000000000..2cb802688
--- /dev/null
+++ b/runtime/queries/tablegen/textobjects.scm
@@ -0,0 +1,7 @@
+(class
+  body: (_) @class.inside) @class.around
+
+(multiclass
+  body: (_) @class.inside) @class.around
+
+(_ argument: _ @parameter.inside)
diff --git a/runtime/queries/toml/injections.scm b/runtime/queries/toml/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/toml/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tsq/injections.scm b/runtime/queries/tsq/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/tsq/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tsx/injections.scm b/runtime/queries/tsx/injections.scm
new file mode 100644
index 000000000..1b61e36da
--- /dev/null
+++ b/runtime/queries/tsx/injections.scm
@@ -0,0 +1 @@
+; inherits: typescript
diff --git a/runtime/queries/twig/highlights.scm b/runtime/queries/twig/highlights.scm
new file mode 100644
index 000000000..2c95ab637
--- /dev/null
+++ b/runtime/queries/twig/highlights.scm
@@ -0,0 +1,16 @@
+(comment_directive) @comment
+
+[
+  "{%"
+  "{%-"
+  "{%~"
+  "%}"
+  "-%}"
+  "~%}"
+  "{{"
+  "{{-"
+  "{{~"
+  "}}"
+  "-}}"
+  "~}}"
+] @keyword
diff --git a/runtime/queries/twig/injections.scm b/runtime/queries/twig/injections.scm
new file mode 100644
index 000000000..f08227342
--- /dev/null
+++ b/runtime/queries/twig/injections.scm
@@ -0,0 +1,3 @@
+((content) @injection.content
+ (#set! injection.language "html")
+ (#set! injection.combined))
diff --git a/runtime/queries/typescript/injections.scm b/runtime/queries/typescript/injections.scm
new file mode 100644
index 000000000..ff0ddfacf
--- /dev/null
+++ b/runtime/queries/typescript/injections.scm
@@ -0,0 +1 @@
+; inherits: javascript
diff --git a/runtime/queries/vue/injections.scm b/runtime/queries/vue/injections.scm
index 8ee34ffbd..73df868b6 100644
--- a/runtime/queries/vue/injections.scm
+++ b/runtime/queries/vue/injections.scm
@@ -15,3 +15,6 @@
 ((style_element
   (raw_text) @injection.content)
  (#set! injection.language "css"))
+
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/wgsl/injections.scm b/runtime/queries/wgsl/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/wgsl/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm
index a7efb5e71..e4fed27a9 100644
--- a/runtime/queries/yaml/highlights.scm
+++ b/runtime/queries/yaml/highlights.scm
@@ -1,9 +1,19 @@
-(block_mapping_pair key: (_) @variable.other.member)
-(flow_mapping (_ key: (_) @variable.other.member))
+(block_mapping_pair
+  key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member))
+(block_mapping_pair
+  key: (flow_node (plain_scalar (string_scalar) @variable.other.member)))
+
+(flow_mapping
+  (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member)))
+(flow_mapping
+  (_ key: (flow_node (plain_scalar (string_scalar) @variable.other.member))))
+
 (boolean_scalar) @constant.builtin.boolean
 (null_scalar) @constant.builtin
 (double_quote_scalar) @string
 (single_quote_scalar) @string
+(block_scalar) @string
+(string_scalar) @string
 (escape_sequence) @constant.character.escape
 (integer_scalar) @constant.numeric.integer
 (float_scalar) @constant.numeric.float
@@ -30,4 +40,4 @@
 "}"
 ] @punctuation.bracket
 
-["*" "&"] @punctuation.special
+["*" "&" "---" "..."] @punctuation.special
diff --git a/runtime/queries/yaml/injections.scm b/runtime/queries/yaml/injections.scm
new file mode 100644
index 000000000..321c90add
--- /dev/null
+++ b/runtime/queries/yaml/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/zig/highlights.scm b/runtime/queries/zig/highlights.scm
index 34dbeacd0..62c99acc1 100644
--- a/runtime/queries/zig/highlights.scm
+++ b/runtime/queries/zig/highlights.scm
@@ -144,7 +144,6 @@ field_constant: (IDENTIFIER) @constant
 
 ; VarDecl
 [
-  "comptime"
   "threadlocal"
   "fn"
 ] @keyword.function
@@ -178,6 +177,7 @@ field_constant: (IDENTIFIER) @constant
 
 ; PrecProc
 [
+  "comptime"
   "inline"
   "noinline"
   "asm"
@@ -195,15 +195,14 @@ field_constant: (IDENTIFIER) @constant
   (BitwiseOp)
   (BitShiftOp)
   (AdditionOp)
+  (AssignOp)
   (MultiplyOp)
   (PrefixOp)
   "*"
   "**"
   "->"
-  "=>"
   ".?"
   ".*"
-  "="
   "?"
 ] @operator
 
diff --git a/runtime/queries/zig/indents.toml b/runtime/queries/zig/indents.toml
index 88f88e16b..36ba8e558 100644
--- a/runtime/queries/zig/indents.toml
+++ b/runtime/queries/zig/indents.toml
@@ -3,6 +3,9 @@ indent = [
  "BlockExpr",
  "ContainerDecl",
  "SwitchExpr",
+ "AssignExpr",
+ "ErrorUnionExpr",
+ "Statement",
  "InitList"
 ]
 
diff --git a/runtime/queries/zig/injections.scm b/runtime/queries/zig/injections.scm
new file mode 100644
index 000000000..3df958971
--- /dev/null
+++ b/runtime/queries/zig/injections.scm
@@ -0,0 +1,2 @@
+([(line_comment) (doc_comment)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml
index d65995c05..d19863e03 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,28 @@
 "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"
+"markup.heading" = "base0D"
+"markup.list" = "base08"
+"markup.bold" = { fg = "base0A", modifiers = ["bold"] }
+"markup.italic" = { fg = "base0E", modifiers = ["italic"] }
+"markup.link.url" = { fg = "base09", modifiers = ["underlined"] }
+"markup.link.text" = "base08"
+"markup.quote" = "base0C"
+"markup.raw" = "base0B"
+
+"diff.plus" = "base0B"
+"diff.delta" = "base09"
+"diff.minus" = "base08"
+
+"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 000000000..483e87cc5
--- /dev/null
+++ b/runtime/themes/base16_default_light.toml
@@ -0,0 +1,73 @@
+# 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" }
+
+"markup.heading" = "base0D"
+"markup.list" = "base08"
+"markup.bold" = { fg = "base0A", modifiers = ["bold"] }
+"markup.italic" = { fg = "base0E", modifiers = ["italic"] }
+"markup.link.url" = { fg = "base09", modifiers = ["underlined"] }
+"markup.link.text" = "base08"
+"markup.quote" = "base0C"
+"markup.raw" = "base0B"
+
+"diff.plus" = "base0B"
+"diff.delta" = "base09"
+"diff.minus" = "base08"
+
+"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 000000000..23240d8dd
--- /dev/null
+++ b/runtime/themes/base16_terminal.toml
@@ -0,0 +1,52 @@
+# 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" = { fg = "light-white", 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" }
+
+"markup.heading" = "light-blue"
+"markup.list" = "light-red"
+"markup.bold" = { fg = "light-yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "light-magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "light-red"
+"markup.quote" = "light-cyan"
+"markup.raw" = "light-green"
+
+"diff.plus" = "light-green"
+"diff.delta" = "yellow"
+"diff.minus" = "light-red"
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "black" }
+"info" = "light-blue"
+"hint" = "gray"
+"debug" = "gray"
+"warning" = "yellow"
+"error" = "light-red"
diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml
index 86a6c34bf..32b58d0ac 100644
--- a/runtime/themes/bogster.toml
+++ b/runtime/themes/bogster.toml
@@ -28,6 +28,20 @@
 
 "module" = "#d32c5d"
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "#59dcb7"
+"diff.delta" = "#dc7759"
+"diff.minus" = "#dc597f"
+
 "ui.background" = { bg = "#161c23" }
 "ui.linenr" = { fg = "#415367" }
 "ui.linenr.selected" = { fg = "#e5ded6" }  # TODO
@@ -49,3 +63,6 @@
 "error" = "#dc597f"
 "info" = "#59dcb7"
 "hint" = "#59c0dc"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml
index 0554f827f..ab7c16ecc 100644
--- a/runtime/themes/dark_plus.toml
+++ b/runtime/themes/dark_plus.toml
@@ -39,6 +39,20 @@
 "constant.numeric" = { fg = "pale_green" }
 "constant.character.escape" = { fg = "gold" }
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "pale_green" }
+"diff.delta" = { fg = "gold" }
+"diff.minus" = { fg = "red" }
+
 "ui.background" = { fg = "light_gray", bg = "dark_gray2" }
 
 "ui.window" = { bg = "widget" }
diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml
new file mode 100644
index 000000000..1db25d8f4
--- /dev/null
+++ b/runtime/themes/dracula.toml
@@ -0,0 +1,63 @@
+# Author : Sebastian Zivota <loewenheim@mailbox.org>
+"comment" = { fg = "comment" }
+"constant" = { fg = "purple" }
+"constant.character.escape" = { fg = "pink" }
+"function" = { fg = "green" }
+"keyword" = { fg = "pink" }
+"operator" = { fg = "pink" }
+"punctuation" = { fg = "foreground" }
+"string" = { fg = "yellow" }
+"string.regexp" = { fg = "red" }
+"tag" = { fg = "pink" }
+"type" = { fg = "cyan", modifiers = ["italic"] }
+"type.enum.variant" = { fg = "foreground", modifiers = ["italic"] }
+"variable" = { fg = "foreground" }
+"variable.builtin" = { fg = "cyan", modifiers = ["italic"] }
+"variable.parameter" = { fg ="orange", modifiers = ["italic"] }
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
+"ui.background" = { fg = "foreground", bg = "background" }
+"ui.cursor" =  { fg = "background", bg = "orange", modifiers = ["dim"] }
+"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] }
+"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] }
+"ui.help" = { fg = "foreground", bg = "background_dark" }
+"ui.linenr" = { fg = "comment" }
+"ui.linenr.selected" = { fg = "foreground" }
+"ui.menu" = { fg = "foreground", bg = "background_dark" }
+"ui.menu.selected" = { fg = "cyan", bg = "background_dark" }
+"ui.popup" = { fg = "foreground", bg = "background_dark" }
+"ui.selection" = { fg = "background", bg = "purple", modifiers = ["dim"] }
+"ui.selection.primary" = { fg = "background", bg = "pink" }
+"ui.statusline" = { fg = "foreground", bg = "background_dark" }
+"ui.statusline.inactive" = { fg = "comment", bg = "background_dark" }
+"ui.text" = { fg = "foreground" }
+"ui.text.focus" = { fg = "cyan" }
+"ui.window" = { fg = "foreground" }
+
+"error" = { fg = "red" }
+"warning" = { fg = "cyan" }
+
+"markup.heading" = { fg = "purple", modifiers = ["bold"] }
+"markup.list" = "cyan"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
+"markup.link.url" = "cyan"
+"markup.link.text" = "pink"
+"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
+"markup.raw" = { fg = "foreground" }
+
+[palette]
+background = "#282a36"
+background_dark = "#21222c"
+foreground = "#f8f8f2"
+comment = "#6272a4"
+red = "#ff5555"
+orange = "#ffb86c"
+yellow = "#f1fa8c"
+green = "#50fa7b"
+purple = "#bd93f9"
+cyan = "#8be9fd"
+pink = "#ff79c6"
diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml
index bbd005e6a..a6389da2c 100644
--- a/runtime/themes/everforest_dark.toml
+++ b/runtime/themes/everforest_dark.toml
@@ -12,7 +12,7 @@
 "type" = "yellow"
 "constant" = "purple"
 "constant.numeric" = "purple"
-"string" = "grey2"
+"string" = "green"
 "comment" = "grey0"
 "variable" = "fg"
 "variable.builtin" = "blue"
@@ -34,6 +34,20 @@
 "module" = "blue"
 "special" = "orange"
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
 "ui.background" = { bg = "bg0" }
 "ui.cursor" = { fg = "bg0", bg = "fg" }
 "ui.cursor.match" = { fg = "orange", bg = "bg_yellow" }
diff --git a/runtime/themes/everforest_light.toml b/runtime/themes/everforest_light.toml
new file mode 100644
index 000000000..5490adb3c
--- /dev/null
+++ b/runtime/themes/everforest_light.toml
@@ -0,0 +1,100 @@
+# Everforest (Dark Hard)
+# Author: CptPotato
+
+# Original Author:
+# URL: https://github.com/sainnhe/everforest
+# Filename: autoload/everforest.vim
+# Author: sainnhe
+# Email: sainnhe@gmail.com
+# License: MIT License
+
+"constant.character.escape" = "orange"
+"type" = "yellow"
+"constant" = "purple"
+"constant.numeric" = "purple"
+"string" = "green"
+"comment" = "grey0"
+"variable" = "fg"
+"variable.builtin" = "blue"
+"variable.parameter" = "fg"
+"variable.other.member" = "fg"
+"label" = "aqua"
+"punctuation" = "grey2"
+"punctuation.delimiter" = "grey2"
+"punctuation.bracket" = "fg"
+"keyword" = "red"
+"operator" = "orange"
+"function" = "green"
+"function.builtin" = "blue"
+"function.macro" = "aqua"
+"tag" = "yellow"
+"namespace" = "aqua"
+"attribute" = "aqua"
+"constructor" = "yellow"
+"module" = "blue"
+"special" = "orange"
+
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+"ui.background" = { bg = "bg0" }
+"ui.cursor" = { fg = "bg0", bg = "fg" }
+"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" }
+"ui.cursor.insert" = { fg = "bg0", bg = "grey1" }
+"ui.cursor.select" = { fg = "bg0", bg = "blue" }
+"ui.linenr" = "grey0"
+"ui.linenr.selected" = "fg"
+"ui.statusline" = { fg = "grey2", bg = "bg2" }
+"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" }
+"ui.popup" = { fg = "grey2", bg = "bg1" }
+"ui.window" = { fg = "grey2", bg = "bg1" }
+"ui.help" = { fg = "fg", bg = "bg1" }
+"ui.text" = "fg"
+"ui.text.focus" = "fg"
+"ui.menu" = { fg = "fg", bg = "bg2" }
+"ui.menu.selected" = { fg = "bg0", bg = "green" }
+"ui.selection" = { bg = "bg3" }
+
+"hint" = "blue"
+"info" = "aqua"
+"warning" = "yellow"
+"error" = "red"
+"diagnostic" = { modifiers = ["underlined"] }
+
+
+[palette]
+
+bg0 = "#fff9e8"
+bg1 = "#f7f4e0"
+bg2 = "#f0eed9"
+bg3 = "#e9e8d2"
+bg4 = "#e1ddcb"
+bg5 = "#bec5b2"
+bg_visual = "#edf0cd"
+bg_red = "#fce5dc"
+bg_green = "#f1f3d4"
+bg_blue = "#eaf2eb"
+bg_yellow = "#fbefd0"
+
+fg = "#5c6a72"
+red = "#f85552"
+orange = "#f57d26"
+yellow = "#dfa000"
+green = "#8da101"
+aqua = "#35a77c"
+blue = "#3a94c5"
+purple = "#df69ba"
+grey0 = "#a6b0a0"
+grey1 = "#939f91"
+grey2 = "#829181"
diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml
index 0ff039eab..a976a9bdc 100644
--- a/runtime/themes/gruvbox.toml
+++ b/runtime/themes/gruvbox.toml
@@ -19,6 +19,7 @@
 "function" = { fg = "green1", modifiers = ["bold"] }
 "function.macro" = "aqua1"
 "function.builtin" = "yellow1"
+"tag" = "red1"
 "comment" = { fg = "gray1", modifiers = ["italic"]  }
 "constant" = { fg = "purple1" }
 "constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
@@ -28,6 +29,10 @@
 "label" = "aqua1"
 "module" = "aqua1"
 
+"diff.plus" = "green1"
+"diff.delta" = "orange1"
+"diff.minus" = "red1"
+
 "warning" = { fg = "orange1", bg = "bg1" }
 "error" = { fg = "red1", bg = "bg1" }
 "info" = { fg = "aqua1", bg = "bg1" }
@@ -51,6 +56,13 @@
 
 "diagnostic" = { modifiers = ["underlined"] }
 
+"markup.heading" = "aqua1"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.link.url" = { fg = "green1", modifiers = ["underlined"] }
+"markup.link.text" = "red1"
+"markup.raw" = "red1"
+
 [palette]
 bg0 = "#282828" # main background
 bg1 = "#3c3836"
diff --git a/runtime/themes/gruvbox_light.toml b/runtime/themes/gruvbox_light.toml
new file mode 100644
index 000000000..81ea7fd13
--- /dev/null
+++ b/runtime/themes/gruvbox_light.toml
@@ -0,0 +1,96 @@
+# Author : Rohan Jain <crodjer@pm.me> 
+# Author : Jakub Bartodziej <kubabartodziej@gmail.com>
+# The theme uses the gruvbox light palette with standard contrast: github.com/morhetz/gruvbox
+
+"attribute" = "aqua1"
+"keyword" = { fg = "red1" }
+"keyword.directive" = "red0"
+"namespace" = "aqua1"
+"punctuation" = "orange1"
+"punctuation.delimiter" = "orange1"
+"operator" = "purple1"
+"special" = "purple0"
+"variable.other.member" = "blue1"
+"variable" = "fg1"
+"variable.builtin" = "orange1"
+"variable.parameter" = "fg2"
+"type" = "yellow1"
+"type.builtin" = "yellow1"
+"constructor" = { fg = "purple1", modifiers = ["bold"] }
+"function" = { fg = "green1", modifiers = ["bold"] }
+"function.macro" = "aqua1"
+"function.builtin" = "yellow1"
+"tag" = "red1"
+"comment" = { fg = "gray1", modifiers = ["italic"]  }
+"constant" = { fg = "purple1" }
+"constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
+"string" = "green1"
+"constant.numeric" = "purple1"
+"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] }
+"label" = "aqua1"
+"module" = "aqua1"
+
+"diff.plus" = "green1"
+"diff.delta" = "orange1"
+"diff.minus" = "red1"
+
+"warning" = { fg = "orange1", bg = "bg1" }
+"error" = { fg = "red1", bg = "bg1" }
+"info" = { fg = "aqua1", bg = "bg1" }
+"hint" = { fg = "blue1", bg = "bg1" }
+
+"ui.background" = { bg = "bg0" }
+"ui.linenr" = { fg = "bg4" }
+"ui.linenr.selected" = { fg = "yellow1" }
+"ui.statusline" = { fg = "fg1", bg = "bg2" }
+"ui.statusline.inactive" = { fg = "fg4", bg = "bg1" }
+"ui.popup" = { bg = "bg1" }
+"ui.window" = { bg = "bg1" }
+"ui.help" = { bg = "bg1", fg = "fg1" }
+"ui.text" = { fg = "fg1" }
+"ui.text.focus" = { fg = "fg1" }
+"ui.selection" = { bg = "bg3", modifiers = ["reversed"] }
+"ui.cursor.primary" = { modifiers = ["reversed"] }
+"ui.cursor.match" = { modifiers = ["reversed"] }
+"ui.menu" = { fg = "fg1", bg = "bg2" }
+"ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] }
+
+"diagnostic" = { modifiers = ["underlined"] }
+
+"markup.heading" = "aqua1"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.link.url" = { fg = "green1", modifiers = ["underlined"] }
+"markup.link.text" = "red1"
+"markup.raw" = "red1"
+
+[palette]
+bg0 = "#fbf1c7" # main background
+bg1 = "#ebdbb2"
+bg2 = "#d5c4a1"
+bg3 = "#bdae93"
+bg4 = "#a89984"
+
+fg0 = "#282828" # main foreground
+fg1 = "#3c3836"
+fg2 = "#504945"
+fg3 = "#665c54"
+fg4 = "#7c6f64" # gray0
+
+gray0 = "#7c6f64"
+gray1 = "#928374"
+
+red0 = "#cc241d" # neutral
+red1 = "#9d0006" # bright
+green0 = "#98971a"
+green1 = "#79740e"
+yellow0 = "#d79921"
+yellow1 = "#b57614"
+blue0 = "#458588"
+blue1 = "#076678"
+purple0 = "#b16286"
+purple1 = "#8f3f71"
+aqua0 = "#689d6a"
+aqua1 = "#427b58"
+orange0 = "#d65d0e"
+orange1 = "#af3a03"
diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml
index 308294759..a7c33e2df 100644
--- a/runtime/themes/ingrid.toml
+++ b/runtime/themes/ingrid.toml
@@ -28,6 +28,20 @@
 
 "module" = "#839A53"
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "#839A53"
+"diff.delta" = "#D4A520"
+"diff.minus" = "#D74E50"
+
 "ui.background" = { bg = "#FFFCFD" }
 "ui.linenr" = { fg = "#bbbbbb" }
 "ui.linenr.selected" = { fg = "#F3EAE9" }  # TODO
diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml
index 38f9f1707..e6ff0a5e3 100644
--- a/runtime/themes/monokai.toml
+++ b/runtime/themes/monokai.toml
@@ -39,6 +39,20 @@
 "constant.numeric" = { fg = "#ae81ff" }
 "constant.character.escape" = { fg = "#ae81ff" }
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "#a6e22e" }
+"diff.delta" = { fg = "#fd971f" }
+"diff.minus" = { fg = "#f92672" }
+
 "ui.background" = { fg = "text", bg = "background" }
 
 "ui.window" = { bg = "widget" }
@@ -65,7 +79,7 @@
 "warning" = { fg = "#cca700" }
 "error" = { fg = "#f48771" }
 "info" = { fg = "#75beff" }
-"hint" = { fg = "#eeeeeeb3" }
+"hint" = { fg = "#eeeeeb3" }
 
 diagnostic = { modifiers = ["underlined"] }
 
diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml
new file mode 100644
index 000000000..8de9994c8
--- /dev/null
+++ b/runtime/themes/monokai_pro.toml
@@ -0,0 +1,115 @@
+# 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"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[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 000000000..c5890042a
--- /dev/null
+++ b/runtime/themes/monokai_pro_machine.toml
@@ -0,0 +1,115 @@
+# 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"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[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 000000000..d9badf3ca
--- /dev/null
+++ b/runtime/themes/monokai_pro_octagon.toml
@@ -0,0 +1,115 @@
+# 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"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[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 000000000..ed7ebeaee
--- /dev/null
+++ b/runtime/themes/monokai_pro_ristretto.toml
@@ -0,0 +1,115 @@
+# 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"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[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 000000000..da06e597c
--- /dev/null
+++ b/runtime/themes/monokai_pro_spectrum.toml
@@ -0,0 +1,115 @@
+# 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"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[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/nord.toml b/runtime/themes/nord.toml
index a619f902e..deb904520 100644
--- a/runtime/themes/nord.toml
+++ b/runtime/themes/nord.toml
@@ -84,6 +84,21 @@
 # nord15 - integer, floating point
 "constant.numeric" = "nord15"
 
+# TODO markup
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+# vcs
+"diff.plus" = "nord14"
+"diff.delta" = "nord12"
+"diff.minus" = "nord11"
+
 [palette]
 nord0 = "#2e3440"
 nord1 = "#3b4252"
diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml
index 40ed1abe4..acdaf99c8 100644
--- a/runtime/themes/onedark.toml
+++ b/runtime/themes/onedark.toml
@@ -3,27 +3,42 @@
 "attribute" = { fg = "yellow" }
 "comment" = { fg = "light-gray", modifiers = ["italic"] }
 "constant" = { fg = "cyan" }
-"constant.builtin" = { fg = "blue" }
+"constant.numeric" = { fg = "gold" }
+"constant.builtin" = { fg = "gold" }
+"constant.character.escape" = { fg = "gold" }
 "constructor" = { fg = "blue" }
-"escape" = { fg = "gold" }
 "function" = { fg = "blue" }
 "function.builtin" = { fg = "blue" }
 "function.macro" = { fg = "purple" }
 "keyword" = { fg = "red" }
 "keyword.control" = { fg = "purple" }
+"keyword.control.import" = { fg = "red" }
 "keyword.directive" = { fg = "purple" }
 "label" = { fg = "purple" }
 "namespace" = { fg = "blue" }
-"number" = { fg = "gold" }
 "operator" = { fg = "purple" }
+"keyword.operator" = { fg = "purple" }
 "property" = { fg = "red" }
 "special" = { fg = "blue" }
 "string" = { fg = "green" }
 "type" = { fg = "yellow" }
-"type.builtin" = { fg = "yellow" }
 # "variable" = { fg = "blue" }
 "variable.builtin" = { fg = "blue" }
 "variable.parameter" = { fg = "red" }
+"variable.other.member" = { fg = "red" }
+
+"markup.heading" = { fg = "red" }
+"markup.raw.inline" = { fg = "green" }
+"markup.bold" = { fg = "gold", modifiers = ["bold"] }
+"markup.italic" = { fg = "purple", modifiers = ["italic"] }
+"markup.list" = { fg = "red" }
+"markup.quote" = { fg = "yellow" }
+"markup.link.url" = { fg = "cyan", modifiers = ["underlined"]}
+"markup.link.text" = { fg = "purple" }
+
+"diff.plus" = "green"
+"diff.delta" = "gold"
+"diff.minus" = "red"
 
 diagnostic = { modifiers = ["underlined"] }
 "info" = { fg = "blue", modifiers = ["bold"] }
diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml
index 537770084..66717bb28 100644
--- a/runtime/themes/rose_pine.toml
+++ b/runtime/themes/rose_pine.toml
@@ -1,15 +1,15 @@
 # Author: RayGervais<raygervais@hotmail.ca>
+# Author: ChrisHa<chunghha@users.noreply.github.com>
 
 "ui.background" = { bg = "base" }
-"ui.menu" = "surface"
+"ui.menu" = { fg = "text", bg = "overlay" }
 "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" }
@@ -32,10 +32,13 @@
 "keyword" = "pine"
 "label" = "iris"
 "namespace" = "pine"
-"ui.popup" = { bg = "overlay" }
+"ui.popup" = { bg = "surface" }
 "ui.window" = { bg = "base" }
 "ui.help" = { bg = "overlay", fg = "foam" }
 "text" = "text"
+"diff.plus" = "foam"
+"diff.delta" = "rose"
+"diff.minus" = "love"
 
 "info" = "gold"
 "hint" = "gold"
@@ -43,6 +46,15 @@
 "diagnostic" = "rose"
 "error" = "love"
 
+"markup.heading" = { fg = "rose" }
+"markup.raw.inline" = { fg = "foam" }
+"markup.bold" = { fg = "gold", modifiers = ["bold"] }
+"markup.italic" = { fg = "iris", modifiers = ["italic"] }
+"markup.list" = { fg = "love" }
+"markup.quote" = { fg = "rose" }
+"markup.link.url" = { fg = "pine", modifiers = ["underlined"]}
+"markup.link.text" = { fg = "foam" }
+
 [palette]
 base     = "#191724" 
 surface  = "#1f1d2e" 
diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml
new file mode 100644
index 000000000..bec775069
--- /dev/null
+++ b/runtime/themes/rose_pine_dawn.toml
@@ -0,0 +1,73 @@
+# Author: RayGervais<raygervais@hotmail.ca>
+# Author: ChrisHa<chunghha@users.noreply.github.com>
+
+"ui.background" = { bg = "surface" }
+"ui.menu" = { fg = "text", bg = "overlay" }
+"ui.menu.selected" = { fg = "iris", bg = "surface" }
+"ui.linenr" = {fg = "subtle" }
+"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 = "surface" }
+"ui.window" = { bg = "base" }
+"ui.help" = { bg = "overlay", fg = "foam" }
+"text" = "text"
+"diff.plus" = "foam"
+"diff.delta" = "rose"
+"diff.minus" = "love"
+
+"info" = "gold"
+"hint" = "gold"
+"debug" = "rose"
+"diagnostic" = "rose"
+"error" = "love"
+
+"markup.heading" = { fg = "rose" }
+"markup.raw.inline" = { fg = "foam" }
+"markup.bold" = { fg = "gold", modifiers = ["bold"] }
+"markup.italic" = { fg = "iris", modifiers = ["italic"] }
+"markup.list" = { fg = "love" }
+"markup.quote" = { fg = "rose" }
+"markup.link.url" = { fg = "pine", modifiers = ["underlined"]}
+"markup.link.test" = { fg = "foam" }
+
+[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/serika-dark.toml b/runtime/themes/serika-dark.toml
new file mode 100644
index 000000000..da1457808
--- /dev/null
+++ b/runtime/themes/serika-dark.toml
@@ -0,0 +1,99 @@
+# Serika (Dark)
+# Author: VuiMuich
+
+# Original Author:
+# URL: https://github.com/arturoalviar/serika-syntax
+# Author: arturoalviar
+# License: MIT License
+
+"escape" = "orange"
+"type" = "yellow"
+"constant" = "purple"
+"number" = "purple"
+"string" = "fg"
+"comment" = "grey2"
+"variable" = "yellow"
+"variable.builtin" = "blue"
+"variable.parameter" = "yellow"
+"variable.property" = "yellow"
+"label" = "aqua"
+"punctuation" = "grey0"
+"punctuation.delimiter" = "grey2"
+"punctuation.bracket" = "fg"
+"keyword" = "red"
+"operator" = "grey0"
+"function" = "green"
+"function.builtin" = "blue"
+"function.macro" = "aqua"
+"tag" = "yellow"
+"namespace" = "fg"
+"attribute" = "aqua"
+"constructor" = "yellow"
+"module" = "blue"
+"property" = "yellow"
+"special" = "orange"
+
+"ui.background" = { bg = "bg0" }
+"ui.cursor" = { fg = "bg0", bg = "fg" }
+"ui.cursor.match" = { fg = "grey3", bg = "grey2" }
+"ui.cursor.insert" = { fg = "bg0", bg = "bg_yellow" }
+"ui.cursor.select" = { fg = "bg0", bg = "bg_yellow" }
+"ui.linenr" = "yellow"
+"ui.linenr.selected" = { fg = "fg", modifiers = ["bold", "underlined"] }
+"ui.statusline" = { fg = "grey1", bg = "bg2" }
+"ui.statusline.inactive" = { fg = "grey2", bg = "bg1" }
+"ui.popup" = { fg = "grey2", bg = "bg1" }
+"ui.window" = { fg = "grey2", bg = "bg1" }
+"ui.help" = { fg = "fg", bg = "bg1" }
+"ui.text" = "fg"
+"ui.text.focus" = "yellow"
+"ui.menu" = { fg = "fg", bg = "bg2" }
+"ui.menu.selected" = { fg = "bg0", bg = "bg_yellow" }
+"ui.selection" = { bg = "bg3" }
+
+"hint" = "blue"
+"info" = "aqua"
+"warning" = "yellow"
+"error" = "nasty-red"
+"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] }
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
+"markup.heading" = { fg = "purple", modifiers = ["bold"] }
+"markup.list" = "cyan"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
+"markup.link.url" = "cyan"
+"markup.link.text" = "pink"
+"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
+"markup.raw" = { fg = "foreground" }
+
+[palette]
+
+bg0 = "#323437"
+bg1 = "#494c50"
+bg2 = "#55585e"
+bg3 = "#61656b"
+bg4 = "#6d7278"
+bg5 = "#797e86"
+bg_visual = "#646669"
+bg_red = "#7e2a33"
+bg_green = "#86b365"
+bg_blue = "#6a89af"
+bg_yellow = "#e2b714"
+
+fg = "#d1d0c5"
+red = "#f9ebed"
+nasty-red = "#ca4754"
+dark-red = "#7e2a33"
+orange = "#dd8a3c"
+yellow = "#e2b714"
+green = "#e5eae1"
+aqua = "#b9c2c6"
+blue = "#bdcadb"
+purple = "#d0c4d4"
+grey0 = "#aaaeb3"
+grey1 = "#e1e1e3"
+grey2 = "#646669"
diff --git a/runtime/themes/serika-light.toml b/runtime/themes/serika-light.toml
new file mode 100644
index 000000000..edde90445
--- /dev/null
+++ b/runtime/themes/serika-light.toml
@@ -0,0 +1,100 @@
+# Serika (Light)
+# Author: VuiMuich
+
+# Original Author:
+# URL: https://github.com/arturoalviar/serika-syntax
+# Author: arturoalviar
+# License: MIT License
+
+"escape" = "orange"
+"type" = "yellow"
+"constant" = "purple"
+"number" = "purple"
+"string" = "fg"
+"comment" = "grey2"
+"variable" = "yellow"
+"variable.builtin" = "blue"
+"variable.parameter" = "yellow"
+"variable.property" = "yellow"
+"label" = "aqua"
+"punctuation" = "grey0"
+"punctuation.delimiter" = "grey2"
+"punctuation.bracket" = "fg"
+"keyword" = "red"
+"operator" = "grey0"
+"function" = "green"
+"function.builtin" = "blue"
+"function.macro" = "aqua"
+"tag" = "yellow"
+"namespace" = "fg"
+"attribute" = "aqua"
+"constructor" = "yellow"
+"module" = "blue"
+"property" = "yellow"
+"special" = "orange"
+
+"ui.background" = { bg = "bg0" }
+"ui.cursor" = { fg = "bg0", bg = "fg" }
+"ui.cursor.match" = { fg = "grey1", bg = "grey2" }
+"ui.cursor.insert" = { fg = "bg0", bg = "bg_yellow" }
+"ui.cursor.select" = { fg = "bg0", bg = "bg_yellow" }
+"ui.linenr" = "yellow"
+"ui.linenr.selected" = { fg = "fg", modifiers = ["bold", "underlined"] }
+"ui.statusline" = { fg = "grey1", bg = "bg5" }
+"ui.statusline.inactive" = { fg = "grey2", bg = "bg1" }
+"ui.popup" = { fg = "bg0", bg = "bg5" }
+"ui.window" = { fg = "bg0", bg = "bg5" }
+"ui.help" = { fg = "bg0", bg = "bg5" }
+"ui.text" = "fg"
+"ui.text.focus" = "yellow"
+"ui.menu" = { fg = "bg0", bg = "bg3" }
+"ui.menu.selected" = { fg = "bg0", bg = "bg_yellow" }
+"ui.selection" = { fg = "bg0", bg = "bg3" }
+
+"hint" = "blue"
+"info" = "aqua"
+"warning" = "yellow"
+"error" = "nasty-red"
+"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] }
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
+"markup.heading" = { fg = "purple", modifiers = ["bold"] }
+"markup.list" = "cyan"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
+"markup.link.url" = "cyan"
+"markup.link.text" = "pink"
+"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
+"markup.raw" = { fg = "foreground" }
+
+
+[palette]
+
+bg0 = "#e1e1e3"
+bg1 = "#494c50"
+bg2 = "#55585e"
+bg3 = "#61656b"
+bg4 = "#6d7278"
+bg5 = "#797e86"
+bg_visual = "#646669"
+bg_red = "#7e2a33"
+bg_green = "#86b365"
+bg_blue = "#6a89af"
+bg_yellow = "#e2b714"
+
+fg = "#323437"
+red = "#621d28"
+nasty-red = "#da3333"
+dark-red = "#791717"
+orange = "#57320f"
+yellow = "#e2b714"
+green = "#3f4b34"
+aqua = "#455054"
+blue = "#3f5673"
+purple = "#534059"
+grey0 = "#aaaeb3"
+grey1 = "#e1e1e3"
+grey2 = "#646669"
diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml
index 984c86ee8..dfaa104ae 100644
--- a/runtime/themes/solarized_dark.toml
+++ b/runtime/themes/solarized_dark.toml
@@ -22,6 +22,20 @@
 "module" = { fg = "violet" }
 "tag" = { fg = "magenta" }
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
 # 背景
 "ui.background" = { bg = "base03" }
 
@@ -58,13 +72,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 +87,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 0ab1b9626..c8a3dee11 100644
--- a/runtime/themes/solarized_light.toml
+++ b/runtime/themes/solarized_light.toml
@@ -22,6 +22,20 @@
 "module" = { fg = "violet" }
 "tag" = { fg = "magenta" }
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
 # 背景
 "ui.background" = { bg = "base03" }
 
@@ -58,13 +72,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 +87,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/runtime/themes/spacebones_light.toml b/runtime/themes/spacebones_light.toml
index 92f116ab9..5318dc2d3 100644
--- a/runtime/themes/spacebones_light.toml
+++ b/runtime/themes/spacebones_light.toml
@@ -30,6 +30,20 @@
 "label" = "#b1951d"
 "module" = "#b1951d"
 
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "#2d9574"
+"diff.delta" = "#715ab1"
+"diff.minus" = "#ba2f59"
+
 "warning" = { fg = "#da8b55" }
 "error" = { fg = "#e0211d" }
 "info" = { fg = "#b1951d" }
diff --git a/theme.toml b/theme.toml
index 8c0d1f6ca..d2c1fc32a 100644
--- a/theme.toml
+++ b/theme.toml
@@ -28,6 +28,17 @@ string = "silver"
 # used for lifetimes
 label = "honey"
 
+"markup.heading" = "lilac"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.link.url" = { fg = "silver", modifiers = ["underlined"] }
+"markup.link.text" = "almond"
+"markup.raw" = "almond"
+
+"diff.plus" = "#35bf86"
+"diff.minus" = "#f22c86"
+"diff.delta" = "#6f44f0"
+
 # 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 000000000..717530d0a
--- /dev/null
+++ b/xtask/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "xtask"
+version = "0.6.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+helix-term = { version = "0.6", path = "../helix-term" }
+helix-core = { version = "0.6", path = "../helix-core" }
+toml = "0.5"
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
new file mode 100644
index 000000000..d24a29cc4
--- /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 {
+        format!("| {} |\n", cols.join(" | "))
+    }
+
+    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(())
+}