aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJJ2023-07-17 17:00:43 +0000
committerJJ2023-07-17 17:00:43 +0000
commit765a9e4595dc5c6127b9242c4888b89856e65a72 (patch)
treeb67920fb0cdd468c0f5cb25c75bc5097f1ef3401
parentb0b9194075dc72938e38f3b7c9ef465b893244e3 (diff)
Add patches from helix/staging
-rw-r--r--0001-Make-initial-editing-mode-configurable.patch64
-rw-r--r--0002-Fix-writes-from-insert-mode.patch15
-rw-r--r--0003-Add-support-for-moving-lines.patch519
-rw-r--r--0004-Add-support-for-Unicode-input.patch612
-rw-r--r--0011-Add-file-explorer-and-tree-helper.patch4729
-rw-r--r--0012-Add-rainbow-tree-sitter-matches.patch2432
-rw-r--r--0013-Add-rainbow-indentation-guides.patch190
-rw-r--r--0014-Add-unbind-default-keys-config-option.patch106
8 files changed, 8642 insertions, 25 deletions
diff --git a/0001-Make-initial-editing-mode-configurable.patch b/0001-Make-initial-editing-mode-configurable.patch
index 471c9488..4a8beb40 100644
--- a/0001-Make-initial-editing-mode-configurable.patch
+++ b/0001-Make-initial-editing-mode-configurable.patch
@@ -1,30 +1,31 @@
-From 2ea1169d0d1acec6b8d7368cb79fbb776e1dfbc6 Mon Sep 17 00:00:00 2001
-From: j-james <jj@j-james.me>
-Date: Thu, 29 Sep 2022 16:17:52 -0700
-Subject: [PATCH] Make initial editing mode configurable
+From d3ce6c32f4b4957759209ee6cb215e7968c343e8 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 17:40:45 -0700
+Subject: [PATCH 1/2] Make initial editing mode configurable
+ref: https://github.com/helix-editor/helix/pull/3366
---
book/src/configuration.md | 1 +
helix-view/src/editor.rs | 4 ++++
2 files changed, 5 insertions(+)
diff --git a/book/src/configuration.md b/book/src/configuration.md
-index fdabe768..a55e7d65 100644
+index 1b94ae85..b69bb486 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
-@@ -49,6 +49,7 @@ ### `[editor]` Section
- | `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` |
- | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` |
+@@ -59,6 +59,7 @@ ### `[editor]` Section
+ | `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
+ | `undercurl` | Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative | `false` |
+ | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
+| `initial-mode` | The initial mode for newly opened editors. | `"normal"` |
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
-
+ | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
-index 5eff9983..331b17b9 100644
+index affe87dd..3c00a61e 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
-@@ -166,6 +166,8 @@ pub struct Config {
+@@ -281,6 +281,8 @@ pub struct Config {
pub bufferline: BufferLine,
/// Vertical indent width guides.
pub indent_guides: IndentGuidesConfig,
@@ -32,16 +33,16 @@ index 5eff9983..331b17b9 100644
+ pub initial_mode: Mode,
/// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool,
- }
-@@ -576,6 +578,7 @@ fn default() -> Self {
+ pub soft_wrap: SoftWrap,
+@@ -812,6 +814,7 @@ fn default() -> Self {
whitespace: WhitespaceConfig::default(),
bufferline: BufferLine::default(),
indent_guides: IndentGuidesConfig::default(),
+ initial_mode: Mode::Normal,
color_modes: false,
- }
- }
-@@ -993,6 +996,7 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
+ soft_wrap: SoftWrap {
+ enable: Some(false),
+@@ -1363,6 +1366,7 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
/// Generate an id for a new document and register it.
fn new_document(&mut self, mut doc: Document) -> DocumentId {
@@ -50,5 +51,32 @@ index 5eff9983..331b17b9 100644
// Safety: adding 1 from 1 is fine, probably impossible to reach usize max
self.next_document_id =
--
-2.37.3
+2.41.0
+
+
+From 96f81b33f6a1380a4acb5b3ca379e30a0fd29e48 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 23:40:55 -0700
+Subject: [PATCH 2/2] bandaid for enter_normal_mode panicking upon launch
+
+---
+ helix-view/src/editor.rs | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
+index 5fce5446..1d1f6bf7 100644
+--- a/helix-view/src/editor.rs
++++ b/helix-view/src/editor.rs
+@@ -1332,7 +1332,8 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
+ return;
+ }
+
+- self.enter_normal_mode();
++ // panics upon launch. todo: debug
++ // self.enter_normal_mode();
+
+ match action {
+ Action::Replace => {
+--
+2.41.0
diff --git a/0002-Fix-writes-from-insert-mode.patch b/0002-Fix-writes-from-insert-mode.patch
index f64c3b7f..22b00cf3 100644
--- a/0002-Fix-writes-from-insert-mode.patch
+++ b/0002-Fix-writes-from-insert-mode.patch
@@ -1,15 +1,16 @@
-From 9c4369da34bbbb1a6d26ba35be41739f36fb722b Mon Sep 17 00:00:00 2001
+From 6ef36e958254b873684621bb91d1ba299e2d9d7d Mon Sep 17 00:00:00 2001
From: JJ <git@toki.la>
-Date: Sat, 3 Jun 2023 23:03:58 -0700
+Date: Sat, 15 Jul 2023 17:42:04 -0700
Subject: [PATCH 1/2] Fix writes from insert mode not properly updating the
revision history
+ref: https://github.com/helix-editor/helix/pull/7226
---
helix-term/src/commands/typed.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
-index 706442e4..8e043385 100644
+index dfc71dfd..3f746bf0 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -350,6 +350,8 @@ fn write_impl(
@@ -25,9 +26,9 @@ index 706442e4..8e043385 100644
2.41.0
-From e2a877432ce7f75eba60d5dd579e5a0ab45b73e8 Mon Sep 17 00:00:00 2001
+From 8305a40ec1fba47295987e8e8278a4aa2061009e Mon Sep 17 00:00:00 2001
From: JJ <git@toki.la>
-Date: Sat, 3 Jun 2023 23:06:02 -0700
+Date: Sat, 15 Jul 2023 17:44:04 -0700
Subject: [PATCH 2/2] Write pre-manipulation pastes to the revision history
---
@@ -35,10 +36,10 @@ Subject: [PATCH 2/2] Write pre-manipulation pastes to the revision history
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
-index 43b5d1af..ef4299b4 100644
+index 5b5cda93..252aff0d 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
-@@ -1235,15 +1235,9 @@ fn handle_event(
+@@ -1240,15 +1240,9 @@ fn handle_event(
cx.editor.count = None;
let config = cx.editor.config();
diff --git a/0003-Add-support-for-moving-lines.patch b/0003-Add-support-for-moving-lines.patch
new file mode 100644
index 00000000..afa15cf2
--- /dev/null
+++ b/0003-Add-support-for-moving-lines.patch
@@ -0,0 +1,519 @@
+From a46db95dc5cdae4deca31b8e021805768d1a8390 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 17:55:37 -0700
+Subject: [PATCH 1/2] Add support for moving lines and selections above and
+ below
+
+ref: https://github.com/helix-editor/helix/pull/4545
+---
+ helix-term/src/commands.rs | 213 ++++++++++++++++++++++++++++++-
+ helix-term/src/keymap/default.rs | 2 +
+ 2 files changed, 214 insertions(+), 1 deletion(-)
+
+diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
+index 58c17296..dc8b5b5c 100644
+--- a/helix-term/src/commands.rs
++++ b/helix-term/src/commands.rs
+@@ -28,7 +28,7 @@
+ textobject,
+ tree_sitter::Node,
+ unicode::width::UnicodeWidthChar,
+- visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
++ visual_offset_from_block, Change, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
+ RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
+ };
+ use helix_view::{
+@@ -326,6 +326,8 @@ pub fn doc(&self) -> &str {
+ goto_declaration, "Goto declaration",
+ add_newline_above, "Add newline above",
+ add_newline_below, "Add newline below",
++ move_selection_above, "Move current line or selection up",
++ move_selection_below, "Move current line or selection down",
+ goto_type_definition, "Goto type definition",
+ goto_implementation, "Goto implementation",
+ goto_file_start, "Goto line number <n> else file start",
+@@ -5510,6 +5512,215 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
+ doc.apply(&transaction, view.id);
+ }
+
++#[derive(Debug, PartialEq, Eq)]
++pub enum MoveSelection {
++ Below,
++ Above,
++}
++
++type ExtendedChange = (usize, usize, Option<Tendril>, Option<(usize, usize)>);
++
++/// Move line or block of text in specified direction.
++/// The function respects single line, single selection, multiple lines using
++/// several cursors and multiple selections.
++fn move_selection(cx: &mut Context, direction: MoveSelection) {
++ let (view, doc) = current!(cx.editor);
++ let selection = doc.selection(view.id);
++ let text = doc.text();
++ let slice = text.slice(..);
++ let mut last_step_changes: Vec<ExtendedChange> = vec![];
++ let mut at_doc_edge = false;
++ let all_changes = selection.into_iter().map(|range| {
++ let (start, end) = range.line_range(slice);
++ let line_start = text.line_to_char(start);
++ let line_end = line_end_char_index(&slice, end);
++ let line = text.slice(line_start..line_end).to_string();
++
++ let next_line = match direction {
++ MoveSelection::Above => start.saturating_sub(1),
++ MoveSelection::Below => end + 1,
++ };
++
++ let rel_pos_anchor = range.anchor - line_start;
++ let rel_pos_head = range.head - line_start;
++
++ if next_line == start || next_line >= text.len_lines() || at_doc_edge {
++ at_doc_edge = true;
++ let cursor_rel_pos = (rel_pos_anchor, rel_pos_head);
++ let changes = vec![(
++ line_start,
++ line_end,
++ Some(line.into()),
++ Some(cursor_rel_pos),
++ )];
++ last_step_changes = changes.clone();
++ changes
++ } else {
++ let next_line_start = text.line_to_char(next_line);
++ let next_line_end = line_end_char_index(&slice, next_line);
++ let next_line_text = text.slice(next_line_start..next_line_end).to_string();
++
++ let cursor_rel_pos = (rel_pos_anchor, rel_pos_head);
++ let changes = match direction {
++ MoveSelection::Above => vec![
++ (
++ next_line_start,
++ next_line_end,
++ Some(line.into()),
++ Some(cursor_rel_pos),
++ ),
++ (line_start, line_end, Some(next_line_text.into()), None),
++ ],
++ MoveSelection::Below => vec![
++ (line_start, line_end, Some(next_line_text.into()), None),
++ (
++ next_line_start,
++ next_line_end,
++ Some(line.into()),
++ Some(cursor_rel_pos),
++ ),
++ ],
++ };
++
++ let changes = if last_step_changes.len() > 1 {
++ evaluate_changes(last_step_changes.clone(), changes.clone(), &direction)
++ } else {
++ changes
++ };
++ last_step_changes = changes.clone();
++ changes
++ }
++ });
++
++ /// Merge changes from subsequent cursors
++ fn evaluate_changes(
++ mut last_changes: Vec<ExtendedChange>,
++ current_changes: Vec<ExtendedChange>,
++ direction: &MoveSelection,
++ ) -> Vec<ExtendedChange> {
++ let mut current_it = current_changes.into_iter();
++
++ if let (Some(mut last), Some(mut current_first), Some(current_last)) =
++ (last_changes.pop(), current_it.next(), current_it.next())
++ {
++ if last.0 == current_first.0 {
++ match direction {
++ MoveSelection::Above => {
++ last.0 = current_last.0;
++ last.1 = current_last.1;
++ if let Some(first) = last_changes.pop() {
++ last_changes.push(first)
++ }
++ last_changes.extend(vec![current_first, last.to_owned()]);
++ last_changes
++ }
++ MoveSelection::Below => {
++ current_first.0 = last_changes[0].0;
++ current_first.1 = last_changes[0].1;
++ last_changes[0] = current_first;
++ last_changes.extend(vec![last.to_owned(), current_last]);
++ last_changes
++ }
++ }
++ } else {
++ if let Some(first) = last_changes.pop() {
++ last_changes.push(first)
++ }
++ last_changes.extend(vec![last.to_owned(), current_first, current_last]);
++ last_changes
++ }
++ } else {
++ last_changes
++ }
++ }
++
++ let mut flattened: Vec<Vec<ExtendedChange>> = all_changes.into_iter().collect();
++ let last_changes = flattened.pop().unwrap_or(vec![]);
++
++ let acc_cursors = get_adjusted_selection(&doc, &last_changes, direction, at_doc_edge);
++
++ let changes: Vec<Change> = last_changes
++ .into_iter()
++ .map(|change| (change.0, change.1, change.2.to_owned()))
++ .collect();
++
++ let new_sel = Selection::new(acc_cursors.into(), 0);
++ let transaction = Transaction::change(doc.text(), changes.into_iter());
++
++ doc.apply(&transaction, view.id);
++ doc.set_selection(view.id, new_sel);
++}
++
++/// Returns selection range that is valid for the updated document
++/// This logic is necessary because it's not possible to apply changes
++/// to the document first and then set selection.
++fn get_adjusted_selection(
++ doc: &Document,
++ last_changes: &Vec<ExtendedChange>,
++ direction: MoveSelection,
++ at_doc_edge: bool,
++) -> Vec<Range> {
++ let mut first_change_len = 0;
++ let mut next_start = 0;
++ let mut acc_cursors: Vec<Range> = vec![];
++
++ for change in last_changes.iter() {
++ let change_len = change.2.as_ref().map_or(0, |x| x.len());
++
++ if let Some((rel_anchor, rel_head)) = change.3 {
++ let (anchor, head) = if at_doc_edge {
++ let anchor = change.0 + rel_anchor;
++ let head = change.0 + rel_head;
++ (anchor, head)
++ } else {
++ match direction {
++ MoveSelection::Above => {
++ if next_start == 0 {
++ next_start = change.0;
++ }
++ let anchor = next_start + rel_anchor;
++ let head = next_start + rel_head;
++
++ // If there is next cursor below, selection position should be adjusted
++ // according to the length of the current line.
++ next_start += change_len + doc.line_ending.len_chars();
++ (anchor, head)
++ }
++ MoveSelection::Below => {
++ let anchor = change.0 + first_change_len + rel_anchor - change_len;
++ let head = change.0 + first_change_len + rel_head - change_len;
++ (anchor, head)
++ }
++ }
++ };
++
++ let cursor = Range::new(anchor, head);
++ if let Some(last) = acc_cursors.pop() {
++ if cursor.overlaps(&last) {
++ acc_cursors.push(last);
++ } else {
++ acc_cursors.push(last);
++ acc_cursors.push(cursor);
++ };
++ } else {
++ acc_cursors.push(cursor);
++ };
++ } else {
++ first_change_len = change.2.as_ref().map_or(0, |x| x.len());
++ next_start = 0;
++ };
++ }
++ acc_cursors
++}
++
++fn move_selection_below(cx: &mut Context) {
++ move_selection(cx, MoveSelection::Below)
++}
++
++fn move_selection_above(cx: &mut Context) {
++ move_selection(cx, MoveSelection::Above)
++}
++
+ enum IncrementDirection {
+ Increase,
+ Decrease,
+diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
+index c84c616c..f384c868 100644
+--- a/helix-term/src/keymap/default.rs
++++ b/helix-term/src/keymap/default.rs
+@@ -321,6 +321,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
+
+ "C-a" => increment,
+ "C-x" => decrement,
++ "C-k" => move_selection_above,
++ "C-j" => move_selection_below,
+ });
+ let mut select = normal.clone();
+ select.merge_nodes(keymap!({ "Select mode"
+--
+2.41.0
+
+
+From 3888d20b74bf8a723af276973485493231ab0f85 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 17:55:58 -0700
+Subject: [PATCH 2/2] Unit test moving lines and selections
+
+---
+ helix-term/tests/test/commands.rs | 186 ++++++++++++++++++++++++++++--
+ 1 file changed, 176 insertions(+), 10 deletions(-)
+
+diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs
+index b13c37bc..4ca7e9f2 100644
+--- a/helix-term/tests/test/commands.rs
++++ b/helix-term/tests/test/commands.rs
+@@ -96,6 +96,172 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
+ Ok(())
+ }
+
++// Line selection movement tests
++
++#[tokio::test(flavor = "multi_thread")]
++async fn test_move_selection_single_selection_up() -> anyhow::Result<()> {
++ test((
++ platform_line(indoc! {"
++ aaaaaa
++ bbbbbb
++ cc#[|c]#ccc
++ dddddd
++ "})
++ .as_str(),
++ "<C-k>",
++ platform_line(indoc! {"
++ aaaaaa
++ cc#[|c]#ccc
++ bbbbbb
++ dddddd
++ "})
++ .as_str(),
++ ))
++ .await?;
++
++ Ok(())
++}
++
++#[tokio::test(flavor = "multi_thread")]
++async fn test_move_selection_single_selection_down() -> anyhow::Result<()> {
++ test((
++ platform_line(indoc! {"
++ aa#[|a]#aaa
++ bbbbbb
++ cccccc
++ dddddd
++ "})
++ .as_str(),
++ "<C-j>",
++ platform_line(indoc! {"
++ bbbbbb
++ aa#[|a]#aaa
++ cccccc
++ dddddd
++ "})
++ .as_str(),
++ ))
++ .await?;
++
++ Ok(())
++}
++
++#[tokio::test(flavor = "multi_thread")]
++async fn test_move_selection_single_selection_top_up() -> anyhow::Result<()> {
++ // if already on top of the file and going up, nothing should change
++ test((
++ platform_line(indoc! {"
++ aa#[|a]#aaa
++ bbbbbb
++ cccccc
++ dddddd"})
++ .as_str(),
++ "<C-k>",
++ platform_line(indoc! {"
++ aa#[|a]#aaa
++ bbbbbb
++ cccccc
++ dddddd"})
++ .as_str(),
++ ))
++ .await?;
++
++ Ok(())
++}
++
++#[tokio::test(flavor = "multi_thread")]
++async fn test_move_selection_single_selection_bottom_down() -> anyhow::Result<()> {
++ // If going down on the bottom line, nothing should change
++ // Note that platform_line is not used here, because it inserts trailing
++ // linebreak, making it impossible to test
++ test((
++ "aaaaaa\nbbbbbb\ncccccc\ndd#[|d]#ddd",
++ "<C-j><C-j>",
++ "aaaaaa\nbbbbbb\ncccccc\ndd#[|d]#ddd",
++ ))
++ .await?;
++
++ Ok(())
++}
++
++#[tokio::test(flavor = "multi_thread")]
++async fn test_move_selection_block_up() -> anyhow::Result<()> {
++ test((
++ platform_line(indoc! {"
++ aaaaaa
++ bb#[bbbb
++ ccc|]#ccc
++ dddddd
++ eeeeee
++ "})
++ .as_str(),
++ "<C-k>",
++ platform_line(indoc! {"
++ bb#[bbbb
++ ccc|]#ccc
++ aaaaaa
++ dddddd
++ eeeeee
++ "})
++ .as_str(),
++ ))
++ .await?;
++
++ Ok(())
++}
++
++#[tokio::test(flavor = "multi_thread")]
++async fn test_move_selection_block_down() -> anyhow::Result<()> {
++ test((
++ platform_line(indoc! {"
++ #[|aaaaaa
++ bbbbbb
++ ccc]#ccc
++ dddddd
++ eeeeee
++ "})
++ .as_str(),
++ "<C-j>",
++ platform_line(indoc! {"
++ dddddd
++ #[|aaaaaa
++ bbbbbb
++ ccc]#ccc
++ eeeeee
++ "})
++ .as_str(),
++ ))
++ .await?;
++
++ Ok(())
++}
++
++#[tokio::test(flavor = "multi_thread")]
++async fn test_move_two_cursors_down() -> anyhow::Result<()> {
++ test((
++ platform_line(indoc! {"
++ aaaaaa
++ bb#[|b]#bbb
++ cccccc
++ d#(dd|)#ddd
++ eeeeee
++ "})
++ .as_str(),
++ "<C-j>",
++ platform_line(indoc! {"
++ aaaaaa
++ cccccc
++ bb#[|b]#bbb
++ eeeeee
++ d#(dd|)#ddd
++ "})
++ .as_str(),
++ ))
++ .await?;
++
++ Ok(())
++}
++
+ #[tokio::test(flavor = "multi_thread")]
+ async fn test_goto_file_impl() -> anyhow::Result<()> {
+ let file = tempfile::NamedTempFile::new()?;
+@@ -187,11 +353,11 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
+ "|echo foo<ret>",
+ platform_line(indoc! {"\
+ #[|foo\n]#
+-
++
+ #(|foo\n)#
+-
++
+ #(|foo\n)#
+-
++
+ "}),
+ ))
+ .await?;
+@@ -225,11 +391,11 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
+ "<A-!>echo foo<ret>",
+ platform_line(indoc! {"\
+ lorem#[|foo\n]#
+-
++
+ ipsum#(|foo\n)#
+-
++
+ dolor#(|foo\n)#
+-
++
+ "}),
+ ))
+ .await?;
+@@ -273,14 +439,14 @@ async fn test_extend_line() -> anyhow::Result<()> {
+ #[l|]#orem
+ ipsum
+ dolor
+-
++
+ "}),
+ "x2x",
+ platform_line(indoc! {"\
+ #[lorem
+ ipsum
+ dolor\n|]#
+-
++
+ "}),
+ ))
+ .await?;
+@@ -290,13 +456,13 @@ async fn test_extend_line() -> anyhow::Result<()> {
+ platform_line(indoc! {"\
+ #[l|]#orem
+ ipsum
+-
++
+ "}),
+ "2x",
+ platform_line(indoc! {"\
+ #[lorem
+ ipsum\n|]#
+-
++
+ "}),
+ ))
+ .await?;
+--
+2.41.0
+
diff --git a/0004-Add-support-for-Unicode-input.patch b/0004-Add-support-for-Unicode-input.patch
new file mode 100644
index 00000000..e27ed7e4
--- /dev/null
+++ b/0004-Add-support-for-Unicode-input.patch
@@ -0,0 +1,612 @@
+From 9a032b407260c8bd685b42da300d13afffb12d94 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 22:10:15 -0700
+Subject: [PATCH] Add support for digraphs
+
+ref: https://github.com/helix-editor/helix/pull/2852
+---
+ book/src/configuration.md | 12 +
+ book/src/keymap.md | 1 +
+ helix-term/src/commands.rs | 48 ++++
+ helix-term/src/keymap/default.rs | 2 +
+ helix-view/src/digraph.rs | 437 +++++++++++++++++++++++++++++++
+ helix-view/src/editor.rs | 4 +
+ helix-view/src/lib.rs | 1 +
+ 7 files changed, 505 insertions(+)
+ create mode 100644 helix-view/src/digraph.rs
+
+diff --git a/book/src/configuration.md b/book/src/configuration.md
+index 6609c30e..0af686f4 100644
+--- a/book/src/configuration.md
++++ b/book/src/configuration.md
+@@ -360,3 +360,15 @@ ### `[editor.explorer]` Section
+ | -------------- | ------------------------------------------- | ------- |
+ | `column-width` | explorer side width | 30 |
+ | `position` | explorer widget position, `left` or `right` | `left` |
++
++### `[editor.digraphs]` Section
++
++By default, special characters can be input using the `insert_digraphs` command, bound to `\` in normal mode.
++Custom digraphs can be added to the `editor.digraphs` section of the config.
++
++```toml
++[editor.digraphs]
++ka = "か"
++ku = { symbols = "く", description = "The japanese character Ku" }
++shrug = "¯\\_(ツ)_/¯"
++```
+diff --git a/book/src/keymap.md b/book/src/keymap.md
+index 4e6e878d..11ad358a 100644
+--- a/book/src/keymap.md
++++ b/book/src/keymap.md
+@@ -72,6 +72,7 @@ ### Changes
+ | `a` | Insert after selection (append) | `append_mode` |
+ | `I` | Insert at the start of the line | `insert_at_line_start` |
+ | `A` | Insert at the end of the line | `insert_at_line_end` |
++| `\` | Insert digraphs | `insert_digraph` |
+ | `o` | Open new line below selection | `open_below` |
+ | `O` | Open new line above selection | `open_above` |
+ | `.` | Repeat last insert | N/A |
+diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
+index edeb419b..771c73bc 100644
+--- a/helix-term/src/commands.rs
++++ b/helix-term/src/commands.rs
+@@ -490,6 +490,7 @@ pub fn doc(&self) -> &str {
+ command_palette, "Open command palette",
+ open_or_focus_explorer, "Open or focus explorer",
+ reveal_current_file, "Reveal current file in explorer",
++ insert_digraph, "Insert Unicode characters with prompt",
+ );
+ }
+
+@@ -5901,3 +5902,50 @@ fn replay_macro(cx: &mut Context) {
+ cx.editor.macro_replaying.pop();
+ }));
+ }
++
++fn insert_digraph(cx: &mut Context) {
++ ui::prompt(
++ cx,
++ "digraph:".into(),
++ Some('K'), // todo: decide on register to use
++ move |editor, input| {
++ editor
++ .config()
++ .digraphs
++ .search(input)
++ .take(10)
++ .map(|entry| {
++ // todo: Prompt does not currently allow additional text as part
++ // of it's suggestions. Show the user the symbol and description
++ // once prompt has been made more robust
++ #[allow(clippy::useless_format)]
++ ((0..), Cow::from(format!("{}", entry.sequence)))
++ })
++ .collect()
++ },
++ move |cx, input, event| {
++ match event {
++ PromptEvent::Validate => (),
++ _ => return,
++ }
++ let config = cx.editor.config();
++ let symbols = if let Some(entry) = config.digraphs.get(input) {
++ &entry.symbols
++ } else {
++ cx.editor.set_error("Digraph not found");
++ return;
++ };
++
++ let (view, doc) = current!(cx.editor);
++ let selection = doc.selection(view.id);
++ let mut changes = Vec::with_capacity(selection.len());
++
++ for range in selection.ranges() {
++ changes.push((range.from(), range.from(), Some(symbols.clone().into())));
++ }
++ let trans = Transaction::change(doc.text(), changes.into_iter());
++ doc.apply(&trans, view.id);
++ doc.append_changes_to_history(view);
++ },
++ )
++}
+diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
+index 43d101fe..9702c627 100644
+--- a/helix-term/src/keymap/default.rs
++++ b/helix-term/src/keymap/default.rs
+@@ -135,6 +135,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
+ "N" => search_prev,
+ "*" => search_selection,
+
++ "\\" => insert_digraph,
++
+ "u" => undo,
+ "U" => redo,
+ "A-u" => earlier,
+diff --git a/helix-view/src/digraph.rs b/helix-view/src/digraph.rs
+new file mode 100644
+index 00000000..86333333
+--- /dev/null
++++ b/helix-view/src/digraph.rs
+@@ -0,0 +1,437 @@
++use anyhow::Result;
++use serde::{ser::SerializeMap, Deserialize, Serialize};
++use std::collections::HashMap;
++
++// Errors
++#[derive(PartialEq, Eq, Debug, Clone)]
++pub enum Error {
++ EmptyInput(String),
++ DuplicateEntry {
++ seq: String,
++ current: String,
++ existing: String,
++ },
++ Custom(String),
++}
++
++impl serde::de::Error for Error {
++ fn custom<T>(msg: T) -> Self
++ where
++ T: std::fmt::Display,
++ {
++ Error::Custom(msg.to_string())
++ }
++}
++
++impl std::fmt::Display for Error {
++ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
++ match self {
++ Error::EmptyInput(s) => {
++ f.write_str(&format!("No symbols were given for key sequence {}", s))
++ }
++ Error::DuplicateEntry {
++ seq,
++ current,
++ existing,
++ } => f.write_str(&format!(
++ "Attempted to bind {} to symbols ({}) when already bound to ({})",
++ seq, current, existing
++ )),
++ Error::Custom(s) => f.write_str(s),
++ }
++ }
++}
++
++impl std::error::Error for Error {}
++
++/// Trie implementation for storing and searching input
++/// strings -> unicode characters defined by the user.
++#[derive(Default, Debug, Clone, PartialEq, Eq)]
++pub struct DigraphStore {
++ head: DigraphNode,
++}
++
++#[derive(Default, Debug, Clone, PartialEq, Eq)]
++struct DigraphNode {
++ output: Option<FullDigraphEntry>,
++ children: Option<HashMap<char, DigraphNode>>,
++}
++
++#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
++pub struct DigraphEntry {
++ pub symbols: String,
++ pub description: Option<String>,
++}
++
++#[derive(Default, Debug, Clone, PartialEq, Eq)]
++pub struct FullDigraphEntry {
++ pub sequence: String,
++ pub symbols: String,
++ pub description: Option<String>,
++}
++
++impl<'de> Deserialize<'de> for DigraphStore {
++ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
++ where
++ D: serde::Deserializer<'de>,
++ {
++ #[derive(Deserialize)]
++ #[serde(untagged)]
++ enum EntryDef {
++ Full(DigraphEntry),
++ Symbols(String),
++ }
++
++ let mut store = Self::default();
++ HashMap::<String, EntryDef>::deserialize(deserializer)?
++ .into_iter()
++ .map(|(k, d)| match d {
++ EntryDef::Symbols(symbols) => (
++ k,
++ DigraphEntry {
++ symbols,
++ description: None,
++ },
++ ),
++ EntryDef::Full(entry) => (k, entry),
++ })
++ .try_for_each(|(k, v)| store.insert(&k, v))
++ .map_err(serde::de::Error::custom)?;
++
++ Ok(store)
++ }
++}
++
++impl Serialize for DigraphStore {
++ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
++ where
++ S: serde::Serializer,
++ {
++ let mut m = serializer.serialize_map(None)?;
++
++ self.search("").try_for_each(|entry| {
++ m.serialize_entry(
++ &entry.sequence,
++ &DigraphEntry {
++ symbols: entry.symbols.clone(),
++ description: entry.description.clone(),
++ },
++ )
++ })?;
++ m.end()
++ }
++}
++
++/// A Store of input -> unicode strings that can be quickly looked up and
++/// searched.
++impl DigraphStore {
++ /// Inserts a new unicode string into the store
++ pub fn insert(&mut self, input_seq: &str, entry: DigraphEntry) -> Result<(), Error> {
++ if input_seq.is_empty() {
++ return Err(Error::EmptyInput(input_seq.to_string()));
++ }
++
++ self.head.insert(
++ input_seq,
++ FullDigraphEntry {
++ sequence: input_seq.to_string(),
++ symbols: entry.symbols,
++ description: entry.description,
++ },
++ )
++ }
++
++ /// Attempts to retrieve a stored unicode string if it exists
++ pub fn get(&self, exact_seq: &str) -> Option<&FullDigraphEntry> {
++ self.head.get(exact_seq).and_then(|n| n.output.as_ref())
++ }
++
++ /// Returns an iterator of closest matches to the input string
++ pub fn search(&self, input_seq: &str) -> impl Iterator<Item = &FullDigraphEntry> {
++ self.head.get(input_seq).into_iter().flat_map(|x| x.iter())
++ }
++}
++
++impl DigraphNode {
++ fn insert(&mut self, input_seq: &str, entry: FullDigraphEntry) -> Result<(), Error> {
++ // see if we found the spot to insert our unicode
++ if input_seq.is_empty() {
++ if let Some(existing) = &self.output {
++ return Err(Error::DuplicateEntry {
++ seq: entry.sequence,
++ existing: existing.symbols.clone(),
++ current: entry.symbols,
++ });
++ } else {
++ self.output = Some(entry);
++ return Ok(());
++ }
++ }
++
++ // continue searching
++ let node = self
++ .children
++ .get_or_insert(Default::default())
++ .entry(input_seq.chars().next().unwrap())
++ .or_default();
++
++ node.insert(&input_seq[1..], entry)
++ }
++
++ fn get(&self, exact_seq: &str) -> Option<&Self> {
++ if exact_seq.is_empty() {
++ return Some(self);
++ }
++
++ self.children
++ .as_ref()
++ .and_then(|cm| cm.get(&exact_seq.chars().next().unwrap()))
++ .and_then(|node| node.get(&exact_seq[1..]))
++ }
++
++ fn iter<'a>(&'a self) -> impl Iterator<Item = &FullDigraphEntry> + 'a {
++ DigraphIter::new(self)
++ }
++}
++
++pub struct DigraphIter<'a, 'b>
++where
++ 'a: 'b,
++{
++ element_iter: Box<dyn Iterator<Item = &'a FullDigraphEntry> + 'b>,
++ node_iter: Box<dyn Iterator<Item = &'a DigraphNode> + 'b>,
++}
++
++impl<'a, 'b> DigraphIter<'a, 'b>
++where
++ 'a: 'b,
++{
++ fn new(node: &'a DigraphNode) -> Self {
++ // do a lazy breadth-first search by keeping track of the next 'rung' of
++ // elements to produce, and the next 'rung' of nodes to refill the element
++ // iterator when empty
++ Self {
++ element_iter: Box::new(node.output.iter().chain(Self::get_child_elements(node))),
++ node_iter: Box::new(Self::get_child_nodes(node)),
++ }
++ }
++
++ fn get_child_elements(
++ node: &'a DigraphNode,
++ ) -> impl Iterator<Item = &'a FullDigraphEntry> + 'b {
++ node.children
++ .iter()
++ .flat_map(|hm| hm.iter())
++ .flat_map(|(_, node)| node.output.as_ref())
++ }
++
++ fn get_child_nodes(node: &'a DigraphNode) -> impl Iterator<Item = &'a DigraphNode> + 'b {
++ node.children
++ .iter()
++ .flat_map(|x| x.iter().map(|(_, node)| node))
++ }
++}
++impl<'a, 'b> Iterator for DigraphIter<'a, 'b>
++where
++ 'a: 'b,
++{
++ type Item = &'a FullDigraphEntry;
++
++ fn next(&mut self) -> Option<Self::Item> {
++ loop {
++ if let Some(e) = self.element_iter.next() {
++ return Some(e);
++ }
++
++ // We ran out of elements, fetch more by traversing the next rung of nodes
++ match self.node_iter.next() {
++ Some(node) => {
++ // todo: figure out a better way to update self's nodes
++ let mut new_nodes: Box<dyn Iterator<Item = &DigraphNode>> =
++ Box::new(std::iter::empty());
++ std::mem::swap(&mut new_nodes, &mut self.node_iter);
++ let mut new_nodes: Box<dyn Iterator<Item = &DigraphNode>> =
++ Box::new(new_nodes.chain(Self::get_child_nodes(node)));
++ std::mem::swap(&mut new_nodes, &mut self.node_iter);
++
++ self.element_iter = Box::new(Self::get_child_elements(node));
++ }
++ None => return None,
++ }
++ }
++ }
++}
++
++#[cfg(test)]
++mod tests {
++ use super::*;
++
++ #[test]
++ fn digraph_insert() {
++ let mut dg = DigraphStore::default();
++ dg.insert(
++ "abc",
++ DigraphEntry {
++ symbols: "testbug".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ dg.insert(
++ "abd",
++ DigraphEntry {
++ symbols: "deadbeef".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ assert_eq!(
++ dg.head
++ .children
++ .as_ref()
++ .unwrap()
++ .get(&'a')
++ .unwrap()
++ .children
++ .as_ref()
++ .unwrap()
++ .get(&'b')
++ .unwrap()
++ .children
++ .as_ref()
++ .unwrap()
++ .get(&'c')
++ .unwrap()
++ .output
++ .clone()
++ .unwrap()
++ .symbols
++ .clone(),
++ "testbug".to_string()
++ );
++ }
++
++ #[test]
++ fn digraph_insert_and_get() {
++ let mut dg = DigraphStore::default();
++ dg.insert(
++ "abc",
++ DigraphEntry {
++ symbols: "testbug".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ dg.insert(
++ "abd",
++ DigraphEntry {
++ symbols: "deadbeef".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ assert_eq!(
++ dg.get("abc").map(|x| x.symbols.clone()),
++ Some("testbug".to_string())
++ );
++ assert_eq!(
++ dg.get("abd").map(|x| x.symbols.clone()),
++ Some("deadbeef".to_string())
++ );
++ assert_eq!(dg.get("abe").map(|x| x.symbols.clone()), None);
++ }
++
++ #[test]
++ fn digraph_node_iter() {
++ let mut dg = DigraphStore::default();
++ dg.insert(
++ "abc",
++ DigraphEntry {
++ symbols: "testbug".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ dg.insert(
++ "abd",
++ DigraphEntry {
++ symbols: "deadbeef".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ assert_eq!(dg.head.iter().count(), 2);
++ }
++
++ #[test]
++ fn digraph_search() {
++ let mut dg = DigraphStore::default();
++ dg.insert(
++ "abc",
++ DigraphEntry {
++ symbols: "testbug".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ dg.insert(
++ "abd",
++ DigraphEntry {
++ symbols: "deadbeef".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++ dg.insert(
++ "azz",
++ DigraphEntry {
++ symbols: "qwerty".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ assert_eq!(dg.search("ab").count(), 2);
++ assert_eq!(dg.search("az").next().unwrap().symbols, "qwerty");
++ }
++
++ #[test]
++ fn digraph_search_breadth() {
++ let mut dg = DigraphStore::default();
++ dg.insert(
++ "abccccc",
++ DigraphEntry {
++ symbols: "testbug".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ dg.insert(
++ "abd",
++ DigraphEntry {
++ symbols: "deadbeef".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++ dg.insert(
++ "abee",
++ DigraphEntry {
++ symbols: "qwerty".into(),
++ ..Default::default()
++ },
++ )
++ .unwrap();
++
++ assert_eq!(dg.search("ab").count(), 3);
++ assert_eq!(dg.search("ab").next().unwrap().symbols, "deadbeef");
++ }
++}
+diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
+index 543c7bde..5fce5446 100644
+--- a/helix-view/src/editor.rs
++++ b/helix-view/src/editor.rs
+@@ -1,6 +1,7 @@
+ use crate::{
+ align_view,
+ clipboard::{get_clipboard_provider, ClipboardProvider},
++ digraph::DigraphStore,
+ document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
+ graphics::{CursorKind, Rect},
+ info::Info,
+@@ -318,6 +319,8 @@ pub struct Config {
+ pub default_line_ending: LineEndingConfig,
+ /// Whether to render rainbow highlights. Defaults to `false`.
+ pub rainbow_brackets: bool,
++ /// User supplied digraphs for use with the `insert_diagraphs` command
++ pub digraphs: DigraphStore,
+ }
+
+ #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+@@ -864,6 +867,7 @@ fn default() -> Self {
+ workspace_lsp_roots: Vec::new(),
+ default_line_ending: LineEndingConfig::default(),
+ rainbow_brackets: false,
++ digraphs: Default::default(),
+ }
+ }
+ }
+diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
+index c3f67345..0eabafa5 100644
+--- a/helix-view/src/lib.rs
++++ b/helix-view/src/lib.rs
+@@ -12,6 +12,7 @@ pub mod handlers {
+ pub mod lsp;
+ }
+ pub mod base64;
++pub mod digraph;
+ pub mod info;
+ pub mod input;
+ pub mod keyboard;
+--
+2.41.0
+
diff --git a/0011-Add-file-explorer-and-tree-helper.patch b/0011-Add-file-explorer-and-tree-helper.patch
new file mode 100644
index 00000000..15cf1743
--- /dev/null
+++ b/0011-Add-file-explorer-and-tree-helper.patch
@@ -0,0 +1,4729 @@
+From b03e6400a70c691720044c477968b89239ee5f89 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 18:46:53 -0700
+Subject: [PATCH 1/2] Add file explorer and tree helper
+
+ref: https://github.com/helix-editor/helix/pull/5768
+---
+ book/src/configuration.md | 9 +
+ book/src/keymap.md | 5 +
+ helix-term/src/commands.rs | 45 ++
+ helix-term/src/keymap/default.rs | 1 +
+ helix-term/src/ui/editor.rs | 70 +-
+ helix-term/src/ui/explorer.rs | 751 +++++++++++++++++++
+ helix-term/src/ui/mod.rs | 4 +
+ helix-term/src/ui/overlay.rs | 21 +-
+ helix-term/src/ui/prompt.rs | 4 +
+ helix-term/src/ui/tree.rs | 1209 ++++++++++++++++++++++++++++++
+ helix-view/src/editor.rs | 39 +
+ helix-view/src/graphics.rs | 28 +
+ 12 files changed, 2159 insertions(+), 27 deletions(-)
+ create mode 100644 helix-term/src/ui/explorer.rs
+ create mode 100644 helix-term/src/ui/tree.rs
+
+diff --git a/book/src/configuration.md b/book/src/configuration.md
+index b69bb486..bed20b28 100644
+--- a/book/src/configuration.md
++++ b/book/src/configuration.md
+@@ -348,3 +348,12 @@ ### `[editor.soft-wrap]` Section
+ max-indent-retain = 0
+ wrap-indicator = "" # set wrap-indicator to "" to hide it
+ ```
++
++### `[editor.explorer]` Section
++
++Sets explorer side width and style.
++
++| Key | Description | Default |
++| -------------- | ------------------------------------------- | ------- |
++| `column-width` | explorer side width | 30 |
++| `position` | explorer widget position, `left` or `right` | `left` |
+diff --git a/book/src/keymap.md b/book/src/keymap.md
+index 153f3b64..4e6e878d 100644
+--- a/book/src/keymap.md
++++ b/book/src/keymap.md
+@@ -296,6 +296,7 @@ #### Space mode
+ | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
+ | `/` | Global search in workspace folder | `global_search` |
+ | `?` | Open command palette | `command_palette` |
++| `e` | Reveal current file in explorer | `reveal_current_file` |
+
+ > 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
+
+@@ -451,3 +452,7 @@ ## Prompt
+ | `Tab` | Select next completion item |
+ | `BackTab` | Select previous completion item |
+ | `Enter` | Open selected |
++
++## File explorer
++
++Press `?` to see keymaps. Remapping currently not supported.
+diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
+index dc8b5b5c..edeb419b 100644
+--- a/helix-term/src/commands.rs
++++ b/helix-term/src/commands.rs
+@@ -488,6 +488,8 @@ pub fn doc(&self) -> &str {
+ record_macro, "Record macro",
+ replay_macro, "Replay macro",
+ command_palette, "Open command palette",
++ open_or_focus_explorer, "Open or focus explorer",
++ reveal_current_file, "Reveal current file in explorer",
+ );
+ }
+
+@@ -2580,6 +2582,49 @@ fn file_picker_in_current_directory(cx: &mut Context) {
+ cx.push_layer(Box::new(overlaid(picker)));
+ }
+
++fn open_or_focus_explorer(cx: &mut Context) {
++ cx.callback = Some(Box::new(
++ |compositor: &mut Compositor, cx: &mut compositor::Context| {
++ if let Some(editor) = compositor.find::<ui::EditorView>() {
++ match editor.explorer.as_mut() {
++ Some(explore) => explore.focus(),
++ None => match ui::Explorer::new(cx) {
++ Ok(explore) => editor.explorer = Some(explore),
++ Err(err) => cx.editor.set_error(format!("{}", err)),
++ },
++ }
++ }
++ },
++ ));
++}
++
++fn reveal_file_in_explorer(cx: &mut Context, path: Option<PathBuf>) {
++ cx.callback = Some(Box::new(
++ |compositor: &mut Compositor, cx: &mut compositor::Context| {
++ if let Some(editor) = compositor.find::<ui::EditorView>() {
++ (|| match editor.explorer.as_mut() {
++ Some(explorer) => match path {
++ Some(path) => explorer.reveal_file(path),
++ None => explorer.reveal_current_file(cx),
++ },
++ None => {
++ editor.explorer = Some(ui::Explorer::new(cx)?);
++ if let Some(explorer) = editor.explorer.as_mut() {
++ explorer.reveal_current_file(cx)?;
++ }
++ Ok(())
++ }
++ })()
++ .unwrap_or_else(|err| cx.editor.set_error(err.to_string()))
++ }
++ },
++ ));
++}
++
++fn reveal_current_file(cx: &mut Context) {
++ reveal_file_in_explorer(cx, None)
++}
++
+ fn buffer_picker(cx: &mut Context) {
+ let current = view!(cx.editor).doc;
+
+diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
+index f384c868..43d101fe 100644
+--- a/helix-term/src/keymap/default.rs
++++ b/helix-term/src/keymap/default.rs
+@@ -275,6 +275,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
+ "r" => rename_symbol,
+ "h" => select_references_to_symbol_under_cursor,
+ "?" => command_palette,
++ "e" => reveal_current_file,
+ },
+ "z" => { "View"
+ "z" | "c" => align_view_center,
+diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
+index 252aff0d..81f8fe22 100644
+--- a/helix-term/src/ui/editor.rs
++++ b/helix-term/src/ui/editor.rs
+@@ -6,7 +6,7 @@
+ keymap::{KeymapResult, Keymaps},
+ ui::{
+ document::{render_document, LinePos, TextRenderer, TranslatedPosition},
+- Completion, ProgressSpinners,
++ Completion, Explorer, ProgressSpinners,
+ },
+ };
+
+@@ -23,7 +23,7 @@
+ };
+ use helix_view::{
+ document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
+- editor::{CompleteAction, CursorShapeConfig},
++ editor::{CompleteAction, CursorShapeConfig, ExplorerPosition},
+ graphics::{Color, CursorKind, Modifier, Rect, Style},
+ input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
+ keyboard::{KeyCode, KeyModifiers},
+@@ -43,6 +43,7 @@ pub struct EditorView {
+ pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
+ pub(crate) completion: Option<Completion>,
+ spinners: ProgressSpinners,
++ pub(crate) explorer: Option<Explorer>,
+ }
+
+ #[derive(Debug, Clone)]
+@@ -71,6 +72,7 @@ pub fn new(keymaps: Keymaps) -> Self {
+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
+ completion: None,
+ spinners: ProgressSpinners::default(),
++ explorer: None,
+ }
+ }
+
+@@ -1224,6 +1226,11 @@ fn handle_event(
+ event: &Event,
+ context: &mut crate::compositor::Context,
+ ) -> EventResult {
++ if let Some(explore) = self.explorer.as_mut() {
++ if let EventResult::Consumed(callback) = explore.handle_event(event, context) {
++ return EventResult::Consumed(callback);
++ }
++ }
+ let mut cx = commands::Context {
+ editor: context.editor,
+ count: None,
+@@ -1380,6 +1387,8 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ surface.set_style(area, cx.editor.theme.get("ui.background"));
+ let config = cx.editor.config();
+
++ let editor_area = area.clip_bottom(1);
++
+ // check if bufferline should be rendered
+ use helix_view::editor::BufferLine;
+ let use_bufferline = match config.bufferline {
+@@ -1388,15 +1397,43 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ _ => false,
+ };
+
+- // -1 for commandline and -1 for bufferline
+- let mut editor_area = area.clip_bottom(1);
+- if use_bufferline {
+- editor_area = editor_area.clip_top(1);
+- }
++ let editor_area = if use_bufferline {
++ editor_area.clip_top(1)
++ } else {
++ editor_area
++ };
++
++ let editor_area = if let Some(explorer) = &self.explorer {
++ let explorer_column_width = if explorer.is_opened() {
++ explorer.column_width().saturating_add(2)
++ } else {
++ 0
++ };
++ // For future developer:
++ // We should have a Dock trait that allows a component to dock to the top/left/bottom/right
++ // of another component.
++ match config.explorer.position {
++ ExplorerPosition::Left => editor_area.clip_left(explorer_column_width),
++ ExplorerPosition::Right => editor_area.clip_right(explorer_column_width),
++ }
++ } else {
++ editor_area
++ };
+
+ // if the terminal size suddenly changed, we need to trigger a resize
+ cx.editor.resize(editor_area);
+
++ if let Some(explorer) = self.explorer.as_mut() {
++ if !explorer.is_focus() {
++ let area = if use_bufferline {
++ area.clip_top(1)
++ } else {
++ area
++ };
++ explorer.render(area, surface, cx);
++ }
++ }
++
+ if use_bufferline {
+ Self::render_bufferline(cx.editor, area.with_height(1), surface);
+ }
+@@ -1475,9 +1512,28 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ if let Some(completion) = self.completion.as_mut() {
+ completion.render(area, surface, cx);
+ }
++
++ if let Some(explore) = self.explorer.as_mut() {
++ if explore.is_focus() {
++ let area = if use_bufferline {
++ area.clip_top(1)
++ } else {
++ area
++ };
++ explore.render(area, surface, cx);
++ }
++ }
+ }
+
+ fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
++ if let Some(explore) = &self.explorer {
++ if explore.is_focus() {
++ let cursor = explore.cursor(_area, editor);
++ if cursor.0.is_some() {
++ return cursor;
++ }
++ }
++ }
+ match editor.cursor() {
+ // All block cursors are drawn manually
+ (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
+diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs
+new file mode 100644
+index 00000000..6df059b1
+--- /dev/null
++++ b/helix-term/src/ui/explorer.rs
+@@ -0,0 +1,751 @@
++use super::{Prompt, TreeOp, TreeView, TreeViewItem};
++use crate::{
++ compositor::{Component, Context, EventResult},
++ ctrl, key, shift, ui,
++};
++use anyhow::{bail, ensure, Result};
++use helix_core::Position;
++use helix_view::{
++ editor::{Action, ExplorerPosition},
++ graphics::{CursorKind, Rect},
++ info::Info,
++ input::{Event, KeyEvent},
++ theme::Modifier,
++ Editor,
++};
++use std::cmp::Ordering;
++use std::path::{Path, PathBuf};
++use std::{borrow::Cow, fs::DirEntry};
++use tui::{
++ buffer::Buffer as Surface,
++ widgets::{Block, Borders, Widget},
++};
++
++#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
++enum FileType {
++ File,
++ Folder,
++ Root,
++}
++
++#[derive(PartialEq, Eq, Debug, Clone)]
++struct FileInfo {
++ file_type: FileType,
++ path: PathBuf,
++}
++
++impl FileInfo {
++ fn root(path: PathBuf) -> Self {
++ Self {
++ file_type: FileType::Root,
++ path,
++ }
++ }
++
++ fn get_text(&self) -> Cow<'static, str> {
++ let text = match self.file_type {
++ FileType::Root => self.path.display().to_string(),
++ FileType::File | FileType::Folder => self
++ .path
++ .file_name()
++ .map_or("/".into(), |p| p.to_string_lossy().into_owned()),
++ };
++
++ #[cfg(test)]
++ let text = text.replace(std::path::MAIN_SEPARATOR, "/");
++
++ text.into()
++ }
++}
++
++impl PartialOrd for FileInfo {
++ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
++ Some(self.cmp(other))
++ }
++}
++
++impl Ord for FileInfo {
++ fn cmp(&self, other: &Self) -> Ordering {
++ use FileType::*;
++ match (self.file_type, other.file_type) {
++ (Root, _) => return Ordering::Less,
++ (_, Root) => return Ordering::Greater,
++ _ => {}
++ };
++
++ if let (Some(p1), Some(p2)) = (self.path.parent(), other.path.parent()) {
++ if p1 == p2 {
++ match (self.file_type, other.file_type) {
++ (Folder, File) => return Ordering::Less,
++ (File, Folder) => return Ordering::Greater,
++ _ => {}
++ };
++ }
++ }
++ self.path.cmp(&other.path)
++ }
++}
++
++impl TreeViewItem for FileInfo {
++ type Params = State;
++
++ fn get_children(&self) -> Result<Vec<Self>> {
++ match self.file_type {
++ FileType::Root | FileType::Folder => {}
++ _ => return Ok(vec![]),
++ };
++ let ret: Vec<_> = std::fs::read_dir(&self.path)?
++ .filter_map(|entry| entry.ok())
++ .filter_map(|entry| dir_entry_to_file_info(entry, &self.path))
++ .collect();
++ Ok(ret)
++ }
++
++ fn name(&self) -> String {
++ self.get_text().to_string()
++ }
++
++ fn is_parent(&self) -> bool {
++ matches!(self.file_type, FileType::Folder | FileType::Root)
++ }
++}
++
++fn dir_entry_to_file_info(entry: DirEntry, path: &Path) -> Option<FileInfo> {
++ entry.metadata().ok().map(|meta| {
++ let file_type = match meta.is_dir() {
++ true => FileType::Folder,
++ false => FileType::File,
++ };
++ FileInfo {
++ file_type,
++ path: path.join(entry.file_name()),
++ }
++ })
++}
++
++#[derive(Clone, Debug)]
++enum PromptAction {
++ CreateFileOrFolder,
++ RemoveFolder,
++ RemoveFile,
++ RenameFile,
++}
++
++#[derive(Clone, Debug, Default)]
++struct State {
++ focus: bool,
++ open: bool,
++ current_root: PathBuf,
++ area_width: u16,
++}
++
++impl State {
++ fn new(focus: bool, current_root: PathBuf) -> Self {
++ Self {
++ focus,
++ current_root,
++ open: true,
++ area_width: 0,
++ }
++ }
++}
++
++struct ExplorerHistory {
++ tree: TreeView<FileInfo>,
++ current_root: PathBuf,
++}
++
++pub struct Explorer {
++ tree: TreeView<FileInfo>,
++ history: Vec<ExplorerHistory>,
++ show_help: bool,
++ state: State,
++ prompt: Option<(PromptAction, Prompt)>,
++ #[allow(clippy::type_complexity)]
++ on_next_key: Option<Box<dyn FnMut(&mut Context, &mut Self, &KeyEvent) -> EventResult>>,
++ column_width: u16,
++}
++
++impl Explorer {
++ pub fn new(cx: &mut Context) -> Result<Self> {
++ let current_root = std::env::current_dir()
++ .unwrap_or_else(|_| "./".into())
++ .canonicalize()?;
++ Ok(Self {
++ tree: Self::new_tree_view(current_root.clone())?,
++ history: vec![],
++ show_help: false,
++ state: State::new(true, current_root),
++ prompt: None,
++ on_next_key: None,
++ column_width: cx.editor.config().explorer.column_width as u16,
++ })
++ }
++
++ #[cfg(test)]
++ fn from_path(root: PathBuf, column_width: u16) -> Result<Self> {
++ Ok(Self {
++ tree: Self::new_tree_view(root.clone())?,
++ history: vec![],
++ show_help: false,
++ state: State::new(true, root),
++ prompt: None,
++ on_next_key: None,
++ column_width,
++ })
++ }
++
++ fn new_tree_view(root: PathBuf) -> Result<TreeView<FileInfo>> {
++ let root = FileInfo::root(root);
++ Ok(TreeView::build_tree(root)?.with_enter_fn(Self::toggle_current))
++ }
++
++ fn push_history(&mut self, tree_view: TreeView<FileInfo>, current_root: PathBuf) {
++ self.history.push(ExplorerHistory {
++ tree: tree_view,
++ current_root,
++ });
++ const MAX_HISTORY_SIZE: usize = 20;
++ Vec::truncate(&mut self.history, MAX_HISTORY_SIZE)
++ }
++
++ fn change_root(&mut self, root: PathBuf) -> Result<()> {
++ if self.state.current_root.eq(&root) {
++ return Ok(());
++ }
++ let tree = Self::new_tree_view(root.clone())?;
++ let old_tree = std::mem::replace(&mut self.tree, tree);
++ self.push_history(old_tree, self.state.current_root.clone());
++ self.state.current_root = root;
++ Ok(())
++ }
++
++ pub fn reveal_file(&mut self, path: PathBuf) -> Result<()> {
++ let current_root = &self.state.current_root.canonicalize()?;
++ let current_path = &path.canonicalize()?;
++ let segments = {
++ let stripped = match current_path.strip_prefix(current_root) {
++ Ok(stripped) => Ok(stripped),
++ Err(_) => {
++ let parent = path.parent().ok_or_else(|| {
++ anyhow::anyhow!("Failed get parent of '{}'", current_path.to_string_lossy())
++ })?;
++ self.change_root(parent.into())?;
++ current_path
++ .strip_prefix(parent.canonicalize()?)
++ .map_err(|_| {
++ anyhow::anyhow!(
++ "Failed to strip prefix (parent) '{}' from '{}'",
++ parent.to_string_lossy(),
++ current_path.to_string_lossy()
++ )
++ })
++ }
++ }?;
++
++ stripped
++ .components()
++ .map(|c| c.as_os_str().to_string_lossy().to_string())
++ .collect::<Vec<_>>()
++ };
++ self.tree.reveal_item(segments)?;
++ Ok(())
++ }
++
++ pub fn reveal_current_file(&mut self, cx: &mut Context) -> Result<()> {
++ self.focus();
++ let current_document_path = doc!(cx.editor).path().cloned();
++ match current_document_path {
++ None => Ok(()),
++ Some(current_path) => self.reveal_file(current_path),
++ }
++ }
++
++ pub fn focus(&mut self) {
++ self.state.focus = true;
++ self.state.open = true;
++ }
++
++ fn unfocus(&mut self) {
++ self.state.focus = false;
++ }
++
++ fn close(&mut self) {
++ self.state.focus = false;
++ self.state.open = false;
++ }
++
++ pub fn is_focus(&self) -> bool {
++ self.state.focus
++ }
++
++ fn new_create_file_or_folder_prompt(&mut self, cx: &mut Context) -> Result<()> {
++ let folder_path = self.nearest_folder()?;
++ self.prompt = Some((
++ PromptAction::CreateFileOrFolder,
++ Prompt::new(
++ format!(
++ " New file or folder (ends with '{}'): ",
++ std::path::MAIN_SEPARATOR
++ )
++ .into(),
++ None,
++ ui::completers::none,
++ |_, _, _| {},
++ )
++ .with_line(format!("{}/", folder_path.to_string_lossy()), cx.editor),
++ ));
++ Ok(())
++ }
++
++ fn nearest_folder(&self) -> Result<PathBuf> {
++ let current = self.tree.current()?.item();
++ if current.is_parent() {
++ Ok(current.path.to_path_buf())
++ } else {
++ let parent_path = current.path.parent().ok_or_else(|| {
++ anyhow::anyhow!(format!(
++ "Unable to get parent path of '{}'",
++ current.path.to_string_lossy()
++ ))
++ })?;
++ Ok(parent_path.to_path_buf())
++ }
++ }
++
++ fn new_remove_prompt(&mut self) -> Result<()> {
++ let item = self.tree.current()?.item();
++ match item.file_type {
++ FileType::Folder => self.new_remove_folder_prompt(),
++ FileType::File => self.new_remove_file_prompt(),
++ FileType::Root => bail!("Root is not removable"),
++ }
++ }
++
++ fn new_rename_prompt(&mut self, cx: &mut Context) -> Result<()> {
++ let path = self.tree.current_item()?.path.clone();
++ self.prompt = Some((
++ PromptAction::RenameFile,
++ Prompt::new(
++ " Rename to ".into(),
++ None,
++ ui::completers::none,
++ |_, _, _| {},
++ )
++ .with_line(path.to_string_lossy().to_string(), cx.editor),
++ ));
++ Ok(())
++ }
++
++ fn new_remove_file_prompt(&mut self) -> Result<()> {
++ let item = self.tree.current_item()?;
++ ensure!(
++ item.path.is_file(),
++ "The path '{}' is not a file",
++ item.path.to_string_lossy()
++ );
++ self.prompt = Some((
++ PromptAction::RemoveFile,
++ Prompt::new(
++ format!(" Delete file: '{}'? y/N: ", item.path.display()).into(),
++ None,
++ ui::completers::none,
++ |_, _, _| {},
++ ),
++ ));
++ Ok(())
++ }
++
++ fn new_remove_folder_prompt(&mut self) -> Result<()> {
++ let item = self.tree.current_item()?;
++ ensure!(
++ item.path.is_dir(),
++ "The path '{}' is not a folder",
++ item.path.to_string_lossy()
++ );
++
++ self.prompt = Some((
++ PromptAction::RemoveFolder,
++ Prompt::new(
++ format!(" Delete folder: '{}'? y/N: ", item.path.display()).into(),
++ None,
++ ui::completers::none,
++ |_, _, _| {},
++ ),
++ ));
++ Ok(())
++ }
++
++ fn toggle_current(item: &mut FileInfo, cx: &mut Context, state: &mut State) -> TreeOp {
++ (|| -> Result<TreeOp> {
++ if item.path == Path::new("") {
++ return Ok(TreeOp::Noop);
++ }
++ let meta = std::fs::metadata(&item.path)?;
++ if meta.is_file() {
++ cx.editor.open(&item.path, Action::Replace)?;
++ state.focus = false;
++ return Ok(TreeOp::Noop);
++ }
++
++ if item.path.is_dir() {
++ return Ok(TreeOp::GetChildsAndInsert);
++ }
++
++ Err(anyhow::anyhow!("Unknown file type: {:?}", meta.file_type()))
++ })()
++ .unwrap_or_else(|err| {
++ cx.editor.set_error(format!("{err}"));
++ TreeOp::Noop
++ })
++ }
++
++ fn render_tree(
++ &mut self,
++ area: Rect,
++ prompt_area: Rect,
++ surface: &mut Surface,
++ cx: &mut Context,
++ ) {
++ self.tree.render(area, prompt_area, surface, cx);
++ }
++
++ fn render_embed(
++ &mut self,
++ area: Rect,
++ surface: &mut Surface,
++ cx: &mut Context,
++ position: &ExplorerPosition,
++ ) {
++ if !self.state.open {
++ return;
++ }
++ let width = area.width.min(self.column_width + 2);
++
++ self.state.area_width = area.width;
++
++ let side_area = match position {
++ ExplorerPosition::Left => Rect { width, ..area },
++ ExplorerPosition::Right => Rect {
++ x: area.width - width,
++ width,
++ ..area
++ },
++ }
++ .clip_bottom(1);
++ let background = cx.editor.theme.get("ui.background");
++ surface.clear_with(side_area, background);
++
++ let prompt_area = area.clip_top(side_area.height);
++
++ let list_area = match position {
++ ExplorerPosition::Left => {
++ render_block(side_area.clip_left(1), surface, Borders::RIGHT).clip_bottom(1)
++ }
++ ExplorerPosition::Right => {
++ render_block(side_area.clip_right(1), surface, Borders::LEFT).clip_bottom(1)
++ }
++ };
++ self.render_tree(list_area, prompt_area, surface, cx);
++
++ {
++ let statusline = if self.is_focus() {
++ cx.editor.theme.get("ui.statusline")
++ } else {
++ cx.editor.theme.get("ui.statusline.inactive")
++ };
++ let area = side_area.clip_top(list_area.height);
++ let area = match position {
++ ExplorerPosition::Left => area.clip_right(1),
++ ExplorerPosition::Right => area.clip_left(1),
++ };
++ surface.clear_with(area, statusline);
++
++ let title_style = cx.editor.theme.get("ui.text");
++ let title_style = if self.is_focus() {
++ title_style.add_modifier(Modifier::BOLD)
++ } else {
++ title_style
++ };
++ surface.set_stringn(
++ area.x,
++ area.y,
++ if self.is_focus() {
++ " EXPLORER: press ? for help"
++ } else {
++ " EXPLORER"
++ },
++ area.width.into(),
++ title_style,
++ );
++ }
++
++ if self.is_focus() && self.show_help {
++ let help_area = match position {
++ ExplorerPosition::Left => area,
++ ExplorerPosition::Right => area.clip_right(list_area.width.saturating_add(2)),
++ };
++ self.render_help(help_area, surface, cx);
++ }
++
++ if let Some((_, prompt)) = self.prompt.as_mut() {
++ prompt.render_prompt(prompt_area, surface, cx)
++ }
++ }
++
++ fn render_help(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
++ Info::new(
++ "Explorer",
++ &[
++ ("?", "Toggle help"),
++ ("a", "Add file/folder"),
++ ("r", "Rename file/folder"),
++ ("d", "Delete file"),
++ ("B", "Change root to parent folder"),
++ ("]", "Change root to current folder"),
++ ("[", "Go to previous root"),
++ ("+, =", "Increase size"),
++ ("-, _", "Decrease size"),
++ ("q", "Close"),
++ ]
++ .into_iter()
++ .chain(ui::tree::tree_view_help().into_iter())
++ .collect::<Vec<_>>(),
++ )
++ .render(area, surface, cx)
++ }
++
++ fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
++ let result = (|| -> Result<EventResult> {
++ let (action, mut prompt) = match self.prompt.take() {
++ Some((action, p)) => (action, p),
++ _ => return Ok(EventResult::Ignored(None)),
++ };
++ let line = prompt.line();
++
++ let current_item_path = self.tree.current_item()?.path.clone();
++ match (&action, event) {
++ (PromptAction::CreateFileOrFolder, key!(Enter)) => {
++ if line.ends_with(std::path::MAIN_SEPARATOR) {
++ self.new_folder(line)?
++ } else {
++ self.new_file(line)?
++ }
++ }
++ (PromptAction::RemoveFolder, key) => {
++ if let key!('y') = key {
++ close_documents(current_item_path, cx)?;
++ self.remove_folder()?;
++ }
++ }
++ (PromptAction::RemoveFile, key) => {
++ if let key!('y') = key {
++ close_documents(current_item_path, cx)?;
++ self.remove_file()?;
++ }
++ }
++ (PromptAction::RenameFile, key!(Enter)) => {
++ close_documents(current_item_path, cx)?;
++ self.rename_current(line)?;
++ }
++ (_, key!(Esc) | ctrl!('c')) => {}
++ _ => {
++ prompt.handle_event(&Event::Key(*event), cx);
++ self.prompt = Some((action, prompt));
++ }
++ }
++ Ok(EventResult::Consumed(None))
++ })();
++ match result {
++ Ok(event_result) => event_result,
++ Err(err) => {
++ cx.editor.set_error(err.to_string());
++ EventResult::Consumed(None)
++ }
++ }
++ }
++
++ fn new_file(&mut self, path: &str) -> Result<()> {
++ let path = helix_core::path::get_normalized_path(&PathBuf::from(path));
++ if let Some(parent) = path.parent() {
++ std::fs::create_dir_all(parent)?;
++ }
++ let mut fd = std::fs::OpenOptions::new();
++ fd.create_new(true).write(true).open(&path)?;
++ self.tree.refresh()?;
++ self.reveal_file(path)
++ }
++
++ fn new_folder(&mut self, path: &str) -> Result<()> {
++ let path = helix_core::path::get_normalized_path(&PathBuf::from(path));
++ std::fs::create_dir_all(&path)?;
++ self.tree.refresh()?;
++ self.reveal_file(path)
++ }
++
++ fn toggle_help(&mut self) {
++ self.show_help = !self.show_help
++ }
++
++ fn go_to_previous_root(&mut self) {
++ if let Some(history) = self.history.pop() {
++ self.tree = history.tree;
++ self.state.current_root = history.current_root
++ }
++ }
++
++ fn change_root_to_current_folder(&mut self) -> Result<()> {
++ self.change_root(self.tree.current_item()?.path.clone())
++ }
++
++ fn change_root_parent_folder(&mut self) -> Result<()> {
++ if let Some(parent) = self.state.current_root.parent() {
++ let path = parent.to_path_buf();
++ self.change_root(path)
++ } else {
++ Ok(())
++ }
++ }
++
++ pub fn is_opened(&self) -> bool {
++ self.state.open
++ }
++
++ pub fn column_width(&self) -> u16 {
++ self.column_width
++ }
++
++ fn increase_size(&mut self) {
++ const EDITOR_MIN_WIDTH: u16 = 10;
++ self.column_width = std::cmp::min(
++ self.state.area_width.saturating_sub(EDITOR_MIN_WIDTH),
++ self.column_width.saturating_add(1),
++ )
++ }
++
++ fn decrease_size(&mut self) {
++ self.column_width = self.column_width.saturating_sub(1)
++ }
++
++ fn rename_current(&mut self, line: &String) -> Result<()> {
++ let item = self.tree.current_item()?;
++ let path = PathBuf::from(line);
++ if let Some(parent) = path.parent() {
++ std::fs::create_dir_all(parent)?;
++ }
++ std::fs::rename(&item.path, &path)?;
++ self.tree.refresh()?;
++ self.reveal_file(path)
++ }
++
++ fn remove_folder(&mut self) -> Result<()> {
++ let item = self.tree.current_item()?;
++ std::fs::remove_dir_all(&item.path)?;
++ self.tree.refresh()
++ }
++
++ fn remove_file(&mut self) -> Result<()> {
++ let item = self.tree.current_item()?;
++ std::fs::remove_file(&item.path)?;
++ self.tree.refresh()
++ }
++}
++
++fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> {
++ let ids = cx
++ .editor
++ .documents
++ .iter()
++ .filter_map(|(id, doc)| {
++ if doc.path()?.starts_with(&current_item_path) {
++ Some(*id)
++ } else {
++ None
++ }
++ })
++ .collect::<Vec<_>>();
++
++ for id in ids {
++ cx.editor.close_document(id, true)?;
++ }
++ Ok(())
++}
++
++impl Component for Explorer {
++ /// Process input events, return true if handled.
++ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
++ if self.tree.prompting() {
++ return self.tree.handle_event(event, cx, &mut self.state);
++ }
++ let key_event = match event {
++ Event::Key(event) => event,
++ Event::Resize(..) => return EventResult::Consumed(None),
++ _ => return EventResult::Ignored(None),
++ };
++ if !self.is_focus() {
++ return EventResult::Ignored(None);
++ }
++ if let Some(mut on_next_key) = self.on_next_key.take() {
++ return on_next_key(cx, self, key_event);
++ }
++
++ if let EventResult::Consumed(c) = self.handle_prompt_event(key_event, cx) {
++ return EventResult::Consumed(c);
++ }
++
++ (|| -> Result<()> {
++ match key_event {
++ key!(Esc) => self.unfocus(),
++ key!('q') => self.close(),
++ key!('?') => self.toggle_help(),
++ key!('a') => self.new_create_file_or_folder_prompt(cx)?,
++ shift!('B') => self.change_root_parent_folder()?,
++ key!(']') => self.change_root_to_current_folder()?,
++ key!('[') => self.go_to_previous_root(),
++ key!('d') => self.new_remove_prompt()?,
++ key!('r') => self.new_rename_prompt(cx)?,
++ key!('-') | key!('_') => self.decrease_size(),
++ key!('+') | key!('=') => self.increase_size(),
++ _ => {
++ self.tree
++ .handle_event(&Event::Key(*key_event), cx, &mut self.state);
++ }
++ };
++ Ok(())
++ })()
++ .unwrap_or_else(|err| cx.editor.set_error(format!("{err}")));
++
++ EventResult::Consumed(None)
++ }
++
++ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
++ if area.width < 10 || area.height < 5 {
++ cx.editor.set_error("explorer render area is too small");
++ return;
++ }
++ let config = &cx.editor.config().explorer;
++ let position = config.position;
++ self.render_embed(area, surface, cx, &position);
++ }
++
++ fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
++ if let Some(prompt) = self
++ .prompt
++ .as_ref()
++ .map(|(_, prompt)| prompt)
++ .or_else(|| self.tree.prompt())
++ {
++ let (x, y) = (area.x, area.y + area.height.saturating_sub(1));
++ prompt.cursor(Rect::new(x, y, area.width, 1), editor)
++ } else {
++ (None, CursorKind::Hidden)
++ }
++ }
++}
++
++fn render_block(area: Rect, surface: &mut Surface, borders: Borders) -> Rect {
++ let block = Block::default().borders(borders);
++ let inner = block.inner(area);
++ block.render(area, surface);
++ inner
++}
+diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
+index 3359155d..eb6464f9 100644
+--- a/helix-term/src/ui/mod.rs
++++ b/helix-term/src/ui/mod.rs
+@@ -1,6 +1,7 @@
+ mod completion;
+ mod document;
+ pub(crate) mod editor;
++mod explorer;
+ mod fuzzy_match;
+ mod info;
+ pub mod lsp;
+@@ -13,12 +14,14 @@
+ mod spinner;
+ mod statusline;
+ mod text;
++mod tree;
+
+ use crate::compositor::{Component, Compositor};
+ use crate::filter_picker_entry;
+ use crate::job::{self, Callback};
+ pub use completion::{Completion, CompletionItem};
+ pub use editor::EditorView;
++pub use explorer::Explorer;
+ pub use markdown::Markdown;
+ pub use menu::Menu;
+ pub use picker::{DynamicPicker, FileLocation, Picker};
+@@ -26,6 +29,7 @@
+ pub use prompt::{Prompt, PromptEvent};
+ pub use spinner::{ProgressSpinners, Spinner};
+ pub use text::Text;
++pub use tree::{TreeOp, TreeView, TreeViewItem};
+
+ use helix_core::regex::Regex;
+ use helix_core::regex::RegexBuilder;
+diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs
+index ff184d40..7b9c0391 100644
+--- a/helix-term/src/ui/overlay.rs
++++ b/helix-term/src/ui/overlay.rs
+@@ -19,26 +19,7 @@ pub struct Overlay<T> {
+ pub fn overlaid<T>(content: T) -> Overlay<T> {
+ Overlay {
+ content,
+- calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)),
+- }
+-}
+-
+-fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect {
+- fn mul_and_cast(size: u16, factor: u8) -> u16 {
+- ((size as u32) * (factor as u32) / 100).try_into().unwrap()
+- }
+-
+- let inner_w = mul_and_cast(rect.width, percent_horizontal);
+- let inner_h = mul_and_cast(rect.height, percent_vertical);
+-
+- let offset_x = rect.width.saturating_sub(inner_w) / 2;
+- let offset_y = rect.height.saturating_sub(inner_h) / 2;
+-
+- Rect {
+- x: rect.x + offset_x,
+- y: rect.y + offset_y,
+- width: inner_w,
+- height: inner_h,
++ calc_child_size: Box::new(|rect: Rect| rect.overlaid()),
+ }
+ }
+
+diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
+index a9ccfb73..e033112d 100644
+--- a/helix-term/src/ui/prompt.rs
++++ b/helix-term/src/ui/prompt.rs
+@@ -94,6 +94,10 @@ pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
+ self
+ }
+
++ pub fn prompt(&self) -> &str {
++ &self.prompt.as_ref()
++ }
++
+ pub fn line(&self) -> &String {
+ &self.line
+ }
+diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs
+new file mode 100644
+index 00000000..68783be8
+--- /dev/null
++++ b/helix-term/src/ui/tree.rs
+@@ -0,0 +1,1209 @@
++use std::cmp::Ordering;
++
++use anyhow::Result;
++use helix_view::theme::Modifier;
++
++use crate::{
++ compositor::{Component, Context, EventResult},
++ ctrl, key, shift, ui,
++};
++use helix_core::movement::Direction;
++use helix_view::{
++ graphics::Rect,
++ input::{Event, KeyEvent},
++};
++use tui::buffer::Buffer as Surface;
++
++use super::Prompt;
++
++pub trait TreeViewItem: Sized + Ord {
++ type Params: Default;
++
++ fn name(&self) -> String;
++ fn is_parent(&self) -> bool;
++
++ fn filter(&self, s: &str) -> bool {
++ self.name().to_lowercase().contains(&s.to_lowercase())
++ }
++
++ fn get_children(&self) -> Result<Vec<Self>>;
++}
++
++fn tree_item_cmp<T: TreeViewItem>(item1: &T, item2: &T) -> Ordering {
++ T::cmp(item1, item2)
++}
++
++fn vec_to_tree<T: TreeViewItem>(mut items: Vec<T>) -> Vec<Tree<T>> {
++ items.sort();
++ index_elems(
++ 0,
++ items
++ .into_iter()
++ .map(|item| Tree::new(item, vec![]))
++ .collect(),
++ )
++}
++
++pub enum TreeOp {
++ Noop,
++ GetChildsAndInsert,
++}
++
++#[derive(Debug, PartialEq, Eq)]
++pub struct Tree<T> {
++ item: T,
++ parent_index: Option<usize>,
++ index: usize,
++ children: Vec<Self>,
++
++ /// Why do we need this property?
++ /// Can't we just use `!children.is_empty()`?
++ ///
++ /// Because we might have for example an open folder that is empty,
++ /// and user just added a new file under that folder,
++ /// and the user refreshes the whole tree.
++ ///
++ /// Without `open`, we will not refresh any node without children,
++ /// and thus the folder still appears empty after refreshing.
++ is_opened: bool,
++}
++
++impl<T: Clone> Clone for Tree<T> {
++ fn clone(&self) -> Self {
++ Self {
++ item: self.item.clone(),
++ index: self.index,
++ children: self.children.clone(),
++ is_opened: self.is_opened,
++ parent_index: self.parent_index,
++ }
++ }
++}
++
++#[derive(Clone)]
++struct TreeIter<'a, T> {
++ current_index_forward: usize,
++ current_index_reverse: isize,
++ tree: &'a Tree<T>,
++}
++
++impl<'a, T> Iterator for TreeIter<'a, T> {
++ type Item = &'a Tree<T>;
++
++ fn next(&mut self) -> Option<Self::Item> {
++ let index = self.current_index_forward;
++ if index > self.tree.len().saturating_sub(1) {
++ None
++ } else {
++ self.current_index_forward = self.current_index_forward.saturating_add(1);
++ self.tree.get(index)
++ }
++ }
++
++ fn size_hint(&self) -> (usize, Option<usize>) {
++ (self.tree.len(), Some(self.tree.len()))
++ }
++}
++
++impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> {
++ fn next_back(&mut self) -> Option<Self::Item> {
++ let index = self.current_index_reverse;
++ if index < 0 {
++ None
++ } else {
++ self.current_index_reverse = self.current_index_reverse.saturating_sub(1);
++ self.tree.get(index as usize)
++ }
++ }
++}
++
++impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {}
++
++impl<T: TreeViewItem> Tree<T> {
++ fn open(&mut self) -> Result<()> {
++ if self.item.is_parent() {
++ self.children = self.get_children()?;
++ self.is_opened = true;
++ }
++ Ok(())
++ }
++
++ fn close(&mut self) {
++ self.is_opened = false;
++ self.children = vec![];
++ }
++
++ fn refresh(&mut self) -> Result<()> {
++ if !self.is_opened {
++ return Ok(());
++ }
++ let latest_children = self.get_children()?;
++ let filtered = std::mem::take(&mut self.children)
++ .into_iter()
++ // Remove children that does not exists in latest_children
++ .filter(|tree| {
++ latest_children
++ .iter()
++ .any(|child| tree.item.name().eq(&child.item.name()))
++ })
++ .map(|mut tree| {
++ tree.refresh()?;
++ Ok(tree)
++ })
++ .collect::<Result<Vec<_>>>()?;
++
++ // Add new children
++ let new_nodes = latest_children
++ .into_iter()
++ .filter(|child| {
++ !filtered
++ .iter()
++ .any(|child_| child.item.name().eq(&child_.item.name()))
++ })
++ .collect::<Vec<_>>();
++
++ self.children = filtered.into_iter().chain(new_nodes).collect();
++
++ self.sort();
++
++ self.regenerate_index();
++
++ Ok(())
++ }
++
++ fn get_children(&self) -> Result<Vec<Tree<T>>> {
++ Ok(vec_to_tree(self.item.get_children()?))
++ }
++
++ fn sort(&mut self) {
++ self.children
++ .sort_by(|a, b| tree_item_cmp(&a.item, &b.item))
++ }
++}
++
++impl<T> Tree<T> {
++ pub fn new(item: T, children: Vec<Tree<T>>) -> Self {
++ let is_opened = !children.is_empty();
++ Self {
++ item,
++ index: 0,
++ parent_index: None,
++ children: index_elems(0, children),
++ is_opened,
++ }
++ }
++
++ fn iter(&self) -> TreeIter<T> {
++ TreeIter {
++ tree: self,
++ current_index_forward: 0,
++ current_index_reverse: (self.len() - 1) as isize,
++ }
++ }
++
++ /// Find an element in the tree with given `predicate`.
++ /// `start_index` is inclusive if direction is `Forward`.
++ /// `start_index` is exclusive if direction is `Backward`.
++ fn find<F>(&self, start_index: usize, direction: Direction, predicate: F) -> Option<usize>
++ where
++ F: Clone + FnMut(&Tree<T>) -> bool,
++ {
++ match direction {
++ Direction::Forward => match self
++ .iter()
++ .skip(start_index)
++ .position(predicate.clone())
++ .map(|index| index + start_index)
++ {
++ Some(index) => Some(index),
++ None => self.iter().position(predicate),
++ },
++
++ Direction::Backward => match self.iter().take(start_index).rposition(predicate.clone())
++ {
++ Some(index) => Some(index),
++ None => self.iter().rposition(predicate),
++ },
++ }
++ }
++
++ pub fn item(&self) -> &T {
++ &self.item
++ }
++
++ fn get(&self, index: usize) -> Option<&Tree<T>> {
++ if self.index == index {
++ Some(self)
++ } else {
++ self.children.iter().find_map(|elem| elem.get(index))
++ }
++ }
++
++ fn get_mut(&mut self, index: usize) -> Option<&mut Tree<T>> {
++ if self.index == index {
++ Some(self)
++ } else {
++ self.children
++ .iter_mut()
++ .find_map(|elem| elem.get_mut(index))
++ }
++ }
++
++ fn len(&self) -> usize {
++ (1_usize).saturating_add(self.children.iter().map(|elem| elem.len()).sum())
++ }
++
++ fn regenerate_index(&mut self) {
++ let items = std::mem::take(&mut self.children);
++ self.children = index_elems(0, items);
++ }
++}
++
++#[derive(Clone, Debug)]
++struct SavedView {
++ selected: usize,
++ winline: usize,
++}
++
++pub struct TreeView<T: TreeViewItem> {
++ tree: Tree<T>,
++
++ search_prompt: Option<(Direction, Prompt)>,
++
++ search_str: String,
++
++ /// Selected item idex
++ selected: usize,
++
++ backward_jumps: Vec<usize>,
++ forward_jumps: Vec<usize>,
++
++ saved_view: Option<SavedView>,
++
++ /// For implementing vertical scroll
++ winline: usize,
++
++ /// For implementing horizontal scoll
++ column: usize,
++
++ /// For implementing horizontal scoll
++ max_len: usize,
++ count: usize,
++ tree_symbol_style: String,
++
++ #[allow(clippy::type_complexity)]
++ pre_render: Option<Box<dyn Fn(&mut Self, Rect) + 'static>>,
++
++ #[allow(clippy::type_complexity)]
++ on_opened_fn: Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static>>,
++
++ #[allow(clippy::type_complexity)]
++ on_folded_fn: Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) + 'static>>,
++
++ #[allow(clippy::type_complexity)]
++ on_next_key: Option<Box<dyn FnMut(&mut Context, &mut Self, &KeyEvent) -> Result<()>>>,
++}
++
++impl<T: TreeViewItem> TreeView<T> {
++ pub fn build_tree(root: T) -> Result<Self> {
++ let children = root.get_children()?;
++ let items = vec_to_tree(children);
++ Ok(Self {
++ tree: Tree::new(root, items),
++ selected: 0,
++ backward_jumps: vec![],
++ forward_jumps: vec![],
++ saved_view: None,
++ winline: 0,
++ column: 0,
++ max_len: 0,
++ count: 0,
++ tree_symbol_style: "ui.text".into(),
++ pre_render: None,
++ on_opened_fn: None,
++ on_folded_fn: None,
++ on_next_key: None,
++ search_prompt: None,
++ search_str: "".into(),
++ })
++ }
++
++ pub fn with_enter_fn<F>(mut self, f: F) -> Self
++ where
++ F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static,
++ {
++ self.on_opened_fn = Some(Box::new(f));
++ self
++ }
++
++ pub fn with_folded_fn<F>(mut self, f: F) -> Self
++ where
++ F: FnMut(&mut T, &mut Context, &mut T::Params) + 'static,
++ {
++ self.on_folded_fn = Some(Box::new(f));
++ self
++ }
++
++ pub fn tree_symbol_style(mut self, style: String) -> Self {
++ self.tree_symbol_style = style;
++ self
++ }
++
++ /// Reveal item in the tree based on the given `segments`.
++ ///
++ /// The name of the root should be excluded.
++ ///
++ /// Example `segments`:
++ ///
++ /// vec!["helix-term", "src", "ui", "tree.rs"]
++ ///
++ pub fn reveal_item(&mut self, segments: Vec<String>) -> Result<()> {
++ // Expand the tree
++ let root = self.tree.item.name();
++ segments.iter().fold(
++ Ok(&mut self.tree),
++ |current_tree, segment| match current_tree {
++ Err(err) => Err(err),
++ Ok(current_tree) => {
++ match current_tree
++ .children
++ .iter_mut()
++ .find(|tree| tree.item.name().eq(segment))
++ {
++ Some(tree) => {
++ if !tree.is_opened {
++ tree.open()?;
++ }
++ Ok(tree)
++ }
++ None => Err(anyhow::anyhow!(format!(
++ "Unable to find path: '{}'. current_segment = '{segment}'. current_root = '{root}'",
++ segments.join("/"),
++ ))),
++ }
++ }
++ },
++ )?;
++
++ // Locate the item
++ self.regenerate_index();
++ self.set_selected(
++ segments
++ .iter()
++ .fold(&self.tree, |tree, segment| {
++ tree.children
++ .iter()
++ .find(|tree| tree.item.name().eq(segment))
++ .expect("Should be unreachable")
++ })
++ .index,
++ );
++
++ self.align_view_center();
++ Ok(())
++ }
++
++ fn align_view_center(&mut self) {
++ self.pre_render = Some(Box::new(|tree, area| {
++ tree.winline = area.height as usize / 2
++ }))
++ }
++
++ fn align_view_top(&mut self) {
++ self.winline = 0
++ }
++
++ fn align_view_bottom(&mut self) {
++ self.pre_render = Some(Box::new(|tree, area| tree.winline = area.height as usize))
++ }
++
++ fn regenerate_index(&mut self) {
++ self.tree.regenerate_index();
++ }
++
++ fn move_to_parent(&mut self) -> Result<()> {
++ if let Some(parent) = self.current_parent()? {
++ let index = parent.index;
++ self.set_selected(index)
++ }
++ Ok(())
++ }
++
++ fn move_to_children(&mut self) -> Result<()> {
++ let current = self.current_mut()?;
++ if current.is_opened {
++ self.set_selected(self.selected + 1);
++ Ok(())
++ } else {
++ current.open()?;
++ if !current.children.is_empty() {
++ self.set_selected(self.selected + 1);
++ self.regenerate_index();
++ }
++ Ok(())
++ }
++ }
++
++ pub fn refresh(&mut self) -> Result<()> {
++ self.tree.refresh()?;
++ self.set_selected(self.selected);
++ Ok(())
++ }
++
++ fn move_to_first_line(&mut self) {
++ self.move_up(usize::MAX / 2)
++ }
++
++ fn move_to_last_line(&mut self) {
++ self.move_down(usize::MAX / 2)
++ }
++
++ fn move_leftmost(&mut self) {
++ self.move_left(usize::MAX / 2);
++ }
++
++ fn move_rightmost(&mut self) {
++ self.move_right(usize::MAX / 2)
++ }
++
++ fn restore_saved_view(&mut self) -> Result<()> {
++ if let Some(saved_view) = self.saved_view.take() {
++ self.selected = saved_view.selected;
++ self.winline = saved_view.winline;
++ self.refresh()
++ } else {
++ Ok(())
++ }
++ }
++
++ pub fn prompt(&self) -> Option<&Prompt> {
++ if let Some((_, prompt)) = self.search_prompt.as_ref() {
++ Some(prompt)
++ } else {
++ None
++ }
++ }
++}
++
++pub fn tree_view_help() -> Vec<(&'static str, &'static str)> {
++ vec![
++ ("o, Enter", "Open/Close"),
++ ("j, down, C-n", "Down"),
++ ("k, up, C-p", "Up"),
++ ("h, left", "Go to parent"),
++ ("l, right", "Expand"),
++ ("J", "Go to next sibling"),
++ ("K", "Go to previous sibling"),
++ ("H", "Go to first child"),
++ ("L", "Go to last child"),
++ ("R", "Refresh"),
++ ("/", "Search"),
++ ("n", "Go to next search match"),
++ ("N", "Go to previous search match"),
++ ("gh, Home", "Scroll to the leftmost"),
++ ("gl, End", "Scroll to the rightmost"),
++ ("C-o", "Jump backward"),
++ ("C-i, Tab", "Jump forward"),
++ ("C-d", "Half page down"),
++ ("C-u", "Half page up"),
++ ("PageDown", "Full page down"),
++ ("PageUp", "Full page up"),
++ ("zt", "Align view top"),
++ ("zz", "Align view center"),
++ ("zb", "Align view bottom"),
++ ("gg", "Go to first line"),
++ ("ge", "Go to last line"),
++ ]
++}
++
++impl<T: TreeViewItem> TreeView<T> {
++ pub fn on_enter(
++ &mut self,
++ cx: &mut Context,
++ params: &mut T::Params,
++ selected_index: usize,
++ ) -> Result<()> {
++ let selected_item = self.get_mut(selected_index)?;
++ if selected_item.is_opened {
++ selected_item.close();
++ self.regenerate_index();
++ return Ok(());
++ }
++
++ if let Some(mut on_open_fn) = self.on_opened_fn.take() {
++ let mut f = || -> Result<()> {
++ let current = self.current_mut()?;
++ match on_open_fn(&mut current.item, cx, params) {
++ TreeOp::GetChildsAndInsert => {
++ if let Err(err) = current.open() {
++ cx.editor.set_error(format!("{err}"))
++ }
++ }
++ TreeOp::Noop => {}
++ };
++ Ok(())
++ };
++ f()?;
++ self.regenerate_index();
++ self.on_opened_fn = Some(on_open_fn);
++ };
++ Ok(())
++ }
++
++ fn set_search_str(&mut self, s: String) {
++ self.search_str = s;
++ self.saved_view = None;
++ }
++
++ fn saved_view(&self) -> SavedView {
++ self.saved_view.clone().unwrap_or(SavedView {
++ selected: self.selected,
++ winline: self.winline,
++ })
++ }
++
++ fn search_next(&mut self, s: &str) {
++ let saved_view = self.saved_view();
++ let skip = std::cmp::max(2, saved_view.selected + 1);
++ self.set_selected(
++ self.tree
++ .find(skip, Direction::Forward, |e| e.item.filter(s))
++ .unwrap_or(saved_view.selected),
++ );
++ }
++
++ fn search_previous(&mut self, s: &str) {
++ let saved_view = self.saved_view();
++ let take = saved_view.selected;
++ self.set_selected(
++ self.tree
++ .find(take, Direction::Backward, |e| e.item.filter(s))
++ .unwrap_or(saved_view.selected),
++ );
++ }
++
++ fn move_to_next_search_match(&mut self) {
++ self.search_next(&self.search_str.clone())
++ }
++
++ fn move_to_previous_next_match(&mut self) {
++ self.search_previous(&self.search_str.clone())
++ }
++
++ pub fn move_down(&mut self, rows: usize) {
++ self.set_selected(self.selected.saturating_add(rows))
++ }
++
++ fn set_selected(&mut self, selected: usize) {
++ let previous_selected = self.selected;
++ self.set_selected_without_history(selected);
++ if previous_selected.abs_diff(selected) > 1 {
++ self.backward_jumps.push(previous_selected)
++ }
++ }
++
++ fn set_selected_without_history(&mut self, selected: usize) {
++ let selected = selected.clamp(0, self.tree.len().saturating_sub(1));
++ if selected > self.selected {
++ // Move down
++ self.winline = selected.min(
++ self.winline
++ .saturating_add(selected.saturating_sub(self.selected)),
++ );
++ } else {
++ // Move up
++ self.winline = selected.min(
++ self.winline
++ .saturating_sub(self.selected.saturating_sub(selected)),
++ );
++ }
++ self.selected = selected
++ }
++
++ fn jump_backward(&mut self) {
++ if let Some(index) = self.backward_jumps.pop() {
++ self.forward_jumps.push(self.selected);
++ self.set_selected_without_history(index);
++ }
++ }
++
++ fn jump_forward(&mut self) {
++ if let Some(index) = self.forward_jumps.pop() {
++ self.set_selected(index)
++ }
++ }
++
++ pub fn move_up(&mut self, rows: usize) {
++ self.set_selected(self.selected.saturating_sub(rows))
++ }
++
++ fn move_to_next_sibling(&mut self) -> Result<()> {
++ if let Some(parent) = self.current_parent()? {
++ if let Some(local_index) = parent
++ .children
++ .iter()
++ .position(|child| child.index == self.selected)
++ {
++ if let Some(next_sibling) = parent.children.get(local_index.saturating_add(1)) {
++ self.set_selected(next_sibling.index)
++ }
++ }
++ }
++ Ok(())
++ }
++
++ fn move_to_previous_sibling(&mut self) -> Result<()> {
++ if let Some(parent) = self.current_parent()? {
++ if let Some(local_index) = parent
++ .children
++ .iter()
++ .position(|child| child.index == self.selected)
++ {
++ if let Some(next_sibling) = parent.children.get(local_index.saturating_sub(1)) {
++ self.set_selected(next_sibling.index)
++ }
++ }
++ }
++ Ok(())
++ }
++
++ fn move_to_last_sibling(&mut self) -> Result<()> {
++ if let Some(parent) = self.current_parent()? {
++ if let Some(last) = parent.children.last() {
++ self.set_selected(last.index)
++ }
++ }
++ Ok(())
++ }
++
++ fn move_to_first_sibling(&mut self) -> Result<()> {
++ if let Some(parent) = self.current_parent()? {
++ if let Some(last) = parent.children.first() {
++ self.set_selected(last.index)
++ }
++ }
++ Ok(())
++ }
++
++ fn move_left(&mut self, cols: usize) {
++ self.column = self.column.saturating_sub(cols);
++ }
++
++ fn move_right(&mut self, cols: usize) {
++ self.pre_render = Some(Box::new(move |tree, area| {
++ let max_scroll = tree
++ .max_len
++ .saturating_sub(area.width as usize)
++ .saturating_add(1);
++ tree.column = max_scroll.min(tree.column + cols);
++ }));
++ }
++
++ fn move_down_half_page(&mut self) {
++ self.pre_render = Some(Box::new(|tree, area| {
++ tree.move_down((area.height / 2) as usize);
++ }));
++ }
++
++ fn move_up_half_page(&mut self) {
++ self.pre_render = Some(Box::new(|tree, area| {
++ tree.move_up((area.height / 2) as usize);
++ }));
++ }
++
++ fn move_down_page(&mut self) {
++ self.pre_render = Some(Box::new(|tree, area| {
++ tree.move_down((area.height) as usize);
++ }));
++ }
++
++ fn move_up_page(&mut self) {
++ self.pre_render = Some(Box::new(|tree, area| {
++ tree.move_up((area.height) as usize);
++ }));
++ }
++
++ fn save_view(&mut self) {
++ self.saved_view = Some(SavedView {
++ selected: self.selected,
++ winline: self.winline,
++ })
++ }
++
++ fn get(&self, index: usize) -> Result<&Tree<T>> {
++ self.tree.get(index).ok_or_else(|| {
++ anyhow::anyhow!("Programming error: TreeView.get: index {index} is out of bound")
++ })
++ }
++
++ fn get_mut(&mut self, index: usize) -> Result<&mut Tree<T>> {
++ self.tree.get_mut(index).ok_or_else(|| {
++ anyhow::anyhow!("Programming error: TreeView.get_mut: index {index} is out of bound")
++ })
++ }
++
++ pub fn current(&self) -> Result<&Tree<T>> {
++ self.get(self.selected)
++ }
++
++ pub fn current_mut(&mut self) -> Result<&mut Tree<T>> {
++ self.get_mut(self.selected)
++ }
++
++ fn current_parent(&self) -> Result<Option<&Tree<T>>> {
++ if let Some(parent_index) = self.current()?.parent_index {
++ Ok(Some(self.get(parent_index)?))
++ } else {
++ Ok(None)
++ }
++ }
++
++ pub fn current_item(&self) -> Result<&T> {
++ Ok(&self.current()?.item)
++ }
++
++ pub fn winline(&self) -> usize {
++ self.winline
++ }
++}
++
++#[derive(Clone)]
++struct RenderedLine {
++ indent: String,
++ content: String,
++ selected: bool,
++ is_ancestor_of_current_item: bool,
++}
++struct RenderTreeParams<'a, T> {
++ tree: &'a Tree<T>,
++ prefix: &'a String,
++ level: usize,
++ selected: usize,
++}
++
++fn render_tree<T: TreeViewItem>(
++ RenderTreeParams {
++ tree,
++ prefix,
++ level,
++ selected,
++ }: RenderTreeParams<T>,
++) -> Vec<RenderedLine> {
++ let indent = if level > 0 {
++ let indicator = if tree.item().is_parent() {
++ if tree.is_opened {
++ "⏷"
++ } else {
++ "⏵"
++ }
++ } else {
++ " "
++ };
++ format!("{}{} ", prefix, indicator)
++ } else {
++ "".to_string()
++ };
++ let name = tree.item.name();
++ let head = RenderedLine {
++ indent,
++ selected: selected == tree.index,
++ is_ancestor_of_current_item: selected != tree.index && tree.get(selected).is_some(),
++ content: name,
++ };
++ let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " });
++ vec![head]
++ .into_iter()
++ .chain(tree.children.iter().flat_map(|elem| {
++ render_tree(RenderTreeParams {
++ tree: elem,
++ prefix: &prefix,
++ level: level + 1,
++ selected,
++ })
++ }))
++ .collect()
++}
++
++impl<T: TreeViewItem + Clone> TreeView<T> {
++ pub fn render(
++ &mut self,
++ area: Rect,
++ prompt_area: Rect,
++ surface: &mut Surface,
++ cx: &mut Context,
++ ) {
++ let style = cx.editor.theme.get(&self.tree_symbol_style);
++ if let Some((_, prompt)) = self.search_prompt.as_mut() {
++ prompt.render_prompt(prompt_area, surface, cx)
++ }
++
++ let ancestor_style = {
++ let style = cx.editor.theme.get("ui.selection");
++ let fg = cx.editor.theme.get("ui.text").fg;
++ match (style.fg, fg) {
++ (None, Some(fg)) => style.fg(fg),
++ _ => style,
++ }
++ };
++
++ let iter = self.render_lines(area).into_iter().enumerate();
++
++ for (index, line) in iter {
++ let area = Rect::new(area.x, area.y.saturating_add(index as u16), area.width, 1);
++ let indent_len = line.indent.chars().count() as u16;
++ surface.set_stringn(
++ area.x,
++ area.y,
++ line.indent.clone(),
++ indent_len as usize,
++ style,
++ );
++
++ let style = if line.selected {
++ style.add_modifier(Modifier::REVERSED)
++ } else {
++ style
++ };
++ let x = area.x.saturating_add(indent_len);
++ surface.set_stringn(
++ x,
++ area.y,
++ line.content.clone(),
++ area.width
++ .saturating_sub(indent_len)
++ .saturating_sub(1)
++ .into(),
++ if line.is_ancestor_of_current_item {
++ ancestor_style
++ } else {
++ style
++ },
++ );
++ }
++ }
++
++ #[cfg(test)]
++ pub fn render_to_string(&mut self, area: Rect) -> String {
++ let lines = self.render_lines(area);
++ lines
++ .into_iter()
++ .map(|line| {
++ let name = if line.selected {
++ format!("({})", line.content)
++ } else if line.is_ancestor_of_current_item {
++ format!("[{}]", line.content)
++ } else {
++ line.content
++ };
++ format!("{}{}", line.indent, name)
++ })
++ .collect::<Vec<_>>()
++ .join("\n")
++ }
++
++ fn render_lines(&mut self, area: Rect) -> Vec<RenderedLine> {
++ if let Some(pre_render) = self.pre_render.take() {
++ pre_render(self, area);
++ }
++
++ self.winline = self.winline.min(area.height.saturating_sub(1) as usize);
++ let skip = self.selected.saturating_sub(self.winline);
++ let params = RenderTreeParams {
++ tree: &self.tree,
++ prefix: &"".to_string(),
++ level: 0,
++ selected: self.selected,
++ };
++
++ let lines = render_tree(params);
++
++ self.max_len = lines
++ .iter()
++ .map(|line| {
++ line.indent
++ .chars()
++ .count()
++ .saturating_add(line.content.chars().count())
++ })
++ .max()
++ .unwrap_or(0);
++
++ let max_width = area.width as usize;
++
++ let take = area.height as usize;
++
++ struct RetainAncestorResult {
++ skipped_ancestors: Vec<RenderedLine>,
++ remaining_lines: Vec<RenderedLine>,
++ }
++ fn retain_ancestors(lines: Vec<RenderedLine>, skip: usize) -> RetainAncestorResult {
++ if skip == 0 {
++ return RetainAncestorResult {
++ skipped_ancestors: vec![],
++ remaining_lines: lines,
++ };
++ }
++ if let Some(line) = lines.get(0) {
++ if line.selected {
++ return RetainAncestorResult {
++ skipped_ancestors: vec![],
++ remaining_lines: lines,
++ };
++ }
++ }
++
++ let selected_index = lines.iter().position(|line| line.selected);
++ let skip = match selected_index {
++ None => skip,
++ Some(selected_index) => skip.min(selected_index),
++ };
++ let (skipped, remaining) = lines.split_at(skip.min(lines.len().saturating_sub(1)));
++
++ let skipped_ancestors = skipped
++ .iter()
++ .cloned()
++ .filter(|line| line.is_ancestor_of_current_item)
++ .collect::<Vec<_>>();
++
++ let result = retain_ancestors(remaining.to_vec(), skipped_ancestors.len());
++ RetainAncestorResult {
++ skipped_ancestors: skipped_ancestors
++ .into_iter()
++ .chain(result.skipped_ancestors.into_iter())
++ .collect(),
++ remaining_lines: result.remaining_lines,
++ }
++ }
++
++ let RetainAncestorResult {
++ skipped_ancestors,
++ remaining_lines,
++ } = retain_ancestors(lines, skip);
++
++ let max_ancestors_len = take.saturating_sub(1);
++
++ // Skip furthest ancestors
++ let skipped_ancestors = skipped_ancestors
++ .into_iter()
++ .rev()
++ .take(max_ancestors_len)
++ .rev()
++ .collect::<Vec<_>>();
++
++ let skipped_ancestors_len = skipped_ancestors.len();
++
++ skipped_ancestors
++ .into_iter()
++ .chain(
++ remaining_lines
++ .into_iter()
++ .take(take.saturating_sub(skipped_ancestors_len)),
++ )
++ // Horizontal scroll
++ .map(|line| {
++ let skip = self.column;
++ let indent_len = line.indent.chars().count();
++ RenderedLine {
++ indent: if line.indent.is_empty() {
++ "".to_string()
++ } else {
++ line.indent
++ .chars()
++ .skip(skip)
++ .take(max_width)
++ .collect::<String>()
++ },
++ content: line
++ .content
++ .chars()
++ .skip(skip.saturating_sub(indent_len))
++ .take((max_width.saturating_sub(indent_len)).clamp(0, line.content.len()))
++ .collect::<String>(),
++ ..line
++ }
++ })
++ .collect()
++ }
++
++ #[cfg(test)]
++ pub fn handle_events(
++ &mut self,
++ events: &str,
++ cx: &mut Context,
++ params: &mut T::Params,
++ ) -> Result<()> {
++ use helix_view::input::parse_macro;
++
++ for event in parse_macro(events)? {
++ self.handle_event(&Event::Key(event), cx, params);
++ }
++ Ok(())
++ }
++
++ pub fn handle_event(
++ &mut self,
++ event: &Event,
++ cx: &mut Context,
++ params: &mut T::Params,
++ ) -> EventResult {
++ let key_event = match event {
++ Event::Key(event) => event,
++ Event::Resize(..) => return EventResult::Consumed(None),
++ _ => return EventResult::Ignored(None),
++ };
++ (|| -> Result<EventResult> {
++ if let Some(mut on_next_key) = self.on_next_key.take() {
++ on_next_key(cx, self, key_event)?;
++ return Ok(EventResult::Consumed(None));
++ }
++
++ if let EventResult::Consumed(c) = self.handle_search_event(key_event, cx) {
++ return Ok(EventResult::Consumed(c));
++ }
++
++ let count = std::mem::replace(&mut self.count, 0);
++
++ match key_event {
++ key!(i @ '0'..='9') => {
++ self.count = i.to_digit(10).unwrap_or(0) as usize + count * 10
++ }
++ shift!('J') => self.move_to_next_sibling()?,
++ shift!('K') => self.move_to_previous_sibling()?,
++ shift!('H') => self.move_to_first_sibling()?,
++ shift!('L') => self.move_to_last_sibling()?,
++ key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)),
++ key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)),
++ key!('h') | key!(Left) => self.move_to_parent()?,
++ key!('l') | key!(Right) => self.move_to_children()?,
++ key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected)?,
++ ctrl!('d') => self.move_down_half_page(),
++ ctrl!('u') => self.move_up_half_page(),
++ key!('z') => {
++ self.on_next_key = Some(Box::new(|_, tree, event| {
++ match event {
++ key!('z') => tree.align_view_center(),
++ key!('t') => tree.align_view_top(),
++ key!('b') => tree.align_view_bottom(),
++ _ => {}
++ };
++ Ok(())
++ }));
++ }
++ key!('g') => {
++ self.on_next_key = Some(Box::new(|_, tree, event| {
++ match event {
++ key!('g') => tree.move_to_first_line(),
++ key!('e') => tree.move_to_last_line(),
++ key!('h') => tree.move_leftmost(),
++ key!('l') => tree.move_rightmost(),
++ _ => {}
++ };
++ Ok(())
++ }));
++ }
++ key!('/') => self.new_search_prompt(Direction::Forward),
++ key!('n') => self.move_to_next_search_match(),
++ shift!('N') => self.move_to_previous_next_match(),
++ key!(PageDown) => self.move_down_page(),
++ key!(PageUp) => self.move_up_page(),
++ shift!('R') => {
++ if let Err(error) = self.refresh() {
++ cx.editor.set_error(error.to_string())
++ }
++ }
++ key!(Home) => self.move_leftmost(),
++ key!(End) => self.move_rightmost(),
++ ctrl!('o') => self.jump_backward(),
++ ctrl!('i') | key!(Tab) => self.jump_forward(),
++ _ => return Ok(EventResult::Ignored(None)),
++ };
++ Ok(EventResult::Consumed(None))
++ })()
++ .unwrap_or_else(|err| {
++ cx.editor.set_error(format!("{err}"));
++ EventResult::Consumed(None)
++ })
++ }
++
++ fn handle_search_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
++ if let Some((direction, mut prompt)) = self.search_prompt.take() {
++ match event {
++ key!(Enter) => {
++ self.set_search_str(prompt.line().clone());
++ EventResult::Consumed(None)
++ }
++ key!(Esc) => {
++ if let Err(err) = self.restore_saved_view() {
++ cx.editor.set_error(format!("{err}"))
++ }
++ EventResult::Consumed(None)
++ }
++ _ => {
++ let event = prompt.handle_event(&Event::Key(*event), cx);
++ let line = prompt.line();
++ match direction {
++ Direction::Forward => {
++ self.search_next(line);
++ }
++ Direction::Backward => self.search_previous(line),
++ }
++ self.search_prompt = Some((direction, prompt));
++ event
++ }
++ }
++ } else {
++ EventResult::Ignored(None)
++ }
++ }
++
++ fn new_search_prompt(&mut self, direction: Direction) {
++ self.save_view();
++ self.search_prompt = Some((
++ direction,
++ Prompt::new("search: ".into(), None, ui::completers::none, |_, _, _| {}),
++ ))
++ }
++
++ pub fn prompting(&self) -> bool {
++ self.search_prompt.is_some() || self.on_next_key.is_some()
++ }
++}
++
++/// Recalculate the index of each item of a tree.
++///
++/// For example:
++///
++/// ```txt
++/// foo (0)
++/// bar (1)
++/// spam (2)
++/// jar (3)
++/// yo (4)
++/// ```
++fn index_elems<T>(parent_index: usize, elems: Vec<Tree<T>>) -> Vec<Tree<T>> {
++ fn index_elems<T>(
++ current_index: usize,
++ elems: Vec<Tree<T>>,
++ parent_index: usize,
++ ) -> (usize, Vec<Tree<T>>) {
++ elems
++ .into_iter()
++ .fold((current_index, vec![]), |(current_index, trees), elem| {
++ let index = current_index;
++ let item = elem.item;
++ let (current_index, folded) = index_elems(current_index + 1, elem.children, index);
++ let tree = Tree {
++ item,
++ children: folded,
++ index,
++ is_opened: elem.is_opened,
++ parent_index: Some(parent_index),
++ };
++ (
++ current_index,
++ trees.into_iter().chain(vec![tree].into_iter()).collect(),
++ )
++ })
++ }
++ index_elems(parent_index + 1, elems, parent_index).1
++}
+diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
+index 3c00a61e..1ab5f976 100644
+--- a/helix-view/src/editor.rs
++++ b/helix-view/src/editor.rs
+@@ -211,6 +211,30 @@ fn default() -> Self {
+ }
+ }
+
++#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
++#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
++pub struct ExplorerConfig {
++ pub position: ExplorerPosition,
++ /// explorer column width
++ pub column_width: usize,
++}
++
++#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
++#[serde(rename_all = "kebab-case")]
++pub enum ExplorerPosition {
++ Left,
++ Right,
++}
++
++impl Default for ExplorerConfig {
++ fn default() -> Self {
++ Self {
++ position: ExplorerPosition::Left,
++ column_width: 36,
++ }
++ }
++}
++
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+ #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+ pub struct Config {
+@@ -285,6 +309,8 @@ pub struct Config {
+ pub initial_mode: Mode,
+ /// Whether to color modes with different colors. Defaults to `false`.
+ pub color_modes: bool,
++ /// explore config
++ pub explorer: ExplorerConfig,
+ pub soft_wrap: SoftWrap,
+ /// Workspace specific lsp ceiling dirs
+ pub workspace_lsp_roots: Vec<PathBuf>,
+@@ -816,6 +842,7 @@ fn default() -> Self {
+ indent_guides: IndentGuidesConfig::default(),
+ initial_mode: Mode::Normal,
+ color_modes: false,
++ explorer: ExplorerConfig::default(),
+ soft_wrap: SoftWrap {
+ enable: Some(false),
+ ..SoftWrap::default()
+@@ -988,6 +1015,18 @@ pub enum CloseError {
+ SaveError(anyhow::Error),
+ }
+
++impl From<CloseError> for anyhow::Error {
++ fn from(error: CloseError) -> Self {
++ match error {
++ CloseError::DoesNotExist => anyhow::anyhow!("Document doesn't exist"),
++ CloseError::BufferModified(error) => {
++ anyhow::anyhow!(format!("Buffer modified: '{error}'"))
++ }
++ CloseError::SaveError(error) => anyhow::anyhow!(format!("Save error: {error}")),
++ }
++ }
++}
++
+ impl Editor {
+ pub fn new(
+ mut area: Rect,
+diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
+index 046db86a..fbfde635 100644
+--- a/helix-view/src/graphics.rs
++++ b/helix-view/src/graphics.rs
+@@ -248,6 +248,34 @@ pub fn intersects(self, other: Rect) -> bool {
+ && self.y < other.y + other.height
+ && self.y + self.height > other.y
+ }
++
++ /// Returns a smaller `Rect` with a margin of 5% on each side, and an additional 2 rows at the bottom
++ pub fn overlaid(self) -> Rect {
++ self.clip_bottom(2).clip_relative(90, 90)
++ }
++
++ /// Returns a smaller `Rect` with width and height clipped to the given `percent_horizontal`
++ /// and `percent_vertical`.
++ ///
++ /// Value of `percent_horizontal` and `percent_vertical` is from 0 to 100.
++ pub fn clip_relative(self, percent_horizontal: u8, percent_vertical: u8) -> Rect {
++ fn mul_and_cast(size: u16, factor: u8) -> u16 {
++ ((size as u32) * (factor as u32) / 100).try_into().unwrap()
++ }
++
++ let inner_w = mul_and_cast(self.width, percent_horizontal);
++ let inner_h = mul_and_cast(self.height, percent_vertical);
++
++ let offset_x = self.width.saturating_sub(inner_w) / 2;
++ let offset_y = self.height.saturating_sub(inner_h) / 2;
++
++ Rect {
++ x: self.x + offset_x,
++ y: self.y + offset_y,
++ width: inner_w,
++ height: inner_h,
++ }
++ }
+ }
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+--
+2.41.0
+
+
+From 3476811ef07346ccfa4ea0757306bc0df99be6b0 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 18:48:07 -0700
+Subject: [PATCH 2/2] Unit test file explorer and tree view
+
+---
+ helix-term/src/compositor.rs | 56 ++
+ helix-term/src/ui/explorer.rs | 713 ++++++++++++++++
+ helix-term/src/ui/tree.rs | 1472 +++++++++++++++++++++++++++++++++
+ 3 files changed, 2241 insertions(+)
+
+diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
+index bcb3e449..35009a25 100644
+--- a/helix-term/src/compositor.rs
++++ b/helix-term/src/compositor.rs
+@@ -34,6 +34,47 @@ pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> {
+ tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
+ Ok(())
+ }
++
++ /// Purpose: to test `handle_event` without escalating the test case to integration test
++ /// Usage:
++ /// ```
++ /// let mut editor = Context::dummy_editor();
++ /// let mut jobs = Context::dummy_jobs();
++ /// let mut cx = Context::dummy(&mut jobs, &mut editor);
++ /// ```
++ #[cfg(test)]
++ pub fn dummy(jobs: &'a mut Jobs, editor: &'a mut helix_view::Editor) -> Context<'a> {
++ Context {
++ jobs,
++ scroll: None,
++ editor,
++ }
++ }
++
++ #[cfg(test)]
++ pub fn dummy_jobs() -> Jobs {
++ Jobs::new()
++ }
++
++ #[cfg(test)]
++ pub fn dummy_editor() -> Editor {
++ use crate::config::Config;
++ use arc_swap::{access::Map, ArcSwap};
++ use helix_core::syntax::{self, Configuration};
++ use helix_view::theme;
++ use std::{sync::Arc, collections::HashMap};
++
++ let config = Arc::new(ArcSwap::from_pointee(Config::default()));
++ Editor::new(
++ Rect::new(0, 0, 60, 120),
++ Arc::new(theme::Loader::new(&[])),
++ Arc::new(syntax::Loader::new(Configuration { language: vec![], language_server: HashMap::new() })),
++ Arc::new(Arc::new(Map::new(
++ Arc::clone(&config),
++ |config: &Config| &config.editor,
++ ))),
++ )
++ }
+ }
+
+ pub trait Component: Any + AnyComponent {
+@@ -72,6 +113,21 @@ fn type_name(&self) -> &'static str {
+ fn id(&self) -> Option<&'static str> {
+ None
+ }
++
++ #[cfg(test)]
++ /// Utility method for testing `handle_event` without using integration test.
++ /// Especially useful for testing helper components such as `Prompt`, `TreeView` etc
++ fn handle_events(&mut self, events: &str) -> anyhow::Result<()> {
++ use helix_view::input::parse_macro;
++
++ let mut editor = Context::dummy_editor();
++ let mut jobs = Context::dummy_jobs();
++ let mut cx = Context::dummy(&mut jobs, &mut editor);
++ for event in parse_macro(events)? {
++ self.handle_event(&Event::Key(event), &mut cx);
++ }
++ Ok(())
++ }
+ }
+
+ pub struct Compositor {
+diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs
+index 6df059b1..4ad8dee7 100644
+--- a/helix-term/src/ui/explorer.rs
++++ b/helix-term/src/ui/explorer.rs
+@@ -749,3 +749,716 @@ fn render_block(area: Rect, surface: &mut Surface, borders: Borders) -> Rect {
+ block.render(area, surface);
+ inner
+ }
++
++#[cfg(test)]
++mod test_explorer {
++ use crate::compositor::Component;
++
++ use super::Explorer;
++ use helix_view::graphics::Rect;
++ use std::{fs, path::PathBuf};
++
++ /// This code should create the following file tree:
++ ///
++ /// <temp_path>
++ /// ├── index.html
++ /// ├── .gitignore
++ /// ├── scripts
++ /// │ └── main.js
++ /// └── styles
++ /// ├── style.css
++ /// └── public
++ /// └── file
++ ///
++ fn dummy_file_tree() -> PathBuf {
++ let path = tempfile::tempdir().unwrap().path().to_path_buf();
++ if path.exists() {
++ fs::remove_dir_all(path.clone()).unwrap();
++ }
++ fs::create_dir_all(path.clone()).unwrap();
++ fs::write(path.join("index.html"), "").unwrap();
++ fs::write(path.join(".gitignore"), "").unwrap();
++
++ fs::create_dir_all(path.join("scripts")).unwrap();
++ fs::write(path.join("scripts").join("main.js"), "").unwrap();
++
++ fs::create_dir_all(path.join("styles")).unwrap();
++ fs::write(path.join("styles").join("style.css"), "").unwrap();
++
++ fs::create_dir_all(path.join("styles").join("public")).unwrap();
++ fs::write(path.join("styles").join("public").join("file"), "").unwrap();
++
++ path
++ }
++
++ fn render(explorer: &mut Explorer) -> String {
++ explorer.tree.render_to_string(Rect::new(0, 0, 100, 10))
++ }
++
++ fn new_explorer() -> (PathBuf, Explorer) {
++ let path = dummy_file_tree();
++ (path.clone(), Explorer::from_path(path, 100).unwrap())
++ }
++
++ #[test]
++ fn test_reveal_file() {
++ let (path, mut explorer) = new_explorer();
++
++ let path_str = path.display().to_string();
++
++ // 0a. Expect the "scripts" folder is not opened
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++({path_str})
++⏵ scripts
++⏵ styles
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ // 1. Reveal "scripts/main.js"
++ explorer.reveal_file(path.join("scripts/main.js")).unwrap();
++
++ // 1a. Expect the "scripts" folder is opened, and "main.js" is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏷ [scripts]
++ (main.js)
++⏵ styles
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ // 2. Change root to "scripts"
++ explorer.tree.move_up(1);
++ explorer.change_root_to_current_folder().unwrap();
++
++ // 2a. Expect the current root is "scripts"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++({path_str}/scripts)
++ main.js
++"
++ )
++ .trim()
++ );
++
++ // 3. Reveal "styles/public/file", which is outside of the current root
++ explorer
++ .reveal_file(path.join("styles/public/file"))
++ .unwrap();
++
++ // 3a. Expect the current root is "public", and "file" is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}/styles/public]
++ (file)
++"
++ )
++ .trim()
++ );
++ }
++
++ #[tokio::test(flavor = "multi_thread")]
++ async fn test_rename() {
++ let (path, mut explorer) = new_explorer();
++ let path_str = path.display().to_string();
++
++ explorer.handle_events("jjj").unwrap();
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏵ styles
++ (.gitignore)
++ index.html
++"
++ )
++ .trim()
++ );
++
++ // 0. Open the rename file prompt
++ explorer.handle_events("r").unwrap();
++
++ // 0.1 Expect the current prompt to be related to file renaming
++ let prompt = &explorer.prompt.as_ref().unwrap().1;
++ assert_eq!(prompt.prompt(), " Rename to ");
++ assert_eq!(
++ prompt.line().replace(std::path::MAIN_SEPARATOR, "/"),
++ format!("{path_str}/.gitignore")
++ );
++
++ // 1. Rename the current file to a name that is lexicographically greater than "index.html"
++ explorer.handle_events("<C-w>who.is<ret>").unwrap();
++
++ // 1a. Expect the file is renamed, and is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏵ styles
++ index.html
++ (who.is)
++"
++ )
++ .trim()
++ );
++
++ assert!(path.join("who.is").exists());
++
++ // 2. Rename the current file into an existing folder
++ explorer
++ .handle_events(&format!(
++ "r<C-w>styles{}lol<ret>",
++ std::path::MAIN_SEPARATOR
++ ))
++ .unwrap();
++
++ // 2a. Expect the file is moved to the folder, and is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏷ [styles]
++ ⏵ public
++ (lol)
++ style.css
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(path.join("styles/lol").exists());
++
++ // 3. Rename the current file into a non-existent folder
++ explorer
++ .handle_events(&format!(
++ "r<C-u>{}<ret>",
++ path.join("new_folder/sponge/bob").display()
++ ))
++ .unwrap();
++
++ // 3a. Expect the non-existent folder to be created,
++ // and the file is moved into it,
++ // and the renamed file is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏷ [new_folder]
++ ⏷ [sponge]
++ (bob)
++⏵ scripts
++⏷ styles
++ ⏵ public
++ style.css
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(path.join("new_folder/sponge/bob").exists());
++
++ // 4. Change current root to "new_folder/sponge"
++ explorer.handle_events("k]").unwrap();
++
++ // 4a. Expect the current root to be "sponge"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++({path_str}/new_folder/sponge)
++ bob
++"
++ )
++ .trim()
++ );
++
++ // 5. Move cursor to "bob", and rename it outside of the current root
++ explorer.handle_events("j").unwrap();
++ explorer
++ .handle_events(&format!(
++ "r<C-u>{}<ret>",
++ path.join("scripts/bob").display()
++ ))
++ .unwrap();
++
++ // 5a. Expect the current root to be "scripts"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}/scripts]
++ (bob)
++ main.js
++"
++ )
++ .trim()
++ );
++ }
++
++ #[tokio::test(flavor = "multi_thread")]
++ async fn test_new_folder() {
++ let (path, mut explorer) = new_explorer();
++ let path_str = path.display().to_string();
++
++ // 0. Open the add file/folder prompt
++ explorer.handle_events("a").unwrap();
++ let prompt = &explorer.prompt.as_ref().unwrap().1;
++ fn to_forward_slash(s: &str) -> String {
++ s.replace(std::path::MAIN_SEPARATOR, "/")
++ }
++ fn to_os_main_separator(s: &str) -> String {
++ s.replace('/', format!("{}", std::path::MAIN_SEPARATOR).as_str())
++ }
++ assert_eq!(
++ to_forward_slash(prompt.prompt()),
++ " New file or folder (ends with '/'): "
++ );
++ assert_eq!(to_forward_slash(prompt.line()), format!("{path_str}/"));
++
++ // 1. Add a new folder at the root
++ explorer
++ .handle_events(&to_os_main_separator("yoyo/<ret>"))
++ .unwrap();
++
++ // 1a. Expect the new folder is added, and is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏵ styles
++⏷ (yoyo)
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_dir(path.join("yoyo")).is_ok());
++
++ // 2. Move up to "styles"
++ explorer.handle_events("k").unwrap();
++
++ // 3. Add a new folder
++ explorer
++ .handle_events(&to_os_main_separator("asus.sass/<ret>"))
++ .unwrap();
++
++ // 3a. Expect the new folder is added under "styles", although "styles" is not opened
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏷ [styles]
++ ⏵ public
++ ⏷ (sus.sass)
++ style.css
++⏷ yoyo
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_dir(path.join("styles/sus.sass")).is_ok());
++
++ // 4. Add a new folder with non-existent parents
++ explorer
++ .handle_events(&to_os_main_separator("aa/b/c/<ret>"))
++ .unwrap();
++
++ // 4a. Expect the non-existent parents are created,
++ // and the new folder is created,
++ // and is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏷ [styles]
++ ⏷ [sus.sass]
++ ⏷ [a]
++ ⏷ [b]
++ ⏷ (c)
++ style.css
++⏷ yoyo
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_dir(path.join("styles/sus.sass/a/b/c")).is_ok());
++
++ // 5. Move to "style.css"
++ explorer.handle_events("j").unwrap();
++
++ // 6. Add a new folder here
++ explorer
++ .handle_events(&to_os_main_separator("afoobar/<ret>"))
++ .unwrap();
++
++ // 6a. Expect the folder is added under "styles",
++ // because the folder of the current item, "style.css" is "styles/"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏷ [styles]
++ ⏷ (foobar)
++ ⏵ public
++ ⏷ sus.sass
++ ⏷ a
++ ⏷ b
++ ⏷ c
++ style.css
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_dir(path.join("styles/foobar")).is_ok());
++ }
++
++ #[tokio::test(flavor = "multi_thread")]
++ async fn test_new_file() {
++ let (path, mut explorer) = new_explorer();
++ let path_str = path.display().to_string();
++
++ // 1. Add a new file at the root
++ explorer.handle_events("ayoyo<ret>").unwrap();
++
++ // 1a. Expect the new file is added, and is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏵ styles
++ .gitignore
++ index.html
++ (yoyo)
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_to_string(path.join("yoyo")).is_ok());
++
++ // 2. Move up to "styles"
++ explorer.tree.move_up(3);
++
++ // 3. Add a new file
++ explorer.handle_events("asus.sass<ret>").unwrap();
++
++ // 3a. Expect the new file is added under "styles", although "styles" is not opened
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏷ [styles]
++ ⏵ public
++ style.css
++ (sus.sass)
++ .gitignore
++ index.html
++ yoyo
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_to_string(path.join("styles/sus.sass")).is_ok());
++
++ // 4. Add a new file with non-existent parents
++ explorer.handle_events("aa/b/c<ret>").unwrap();
++
++ // 4a. Expect the non-existent parents are created,
++ // and the new file is created,
++ // and is focused
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏷ [styles]
++ ⏷ [a]
++ ⏷ [b]
++ (c)
++ ⏵ public
++ style.css
++ sus.sass
++ .gitignore
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_to_string(path.join("styles/a/b/c")).is_ok());
++
++ // 5. Move to "style.css"
++ explorer.handle_events("jj").unwrap();
++
++ // 6. Add a new file here
++ explorer.handle_events("afoobar<ret>").unwrap();
++
++ // 6a. Expect the file is added under "styles",
++ // because the folder of the current item, "style.css" is "styles/"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏷ [styles]
++ ⏷ b
++ c
++ ⏵ public
++ (foobar)
++ style.css
++ sus.sass
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_to_string(path.join("styles/foobar")).is_ok());
++ }
++
++ #[tokio::test(flavor = "multi_thread")]
++ async fn test_remove_file() {
++ let (path, mut explorer) = new_explorer();
++ let path_str = path.display().to_string();
++
++ // 1. Move to ".gitignore"
++ explorer.handle_events("/.gitignore<ret>").unwrap();
++
++ // 1a. Expect the cursor is at ".gitignore"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏵ styles
++ (.gitignore)
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_to_string(path.join(".gitignore")).is_ok());
++
++ // 2. Remove the current file
++ explorer.handle_events("dy").unwrap();
++
++ // 3. Expect ".gitignore" is deleted, and the cursor moved down
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏵ styles
++ (index.html)
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_to_string(path.join(".gitignore")).is_err());
++
++ // 3a. Expect "index.html" exists
++ assert!(fs::read_to_string(path.join("index.html")).is_ok());
++
++ // 4. Remove the current file
++ explorer.handle_events("dy").unwrap();
++
++ // 4a. Expect "index.html" is deleted, at the cursor moved up
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏵ (styles)
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_to_string(path.join("index.html")).is_err());
++ }
++
++ #[tokio::test(flavor = "multi_thread")]
++ async fn test_remove_folder() {
++ let (path, mut explorer) = new_explorer();
++ let path_str = path.display().to_string();
++
++ // 1. Move to "styles/"
++ explorer.handle_events("/styles<ret>o").unwrap();
++
++ // 1a. Expect the cursor is at "styles"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏷ (styles)
++ ⏵ public
++ style.css
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_dir(path.join("styles")).is_ok());
++
++ // 2. Remove the current folder
++ explorer.handle_events("dy").unwrap();
++
++ // 3. Expect "styles" is deleted, and the cursor moved down
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++ (.gitignore)
++ index.html
++"
++ )
++ .trim()
++ );
++
++ assert!(fs::read_dir(path.join("styles")).is_err());
++ }
++
++ #[test]
++ fn test_change_root() {
++ let (path, mut explorer) = new_explorer();
++ let path_str = path.display().to_string();
++
++ // 1. Move cursor to "styles"
++ explorer.reveal_file(path.join("styles")).unwrap();
++
++ // 2. Change root to current folder, and move cursor down
++ explorer.change_root_to_current_folder().unwrap();
++ explorer.tree.move_down(1);
++
++ // 2a. Expect the current root to be "styles", and the cursor is at "public"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}/styles]
++⏵ (public)
++ style.css
++"
++ )
++ .trim()
++ );
++
++ let current_root = explorer.state.current_root.clone();
++
++ // 3. Change root to the parent of current folder
++ explorer.change_root_parent_folder().unwrap();
++
++ // 3a. Expect the current root to be "change_root"
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++({path_str})
++⏵ scripts
++⏵ styles
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++
++ // 4. Go back to previous root
++ explorer.go_to_previous_root();
++
++ // 4a. Expect the root te become "styles", and the cursor position is not forgotten
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}/styles]
++⏵ (public)
++ style.css
++"
++ )
++ .trim()
++ );
++ assert_eq!(explorer.state.current_root, current_root);
++
++ // 5. Go back to previous root again
++ explorer.go_to_previous_root();
++
++ // 5a. Expect the current root to be "change_root" again,
++ // but this time the "styles" folder is opened,
++ // because it was opened before any change of root
++ assert_eq!(
++ render(&mut explorer),
++ format!(
++ "
++[{path_str}]
++⏵ scripts
++⏷ (styles)
++ ⏵ public
++ style.css
++ .gitignore
++ index.html
++"
++ )
++ .trim()
++ );
++ }
++}
+diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs
+index 68783be8..d0a9af5b 100644
+--- a/helix-term/src/ui/tree.rs
++++ b/helix-term/src/ui/tree.rs
+@@ -1207,3 +1207,1475 @@ fn index_elems<T>(
+ }
+ index_elems(parent_index + 1, elems, parent_index).1
+ }
++
++#[cfg(test)]
++mod test_tree_view {
++
++ use helix_view::graphics::Rect;
++
++ use crate::compositor::Context;
++
++ use super::{TreeView, TreeViewItem};
++
++ #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
++ /// The children of DivisibleItem is the division of itself.
++ /// This is used to ease the creation of a dummy tree without having to specify so many things.
++ struct DivisibleItem<'a> {
++ name: &'a str,
++ }
++
++ fn item(name: &str) -> DivisibleItem {
++ DivisibleItem { name }
++ }
++
++ impl<'a> TreeViewItem for DivisibleItem<'a> {
++ type Params = ();
++
++ fn name(&self) -> String {
++ self.name.to_string()
++ }
++
++ fn is_parent(&self) -> bool {
++ self.name.len() > 2
++ }
++
++ fn get_children(&self) -> anyhow::Result<Vec<Self>> {
++ if self.name.eq("who_lives_in_a_pineapple_under_the_sea") {
++ Ok(vec![
++ item("gary_the_snail"),
++ item("krabby_patty"),
++ item("larry_the_lobster"),
++ item("patrick_star"),
++ item("sandy_cheeks"),
++ item("spongebob_squarepants"),
++ item("mrs_puff"),
++ item("king_neptune"),
++ item("karen"),
++ item("plankton"),
++ ])
++ } else if self.is_parent() {
++ let (left, right) = self.name.split_at(self.name.len() / 2);
++ Ok(vec![item(left), item(right)])
++ } else {
++ Ok(vec![])
++ }
++ }
++ }
++
++ fn dummy_tree_view<'a>() -> TreeView<DivisibleItem<'a>> {
++ TreeView::build_tree(item("who_lives_in_a_pineapple_under_the_sea")).unwrap()
++ }
++
++ fn dummy_area() -> Rect {
++ Rect::new(0, 0, 50, 5)
++ }
++
++ fn render(view: &mut TreeView<DivisibleItem>) -> String {
++ view.render_to_string(dummy_area())
++ }
++
++ #[test]
++ fn test_init() {
++ let mut view = dummy_tree_view();
++
++ // Expect the items to be sorted
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_up_down() {
++ let mut view = dummy_tree_view();
++ view.move_down(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ (gary_the_snail)
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_down(3);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++"
++ .trim()
++ );
++
++ view.move_down(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++⏵ (larry_the_lobster)
++"
++ .trim()
++ );
++
++ view.move_up(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++⏵ larry_the_lobster
++"
++ .trim()
++ );
++
++ view.move_up(3);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ (gary_the_snail)
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_up(1);
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_to_first_line();
++ view.move_up(1);
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_to_last_line();
++ view.move_down(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ patrick_star
++⏵ plankton
++⏵ sandy_cheeks
++⏵ (spongebob_squarepants)
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_to_first_last_sibling() {
++ let mut view = dummy_tree_view();
++ view.move_to_children().unwrap();
++ view.move_to_children().unwrap();
++ view.move_to_parent().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ (gary_the_snail)
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_last_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ patrick_star
++⏵ plankton
++⏵ sandy_cheeks
++⏵ (spongebob_squarepants)
++"
++ .trim()
++ );
++
++ view.move_to_first_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ (gary_the_snail)
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_to_previous_next_sibling() {
++ let mut view = dummy_tree_view();
++ view.move_to_children().unwrap();
++ view.move_to_children().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ (e_snail)
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_next_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ e_snail
++ ⏵ (gary_th)
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_next_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ e_snail
++ ⏵ (gary_th)
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_previous_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ (e_snail)
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_previous_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ (e_snail)
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_parent().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ (gary_the_snail)
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_next_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ gary_the_snail
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ (karen)
++"
++ .trim()
++ );
++
++ view.move_to_previous_sibling().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ (gary_the_snail)
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_align_view() {
++ let mut view = dummy_tree_view();
++ view.move_down(5);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++⏵ (larry_the_lobster)
++"
++ .trim()
++ );
++
++ view.align_view_center();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ krabby_patty
++⏵ (larry_the_lobster)
++⏵ mrs_puff
++⏵ patrick_star
++"
++ .trim()
++ );
++
++ view.align_view_bottom();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++⏵ (larry_the_lobster)
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_to_first_last() {
++ let mut view = dummy_tree_view();
++
++ view.move_to_last_line();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ patrick_star
++⏵ plankton
++⏵ sandy_cheeks
++⏵ (spongebob_squarepants)
++"
++ .trim()
++ );
++
++ view.move_to_first_line();
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_half() {
++ let mut view = dummy_tree_view();
++ view.move_down_half_page();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ (karen)
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_down_half_page();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++"
++ .trim()
++ );
++
++ view.move_down_half_page();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ king_neptune
++⏵ krabby_patty
++⏵ larry_the_lobster
++⏵ (mrs_puff)
++"
++ .trim()
++ );
++
++ view.move_up_half_page();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ king_neptune
++⏵ (krabby_patty)
++⏵ larry_the_lobster
++⏵ mrs_puff
++"
++ .trim()
++ );
++
++ view.move_up_half_page();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ (karen)
++⏵ king_neptune
++⏵ krabby_patty
++⏵ larry_the_lobster
++"
++ .trim()
++ );
++
++ view.move_up_half_page();
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn move_to_children_parent() {
++ let mut view = dummy_tree_view();
++ view.move_down(1);
++ view.move_to_children().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ (e_snail)
++ ⏵ gary_th
++⏵ karen
++ "
++ .trim()
++ );
++
++ view.move_down(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ e_snail
++ ⏵ (gary_th)
++⏵ karen
++ "
++ .trim()
++ );
++
++ view.move_to_parent().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ (gary_the_snail)
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++ "
++ .trim()
++ );
++
++ view.move_to_last_line();
++ view.move_to_parent().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏷ gary_the_snail
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++ "
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_left_right() {
++ let mut view = dummy_tree_view();
++
++ fn render(view: &mut TreeView<DivisibleItem>) -> String {
++ view.render_to_string(dummy_area().with_width(20))
++ }
++
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pinea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_right(1);
++ assert_eq!(
++ render(&mut view),
++ "
++(ho_lives_in_a_pineap)
++ gary_the_snail
++ karen
++ king_neptune
++ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_right(1);
++ assert_eq!(
++ render(&mut view),
++ "
++(o_lives_in_a_pineapp)
++gary_the_snail
++karen
++king_neptune
++krabby_patty
++"
++ .trim()
++ );
++
++ view.move_right(1);
++ assert_eq!(
++ render(&mut view),
++ "
++(_lives_in_a_pineappl)
++ary_the_snail
++aren
++ing_neptune
++rabby_patty
++"
++ .trim()
++ );
++
++ view.move_left(1);
++ assert_eq!(
++ render(&mut view),
++ "
++(o_lives_in_a_pineapp)
++gary_the_snail
++karen
++king_neptune
++krabby_patty
++"
++ .trim()
++ );
++
++ view.move_leftmost();
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pinea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_left(1);
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pinea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_rightmost();
++ assert_eq!(render(&mut view), "(apple_under_the_sea)\n\n\n\n");
++ }
++
++ #[test]
++ fn test_move_to_parent_child() {
++ let mut view = dummy_tree_view();
++
++ view.move_to_children().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ (gary_the_snail)
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++
++ view.move_to_children().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ (e_snail)
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_down(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ [gary_the_snail]
++ ⏵ e_snail
++ ⏵ (gary_th)
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_parent().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏷ (gary_the_snail)
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_parent().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏷ gary_the_snail
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ );
++
++ view.move_to_parent().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏷ gary_the_snail
++ ⏵ e_snail
++ ⏵ gary_th
++⏵ karen
++"
++ .trim()
++ )
++ }
++
++ #[test]
++ fn test_search_next() {
++ let mut view = dummy_tree_view();
++
++ view.search_next("pat");
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++"
++ .trim()
++ );
++
++ view.search_next("larr");
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++⏵ (larry_the_lobster)
++"
++ .trim()
++ );
++
++ view.move_to_last_line();
++ view.search_next("who_lives");
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_search_previous() {
++ let mut view = dummy_tree_view();
++
++ view.search_previous("larry");
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++⏵ (larry_the_lobster)
++"
++ .trim()
++ );
++
++ view.move_to_last_line();
++ view.search_previous("krab");
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++⏵ larry_the_lobster
++"
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_to_next_search_match() {
++ let mut view = dummy_tree_view();
++ view.set_search_str("pat".to_string());
++ view.move_to_next_search_match();
++
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++ "
++ .trim()
++ );
++
++ view.move_to_next_search_match();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ krabby_patty
++⏵ larry_the_lobster
++⏵ mrs_puff
++⏵ (patrick_star)
++ "
++ .trim()
++ );
++
++ view.move_to_next_search_match();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ (krabby_patty)
++⏵ larry_the_lobster
++⏵ mrs_puff
++⏵ patrick_star
++ "
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_move_to_previous_search_match() {
++ let mut view = dummy_tree_view();
++ view.set_search_str("pat".to_string());
++ view.move_to_previous_next_match();
++
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ krabby_patty
++⏵ larry_the_lobster
++⏵ mrs_puff
++⏵ (patrick_star)
++ "
++ .trim()
++ );
++
++ view.move_to_previous_next_match();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ (krabby_patty)
++⏵ larry_the_lobster
++⏵ mrs_puff
++⏵ patrick_star
++ "
++ .trim()
++ );
++
++ view.move_to_previous_next_match();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ krabby_patty
++⏵ larry_the_lobster
++⏵ mrs_puff
++⏵ (patrick_star)
++ "
++ .trim()
++ );
++ }
++
++ #[test]
++ fn test_jump_backward_forward() {
++ let mut view = dummy_tree_view();
++ view.move_down_half_page();
++ render(&mut view);
++
++ view.move_down_half_page();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++ "
++ .trim()
++ );
++
++ view.jump_backward();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ (karen)
++⏵ king_neptune
++⏵ krabby_patty
++ "
++ .trim()
++ );
++
++ view.jump_backward();
++ assert_eq!(
++ render(&mut view),
++ "
++(who_lives_in_a_pineapple_under_the_sea)
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++ "
++ .trim()
++ );
++
++ view.jump_forward();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ (karen)
++⏵ king_neptune
++⏵ krabby_patty
++ "
++ .trim()
++ );
++
++ view.jump_forward();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ karen
++⏵ king_neptune
++⏵ (krabby_patty)
++ "
++ .trim()
++ );
++
++ view.jump_backward();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ gary_the_snail
++⏵ (karen)
++⏵ king_neptune
++⏵ krabby_patty
++ "
++ .trim()
++ );
++ }
++
++ mod static_tree {
++ use crate::ui::{TreeView, TreeViewItem};
++
++ use super::dummy_area;
++
++ #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
++ /// This is used for test cases where the structure of the tree has to be known upfront
++ pub struct StaticItem<'a> {
++ pub name: &'a str,
++ pub children: Option<Vec<StaticItem<'a>>>,
++ }
++
++ pub fn parent<'a>(name: &'a str, children: Vec<StaticItem<'a>>) -> StaticItem<'a> {
++ StaticItem {
++ name,
++ children: Some(children),
++ }
++ }
++
++ pub fn child(name: &str) -> StaticItem {
++ StaticItem {
++ name,
++ children: None,
++ }
++ }
++
++ impl<'a> TreeViewItem for StaticItem<'a> {
++ type Params = ();
++
++ fn name(&self) -> String {
++ self.name.to_string()
++ }
++
++ fn is_parent(&self) -> bool {
++ self.children.is_some()
++ }
++
++ fn get_children(&self) -> anyhow::Result<Vec<Self>> {
++ match &self.children {
++ Some(children) => Ok(children.clone()),
++ None => Ok(vec![]),
++ }
++ }
++ }
++
++ pub fn render(view: &mut TreeView<StaticItem<'_>>) -> String {
++ view.render_to_string(dummy_area().with_height(3))
++ }
++ }
++
++ #[test]
++ fn test_sticky_ancestors() {
++ // The ancestors of the current item should always be visible
++ // However, if there's not enough space, the current item will take precedence,
++ // and the nearest ancestor has higher precedence than further ancestors
++ use static_tree::*;
++
++ let mut view = TreeView::build_tree(parent(
++ "root",
++ vec![
++ parent("a", vec![child("aa"), child("ab")]),
++ parent(
++ "b",
++ vec![parent(
++ "ba",
++ vec![parent("baa", vec![child("baaa"), child("baab")])],
++ )],
++ ),
++ ],
++ ))
++ .unwrap();
++
++ assert_eq!(
++ render(&mut view),
++ "
++(root)
++⏵ a
++⏵ b
++ "
++ .trim()
++ );
++
++ // 1. Move down to "a", and expand it
++ view.move_down(1);
++ view.move_to_children().unwrap();
++
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++⏷ [a]
++ (aa)
++ "
++ .trim()
++ );
++
++ // 2. Move down by 1
++ view.move_down(1);
++
++ // 2a. Expect all ancestors (i.e. "root" and "a") are visible,
++ // and the cursor is at "ab"
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++⏷ [a]
++ (ab)
++ "
++ .trim()
++ );
++
++ // 3. Move down by 1
++ view.move_down(1);
++
++ // 3a. Expect "a" is out of view, because it is no longer the ancestor of the current item
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++ ab
++⏵ (b)
++ "
++ .trim()
++ );
++
++ // 4. Move to the children of "b", which is "ba"
++ view.move_to_children().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++⏷ [b]
++ ⏵ (ba)
++ "
++ .trim()
++ );
++
++ // 5. Move to the children of "ba", which is "baa"
++ view.move_to_children().unwrap();
++
++ // 5a. Expect the furthest ancestor "root" is out of view,
++ // because when there's no enough space, the nearest ancestor takes precedence
++ assert_eq!(
++ render(&mut view),
++ "
++⏷ [b]
++ ⏷ [ba]
++ ⏵ (baa)
++ "
++ .trim()
++ );
++
++ // 5.1 Move to child
++ view.move_to_children().unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++ ⏷ [ba]
++ ⏷ [baa]
++ (baaa)
++"
++ .trim_matches('\n')
++ );
++
++ // 5.2 Move down
++ view.move_down(1);
++ assert_eq!(
++ render(&mut view),
++ "
++ ⏷ [ba]
++ ⏷ [baa]
++ (baab)
++"
++ .trim_matches('\n')
++ );
++
++ // 5.3 Move up
++ view.move_up(1);
++ assert_eq!(view.current_item().unwrap().name, "baaa");
++ assert_eq!(
++ render(&mut view),
++ "
++ ⏷ [ba]
++ ⏷ [baa]
++ (baaa)
++"
++ .trim_matches('\n')
++ );
++
++ // 5.4 Move up
++ view.move_up(1);
++ assert_eq!(
++ render(&mut view),
++ "
++⏷ [b]
++ ⏷ [ba]
++ ⏷ (baa)
++ "
++ .trim()
++ );
++
++ // 6. Move up by one
++ view.move_up(1);
++
++ // 6a. Expect "root" is visible again, because now there's enough space to render all
++ // ancestors
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++⏷ [b]
++ ⏷ (ba)
++ "
++ .trim()
++ );
++
++ // 7. Move up by one
++ view.move_up(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++⏷ (b)
++ ⏷ ba
++ "
++ .trim()
++ );
++
++ // 8. Move up by one
++ view.move_up(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++⏷ [a]
++ (ab)
++ "
++ .trim()
++ );
++
++ // 9. Move up by one
++ view.move_up(1);
++ assert_eq!(
++ render(&mut view),
++ "
++[root]
++⏷ [a]
++ (aa)
++ "
++ .trim()
++ );
++ }
++
++ #[tokio::test(flavor = "multi_thread")]
++ async fn test_search_prompt() {
++ let mut editor = Context::dummy_editor();
++ let mut jobs = Context::dummy_jobs();
++ let mut cx = Context::dummy(&mut jobs, &mut editor);
++ let mut view = dummy_tree_view();
++
++ view.handle_events("/an", &mut cx, &mut ()).unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ larry_the_lobster
++⏵ mrs_puff
++⏵ patrick_star
++⏵ (plankton)
++ "
++ .trim()
++ );
++
++ view.handle_events("t<ret>", &mut cx, &mut ()).unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ patrick_star
++⏵ plankton
++⏵ sandy_cheeks
++⏵ (spongebob_squarepants)
++ "
++ .trim()
++ );
++
++ view.handle_events("/larry", &mut cx, &mut ()).unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ karen
++⏵ king_neptune
++⏵ krabby_patty
++⏵ (larry_the_lobster)
++ "
++ .trim()
++ );
++
++ view.handle_events("<esc>", &mut cx, &mut ()).unwrap();
++ assert_eq!(
++ render(&mut view),
++ "
++[who_lives_in_a_pineapple_under_the_sea]
++⏵ patrick_star
++⏵ plankton
++⏵ sandy_cheeks
++⏵ (spongebob_squarepants)
++ "
++ .trim()
++ );
++ }
++}
++
++#[cfg(test)]
++mod test_tree {
++ use helix_core::movement::Direction;
++
++ use super::Tree;
++
++ #[test]
++ fn test_get() {
++ let result = Tree::new(
++ "root",
++ vec![
++ Tree::new("foo", vec![Tree::new("bar", vec![])]),
++ Tree::new(
++ "spam",
++ vec![Tree::new("jar", vec![Tree::new("yo", vec![])])],
++ ),
++ ],
++ );
++ assert_eq!(result.get(0).unwrap().item, "root");
++ assert_eq!(result.get(1).unwrap().item, "foo");
++ assert_eq!(result.get(2).unwrap().item, "bar");
++ assert_eq!(result.get(3).unwrap().item, "spam");
++ assert_eq!(result.get(4).unwrap().item, "jar");
++ assert_eq!(result.get(5).unwrap().item, "yo");
++ }
++
++ #[test]
++ fn test_iter() {
++ let tree = Tree::new(
++ "spam",
++ vec![
++ Tree::new("jar", vec![Tree::new("yo", vec![])]),
++ Tree::new("foo", vec![Tree::new("bar", vec![])]),
++ ],
++ );
++
++ let mut iter = tree.iter();
++ assert_eq!(iter.next().map(|tree| tree.item), Some("spam"));
++ assert_eq!(iter.next().map(|tree| tree.item), Some("jar"));
++ assert_eq!(iter.next().map(|tree| tree.item), Some("yo"));
++ assert_eq!(iter.next().map(|tree| tree.item), Some("foo"));
++ assert_eq!(iter.next().map(|tree| tree.item), Some("bar"));
++
++ assert_eq!(iter.next().map(|tree| tree.item), None)
++ }
++
++ #[test]
++ fn test_iter_double_ended() {
++ let tree = Tree::new(
++ "spam",
++ vec![
++ Tree::new("jar", vec![Tree::new("yo", vec![])]),
++ Tree::new("foo", vec![Tree::new("bar", vec![])]),
++ ],
++ );
++
++ let mut iter = tree.iter();
++ assert_eq!(iter.next_back().map(|tree| tree.item), Some("bar"));
++ assert_eq!(iter.next_back().map(|tree| tree.item), Some("foo"));
++ assert_eq!(iter.next_back().map(|tree| tree.item), Some("yo"));
++ assert_eq!(iter.next_back().map(|tree| tree.item), Some("jar"));
++ assert_eq!(iter.next_back().map(|tree| tree.item), Some("spam"));
++ assert_eq!(iter.next_back().map(|tree| tree.item), None)
++ }
++
++ #[test]
++ fn test_len() {
++ let tree = Tree::new(
++ "spam",
++ vec![
++ Tree::new("jar", vec![Tree::new("yo", vec![])]),
++ Tree::new("foo", vec![Tree::new("bar", vec![])]),
++ ],
++ );
++
++ assert_eq!(tree.len(), 5)
++ }
++
++ #[test]
++ fn test_find_forward() {
++ let tree = Tree::new(
++ ".cargo",
++ vec![
++ Tree::new("jar", vec![Tree::new("Cargo.toml", vec![])]),
++ Tree::new("Cargo.toml", vec![Tree::new("bar", vec![])]),
++ ],
++ );
++ let result = tree.find(0, Direction::Forward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(0));
++
++ let result = tree.find(1, Direction::Forward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(2));
++
++ let result = tree.find(2, Direction::Forward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(2));
++
++ let result = tree.find(3, Direction::Forward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(3));
++
++ let result = tree.find(4, Direction::Forward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(0));
++ }
++
++ #[test]
++ fn test_find_backward() {
++ let tree = Tree::new(
++ ".cargo",
++ vec![
++ Tree::new("jar", vec![Tree::new("Cargo.toml", vec![])]),
++ Tree::new("Cargo.toml", vec![Tree::new("bar", vec![])]),
++ ],
++ );
++ let result = tree.find(0, Direction::Backward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(3));
++
++ let result = tree.find(1, Direction::Backward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(0));
++
++ let result = tree.find(2, Direction::Backward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(0));
++
++ let result = tree.find(3, Direction::Backward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(2));
++
++ let result = tree.find(4, Direction::Backward, |tree| {
++ tree.item.to_lowercase().contains(&"cargo".to_lowercase())
++ });
++
++ assert_eq!(result, Some(3));
++ }
++}
+--
+2.41.0
+
diff --git a/0012-Add-rainbow-tree-sitter-matches.patch b/0012-Add-rainbow-tree-sitter-matches.patch
new file mode 100644
index 00000000..e4b09a6f
--- /dev/null
+++ b/0012-Add-rainbow-tree-sitter-matches.patch
@@ -0,0 +1,2432 @@
+From bbc6c8ad315d6a8830d41f1eac59ddc8b31e799c Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 19:00:54 -0700
+Subject: [PATCH 1/2] Add rainbow tree-sitter highlights
+
+ref: https://github.com/helix-editor/helix/pull/2857
+---
+ book/src/SUMMARY.md | 1 +
+ book/src/configuration.md | 1 +
+ book/src/guides/README.md | 2 +-
+ book/src/guides/rainbow_bracket_queries.md | 132 +++++++
+ book/src/languages.md | 2 +
+ book/src/themes.md | 11 +
+ helix-core/src/syntax.rs | 417 +++++++++++++++++----
+ helix-term/src/health.rs | 11 +-
+ helix-term/src/ui/editor.rs | 53 +++
+ helix-view/src/editor.rs | 6 +-
+ helix-view/src/theme.rs | 113 +++++-
+ xtask/src/querycheck.rs | 1 +
+ 12 files changed, 669 insertions(+), 81 deletions(-)
+ create mode 100644 book/src/guides/rainbow_bracket_queries.md
+
+diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
+index ba330cf7..dcd128de 100644
+--- a/book/src/SUMMARY.md
++++ b/book/src/SUMMARY.md
+@@ -17,3 +17,4 @@ # Summary
+ - [Adding textobject queries](./guides/textobject.md)
+ - [Adding indent queries](./guides/indent.md)
+ - [Adding injection queries](./guides/injection.md)
++ - [Adding rainbow bracket queries](./guides/rainbow_bracket_queries.md)
+diff --git a/book/src/configuration.md b/book/src/configuration.md
+index bed20b28..32a4aac4 100644
+--- a/book/src/configuration.md
++++ b/book/src/configuration.md
+@@ -65,6 +65,7 @@ ### `[editor]` Section
+ | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
+ | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
+ | `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
++| `rainbow-brackets` | Whether to render rainbow colors for matching brackets. Requires tree-sitter `rainbows.scm` queries for the language. | `false` |
+
+ ### `[editor.statusline]` Section
+
+diff --git a/book/src/guides/README.md b/book/src/guides/README.md
+index c25768e6..e53983d6 100644
+--- a/book/src/guides/README.md
++++ b/book/src/guides/README.md
+@@ -1,4 +1,4 @@
+ # Guides
+
+ This section contains guides for adding new language server configurations,
+-tree-sitter grammars, textobject queries, and other similar items.
++tree-sitter grammars, textobject and rainbow bracket queries, and other similar items.
+diff --git a/book/src/guides/rainbow_bracket_queries.md b/book/src/guides/rainbow_bracket_queries.md
+new file mode 100644
+index 00000000..1cba6a99
+--- /dev/null
++++ b/book/src/guides/rainbow_bracket_queries.md
+@@ -0,0 +1,132 @@
++# Adding Rainbow Bracket Queries
++
++Helix uses `rainbows.scm` tree-sitter query files to provide rainbow bracket
++functionality.
++
++Tree-sitter queries are documented in the tree-sitter online documentation.
++If you're writing queries for the first time, be sure to check out the section
++on [syntax highlighting queries] and on [query syntax].
++
++Rainbow queries have two captures: `@rainbow.scope` and `@rainbow.bracket`.
++`@rainbow.scope` should capture any node that increases the nesting level
++while `@rainbow.bracket` should capture any bracket nodes. Put another way:
++`@rainbow.scope` switches to the next rainbow color for all nodes in the tree
++under it while `@rainbow.bracket` paints captured nodes with the current
++rainbow color.
++
++For an example, let's add rainbow queries for the tree-sitter query (TSQ)
++language itself. These queries will go into a
++`runtime/queries/tsq/rainbows.scm` file in the repository root.
++
++First we'll add the `@rainbow.bracket` captures. TSQ only has parentheses and
++square brackets:
++
++```tsq
++["(" ")" "[" "]"] @rainbow.bracket
++```
++
++The ordering of the nodes within the alternation (square brackets) is not
++taken into consideration.
++
++> Note: Why are these nodes quoted? Most syntax highlights capture text
++> surrounded by parentheses. These are _named nodes_ and correspond to the
++> names of rules in the grammar. Brackets are usually written in tree-sitter
++> grammars as literal strings, for example:
++>
++> ```js
++> {
++> // ...
++> arguments: seq("(", repeat($.argument), ")"),
++> // ...
++> }
++> ```
++>
++> Nodes written as literal strings in tree-sitter grammars may be captured
++> in queries with those same literal strings.
++
++Then we'll add `@rainbow.scope` captures. The easiest way to do this is to
++view the `grammar.js` file in the tree-sitter grammar's repository. For TSQ,
++that file is [here][tsq grammar.js]. As we scroll down the `grammar.js`, we
++see that the `(alternation)`, (L36) `(group)` (L57), `(named_node)` (L59),
++`(predicate)` (L87) and `(wildcard_node)` (L97) nodes all contain literal
++parentheses or square brackets in their definitions. These nodes are all
++direct parents of brackets and happen to also be the nodes we want to change
++to the next rainbow color, so we capture them as `@rainbow.scope`.
++
++```tsq
++[
++ (group)
++ (named_node)
++ (wildcard_node)
++ (predicate)
++ (alternation)
++] @rainbow.scope
++```
++
++This strategy works as a rule of thumb for most programming and configuration
++languages. Markup languages can be trickier and may take additional
++experimentation to find the correct nodes to use for scopes and brackets.
++
++The `:tree-sitter-subtree` command shows the syntax tree under the primary
++selection in S-expression format and can be a useful tool for determining how
++to write a query.
++
++### Properties
++
++The `rainbow.include-children` property may be applied to `@rainbow.scope`
++captures. By default, all `@rainbow.bracket` captures must be direct descendant
++of a node captured with `@rainbow.scope` in a syntax tree in order to be
++highlighted. The `rainbow.include-children` property disables that check and
++allows `@rainbow.bracket` captures to be highlighted if they are direct or
++indirect descendants of some node captured with `@rainbow.scope`.
++
++For example, this property is used in the HTML rainbow queries.
++
++For a document like `<a>link</a>`, the syntax tree is:
++
++```tsq
++(element ; <a>link</a>
++ (start_tag ; <a>
++ (tag_name)) ; a
++ (text) ; link
++ (end_tag ; </a>
++ (tag_name))) ; a
++```
++
++If we want to highlight the `<`, `>` and `</` nodes with rainbow colors, we
++capture them as `@rainbow.bracket`:
++
++```tsq
++["<" ">" "</"] @rainbow.bracket
++```
++
++And we capture `(element)` as `@rainbow.scope` because `(element)` nodes nest
++within each other: they increment the nesting level and switch to the next
++color in the rainbow.
++
++```tsq
++(element) @rainbow.scope
++```
++
++But this combination of `@rainbow.scope` and `@rainbow.bracket` will not
++highlight any nodes. `<`, `>` and `</` are children of the `(start_tag)` and
++`(end_tag)` nodes. We can't capture `(start_tag)` and `(end_tag)` as
++`@rainbow.scope` because they don't nest other elements. We can fix this case
++by removing the requirement that `<`, `>` and `</` are direct descendants of
++`(element)` using the `rainbow.include-children` property.
++
++```tsq
++((element) @rainbow.scope
++ (#set! rainbow.include-children))
++```
++
++With this property set, `<`, `>`, and `</` will highlight with rainbow colors
++even though they aren't direct descendents of the `(element)` node.
++
++`rainbow.include-children` is not necessary for the vast majority of programming
++languages. It is only necessary when the node that increments the nesting level
++(changes rainbow color) is not the direct parent of the bracket node.
++
++[syntax highlighting queries]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#highlights
++[query syntax]: https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries
++[tsq grammar.js]: https://github.com/the-mikedavis/tree-sitter-tsq/blob/48b5e9f82ae0a4727201626f33a17f69f8e0ff86/grammar.js
+diff --git a/book/src/languages.md b/book/src/languages.md
+index 5e56a332..7c07a39c 100644
+--- a/book/src/languages.md
++++ b/book/src/languages.md
+@@ -68,6 +68,8 @@ ## Language configuration
+ | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
+ | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
+ | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
++| `rulers` | Overrides the `editor.rulers` config key for the language. |
++| `rainbow-brackets` | Overrides the `editor.rainbow-brackets` config key for the language. |
+
+ ### File-type detection and the `file-types` key
+
+diff --git a/book/src/themes.md b/book/src/themes.md
+index 41a3fe10..a70dd7cc 100644
+--- a/book/src/themes.md
++++ b/book/src/themes.md
+@@ -136,6 +136,17 @@ # Override colors in the palette:
+ berry = "#2A2A4D"
+ ```
+
++### Rainbow
++
++The `rainbow` key is used for rainbow highlight for matching brackets.
++The key is a list of styles.
++
++```toml
++rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }]
++```
++
++Colors from the palette and modifiers may be used.
++
+ ### Scopes
+
+ The following is a list of scopes available to use for styling:
+diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
+index f43b03ad..d0f86094 100644
+--- a/helix-core/src/syntax.rs
++++ b/helix-core/src/syntax.rs
+@@ -154,6 +154,9 @@ pub struct LanguageConfiguration {
+ /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
+ /// Falling back to the current working directory if none are configured.
+ pub workspace_lsp_roots: Option<Vec<PathBuf>>,
++
++ /// If set, overrides rainbow brackets for a language.
++ pub rainbow_brackets: Option<bool>,
+ }
+
+ #[derive(Debug, PartialEq, Eq, Hash)]
+@@ -624,6 +627,8 @@ fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfigu
+ // always highlight syntax errors
+ // highlights_query += "\n(ERROR) @error";
+
++ let rainbows_query = read_query(&self.language_id, "rainbows.scm");
++
+ let injections_query = read_query(&self.language_id, "injections.scm");
+ let locals_query = read_query(&self.language_id, "locals.scm");
+
+@@ -642,6 +647,7 @@ fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfigu
+ let config = HighlightConfiguration::new(
+ language,
+ &highlights_query,
++ &rainbows_query,
+ &injections_query,
+ &locals_query,
+ )
+@@ -915,6 +921,36 @@ pub struct TsParser {
+ })
+ }
+
++/// Creates an iterator over the captures in a query within the given range,
++/// re-using a cursor from the pool if available.
++/// SAFETY: The `QueryCaptures` must be droped before the `QueryCursor` is dropped.
++unsafe fn query_captures<'a, 'tree>(
++ query: &'a Query,
++ root: Node<'tree>,
++ source: RopeSlice<'a>,
++ range: Option<std::ops::Range<usize>>,
++) -> (QueryCursor, QueryCaptures<'a, 'tree, RopeProvider<'a>>) {
++ // 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)
++ });
++
++ // This is the unsafe line:
++ // 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 = mem::transmute::<_, &'static mut QueryCursor>(&mut cursor);
++
++ // if reusing cursors & no range this resets to whole range
++ cursor_ref.set_byte_range(range.unwrap_or(0..usize::MAX));
++ cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
++
++ let captures = cursor_ref.captures(query, root, RopeProvider(source));
++
++ (cursor, captures)
++}
++
+ #[derive(Debug)]
+ pub struct Syntax {
+ layers: HopSlotMap<LayerId, LanguageLayer>,
+@@ -1248,6 +1284,46 @@ pub fn tree(&self) -> &Tree {
+ self.layers[self.root].tree()
+ }
+
++ /// Iterate over all captures for a query across injection layers.
++ fn query_iter<'a, F>(
++ &'a self,
++ query_fn: F,
++ source: RopeSlice<'a>,
++ range: Option<std::ops::Range<usize>>,
++ ) -> impl Iterator<Item = (&'a LanguageLayer, QueryMatch<'a, 'a>, usize)>
++ where
++ F: Fn(&'a HighlightConfiguration) -> &'a Query,
++ {
++ let mut layers: Vec<_> = self
++ .layers
++ .iter()
++ .filter_map(|(_, layer)| {
++ let (cursor, captures) = unsafe {
++ query_captures(
++ query_fn(&layer.config),
++ layer.tree().root_node(),
++ source,
++ range.clone(),
++ )
++ };
++ let mut captures = captures.peekable();
++
++ // If there aren't any captures for this layer, skip the layer.
++ captures.peek()?;
++
++ Some(QueryIterLayer {
++ cursor,
++ captures: RefCell::new(captures),
++ layer,
++ })
++ })
++ .collect();
++
++ layers.sort_unstable_by_key(|layer| layer.sort_key());
++
++ QueryIter { layers }
++ }
++
+ /// Iterate over the highlighted regions for a given slice of source code.
+ pub fn highlight_iter<'a>(
+ &'a self,
+@@ -1255,37 +1331,23 @@ pub fn highlight_iter<'a>(
+ range: Option<std::ops::Range<usize>>,
+ cancellation_flag: Option<&'a AtomicUsize>,
+ ) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a {
+- let mut layers = self
++ let mut layers: Vec<_> = self
+ .layers
+ .iter()
+ .filter_map(|(_, layer)| {
+ // TODO: if range doesn't overlap layer range, skip it
+
+- // 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));
+- cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
+-
+- let mut captures = cursor_ref
+- .captures(
++ let (cursor, captures) = unsafe {
++ query_captures(
+ &layer.config.query,
+ layer.tree().root_node(),
+- RopeProvider(source),
++ source,
++ range.clone(),
+ )
+- .peekable();
++ };
++ let mut captures = captures.peekable();
+
+- // If there's no captures, skip the layer
++ // If there are no captures, skip the layer
+ captures.peek()?;
+
+ Some(HighlightIterLayer {
+@@ -1302,11 +1364,13 @@ pub fn highlight_iter<'a>(
+ depth: layer.depth, // TODO: just reuse `layer`
+ })
+ })
+- .collect::<Vec<_>>();
++ .collect();
+
+ layers.sort_unstable_by_key(|layer| layer.sort_key());
+
+- let mut result = HighlightIter {
++ sort_layers(&mut layers);
++
++ HighlightIter {
+ source,
+ byte_offset: range.map_or(0, |r| r.start),
+ cancellation_flag,
+@@ -1314,9 +1378,95 @@ pub fn highlight_iter<'a>(
+ layers,
+ next_event: None,
+ last_highlight_range: None,
+- };
+- result.sort_layers();
+- result
++ }
++ }
++
++ /// Queries for rainbow highlights in the given range.
++ pub fn rainbow_spans<'a>(
++ &'a self,
++ source: RopeSlice<'a>,
++ query_range: Option<std::ops::Range<usize>>,
++ rainbow_length: usize,
++ ) -> Vec<(usize, std::ops::Range<usize>)> {
++ struct RainbowScope {
++ end: usize,
++ node_id: Option<usize>,
++ highlight: usize,
++ }
++
++ let mut spans = Vec::new();
++ let mut scope_stack: Vec<RainbowScope> = Vec::new();
++
++ // Calculating rainbow highlights is similar to determining local highlights
++ // in the highlight iterator. We iterate over the query captures for
++ // `@rainbow.scope` and `@rainbow.bracket`:
++ //
++ // * `@rainbow.scope`: pushes a new `RainbowScope` onto the `scope_stack`
++ // stack. The number of `RainbowScope`s is the level of nesting within
++ // brackets and determines which color of the rainbow should be used as
++ // a highlight: `scope_stack.len() % rainbow_length`.
++ //
++ // * `@rainbow.bracket`: adds a new highlight span to the `spans` Vec.
++ // A `@rainbow.bracket` capture only creates a new highlight if that node
++ // is a child node of the latest node captured with `@rainbow.scope`,
++ // or if the last `RainbowScope` on the `scope_stack` was captured with
++ // the `(set! rainbow.include-children)` property.
++ //
++ // The iterator over the query captures returns captures across injection
++ // layers sorted by the earliest captures in the document first, so
++ // highlight colors are calculated correctly across injection layers.
++
++ // Iterate over all of the captures for rainbow queries across injections.
++ for (layer, match_, capture_index) in
++ self.query_iter(|config| &config.rainbow_query, source, query_range)
++ {
++ let capture = match_.captures[capture_index];
++ let range = capture.node.byte_range();
++
++ // If any scope in the stack ends before this new capture begins,
++ // pop the scope out of the scope stack.
++ while let Some(scope) = scope_stack.last() {
++ if range.start >= scope.end {
++ scope_stack.pop();
++ } else {
++ break;
++ }
++ }
++
++ if Some(capture.index) == layer.config.rainbow_scope_capture_index {
++ // If the capture is a "rainbow.scope", push it onto the scope stack.
++ let mut scope = RainbowScope {
++ end: range.end,
++ node_id: Some(capture.node.id()),
++ highlight: scope_stack.len() % rainbow_length,
++ };
++ for prop in layer
++ .config
++ .rainbow_query
++ .property_settings(match_.pattern_index)
++ {
++ if prop.key.as_ref() == "rainbow.include-children" {
++ scope.node_id = None;
++ }
++ }
++ scope_stack.push(scope);
++ } else if Some(capture.index) == layer.config.rainbow_bracket_capture_index {
++ // If the capture is a "rainbow.bracket", check that the top of the scope stack
++ // is a valid scope for the bracket. The scope is valid if:
++ // * The scope's node is the direct parent of the captured node.
++ // * The scope has the "rainbow.include-children" property set. This allows the
++ // scope to match all descendant nodes in its range.
++ if let Some(scope) = scope_stack.last() {
++ if scope.node_id.is_none()
++ || capture.node.parent().map(|p| p.id()) == scope.node_id
++ {
++ spans.push((scope.highlight, range));
++ }
++ }
++ }
++ }
++
++ spans
+ }
+
+ // Commenting
+@@ -1331,6 +1481,18 @@ pub fn highlight_iter<'a>(
+ // TODO: Folding
+ }
+
++/// Finds the child of `node` which contains the given byte range `range`.
++pub fn child_for_byte_range(node: Node, range: std::ops::Range<usize>) -> Option<Node> {
++ for child in node.children(&mut node.walk()) {
++ let child_range = child.byte_range();
++ if range.start >= child_range.start && range.end <= child_range.end {
++ return Some(child);
++ }
++ }
++
++ None
++}
++
+ bitflags! {
+ /// Flags that track the status of a layer
+ /// in the `Sytaxn::update` function
+@@ -1558,7 +1720,8 @@ pub enum HighlightEvent {
+ #[derive(Debug)]
+ pub struct HighlightConfiguration {
+ pub language: Grammar,
+- pub query: Query,
++ query: Query,
++ rainbow_query: Query,
+ injections_query: Query,
+ combined_injections_query: Option<Query>,
+ highlights_pattern_index: usize,
+@@ -1572,6 +1735,8 @@ pub struct HighlightConfiguration {
+ local_def_capture_index: Option<u32>,
+ local_def_value_capture_index: Option<u32>,
+ local_ref_capture_index: Option<u32>,
++ rainbow_scope_capture_index: Option<u32>,
++ rainbow_bracket_capture_index: Option<u32>,
+ }
+
+ #[derive(Debug)]
+@@ -1656,6 +1821,7 @@ impl HighlightConfiguration {
+ pub fn new(
+ language: Grammar,
+ highlights_query: &str,
++ rainbow_query: &str,
+ injection_query: &str,
+ locals_query: &str,
+ ) -> Result<Self, QueryError> {
+@@ -1675,6 +1841,7 @@ pub fn new(
+ highlights_pattern_index += 1;
+ }
+ }
++ let rainbow_query = Query::new(language, rainbow_query)?;
+
+ let mut injections_query = Query::new(language, injection_query)?;
+
+@@ -1717,6 +1884,8 @@ pub fn new(
+ let mut local_def_value_capture_index = None;
+ let mut local_ref_capture_index = None;
+ let mut local_scope_capture_index = None;
++ let mut rainbow_scope_capture_index = None;
++ let mut rainbow_bracket_capture_index = None;
+ for (i, name) in query.capture_names().iter().enumerate() {
+ let i = Some(i as u32);
+ match name.as_str() {
+@@ -1728,6 +1897,15 @@ pub fn new(
+ }
+ }
+
++ for (i, name) in rainbow_query.capture_names().iter().enumerate() {
++ let i = Some(i as u32);
++ match name.as_str() {
++ "rainbow.scope" => rainbow_scope_capture_index = i,
++ "rainbow.bracket" => rainbow_bracket_capture_index = i,
++ _ => {}
++ }
++ }
++
+ for (i, name) in injections_query.capture_names().iter().enumerate() {
+ let i = Some(i as u32);
+ match name.as_str() {
+@@ -1743,6 +1921,7 @@ pub fn new(
+ Ok(Self {
+ language,
+ query,
++ rainbow_query,
+ injections_query,
+ combined_injections_query,
+ highlights_pattern_index,
+@@ -1756,6 +1935,8 @@ pub fn new(
+ local_def_capture_index,
+ local_def_value_capture_index,
+ local_ref_capture_index,
++ rainbow_scope_capture_index,
++ rainbow_bracket_capture_index,
+ })
+ }
+
+@@ -1896,11 +2077,21 @@ fn injection_for_match<'a>(
+ }
+ }
+
+-impl<'a> HighlightIterLayer<'a> {
+- // 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.
+- fn sort_key(&self) -> Option<(usize, bool, isize)> {
++trait IterLayer {
++ type SortKey: PartialOrd;
++
++ fn sort_key(&self) -> Option<Self::SortKey>;
++
++ fn cursor(self) -> QueryCursor;
++}
++
++impl<'a> IterLayer for HighlightIterLayer<'a> {
++ type SortKey = (usize, bool, isize);
++
++ fn sort_key(&self) -> Option<Self::SortKey> {
++ // 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.
+ let depth = -(self.depth as isize);
+ let next_start = self
+ .captures
+@@ -1921,6 +2112,82 @@ fn sort_key(&self) -> Option<(usize, bool, isize)> {
+ _ => None,
+ }
+ }
++
++ fn cursor(self) -> QueryCursor {
++ self.cursor
++ }
++}
++
++impl<'a> IterLayer for QueryIterLayer<'a> {
++ type SortKey = (usize, isize);
++
++ fn sort_key(&self) -> Option<Self::SortKey> {
++ // Sort the layers so that the first layer in the Vec has the next
++ // capture ordered by start byte and depth (descending).
++ let depth = -(self.layer.depth as isize);
++ let mut captures = self.captures.borrow_mut();
++ let (match_, capture_index) = captures.peek()?;
++ let start = match_.captures[*capture_index].node.start_byte();
++
++ Some((start, depth))
++ }
++
++ fn cursor(self) -> QueryCursor {
++ self.cursor
++ }
++}
++
++/// Re-sort the given layers so that the next capture for the `layers[0]` is
++/// the earliest capture in the document for all layers.
++///
++/// This function assumes that `layers` is already sorted except for the
++/// first layer in the `Vec`. This function shifts the first layer later in
++/// the `Vec` after any layers with earlier captures.
++///
++/// This is quicker than a regular full sort: it can only take as many
++/// iterations as the number of layers and usually takes many fewer than
++/// the full number of layers. The case when `layers[0]` is already the
++/// layer with the earliest capture and the sort is a no-op is a fast-lane
++/// which only takes one comparison operation.
++///
++/// This function also removes any layers which have no more query captures
++/// to emit.
++fn sort_layers<L: IterLayer>(layers: &mut Vec<L>) {
++ while !layers.is_empty() {
++ // If `Layer::sort_key` returns `None`, the layer has no more captures
++ // to emit and can be removed.
++ if let Some(sort_key) = layers[0].sort_key() {
++ let mut i = 0;
++ while i + 1 < layers.len() {
++ if let Some(next_offset) = layers[i + 1].sort_key() {
++ // Compare `0`'s sort key to `i + 1`'s. If `i + 1` comes
++ // before `0`, shift the `0` layer so it comes after the
++ // `i + 1` layers.
++ if next_offset < sort_key {
++ i += 1;
++ continue;
++ }
++ } else {
++ let layer = layers.remove(i + 1);
++ PARSER.with(|ts_parser| {
++ let highlighter = &mut ts_parser.borrow_mut();
++ highlighter.cursors.push(layer.cursor());
++ });
++ }
++ break;
++ }
++ if i > 0 {
++ layers[0..(i + 1)].rotate_left(1);
++ }
++ break;
++ } else {
++ let layer = layers.remove(0);
++ PARSER.with(|ts_parser| {
++ let highlighter = &mut ts_parser.borrow_mut();
++ highlighter.cursors.push(layer.cursor());
++ });
++ }
++ }
+ }
+
+ #[derive(Clone)]
+@@ -2051,42 +2318,9 @@ fn emit_event(
+ } else {
+ result = event.map(Ok);
+ }
+- self.sort_layers();
++ sort_layers(&mut self.layers);
+ result
+ }
+-
+- fn sort_layers(&mut self) {
+- while !self.layers.is_empty() {
+- if let Some(sort_key) = self.layers[0].sort_key() {
+- let mut i = 0;
+- while i + 1 < self.layers.len() {
+- if let Some(next_offset) = self.layers[i + 1].sort_key() {
+- if next_offset < sort_key {
+- 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;
+- }
+- if i > 0 {
+- self.layers[0..(i + 1)].rotate_left(1);
+- }
+- break;
+- } else {
+- let layer = self.layers.remove(0);
+- PARSER.with(|ts_parser| {
+- let highlighter = &mut ts_parser.borrow_mut();
+- highlighter.cursors.push(layer.cursor);
+- });
+- }
+- }
+- }
+ }
+
+ impl<'a> Iterator for HighlightIter<'a> {
+@@ -2238,7 +2472,7 @@ fn next(&mut self) -> Option<Self::Item> {
+ }
+ }
+
+- self.sort_layers();
++ sort_layers(&mut self.layers);
+ continue 'main;
+ }
+
+@@ -2247,7 +2481,7 @@ fn next(&mut self) -> Option<Self::Item> {
+ // a different layer, then skip over this one.
+ if let Some((last_start, last_end, last_depth)) = self.last_highlight_range {
+ if range.start == last_start && range.end == last_end && layer.depth < last_depth {
+- self.sort_layers();
++ sort_layers(&mut self.layers);
+ continue 'main;
+ }
+ }
+@@ -2265,7 +2499,7 @@ fn next(&mut self) -> Option<Self::Item> {
+ }
+ }
+
+- self.sort_layers();
++ sort_layers(&mut self.layers);
+ continue 'main;
+ }
+ }
+@@ -2300,7 +2534,7 @@ fn next(&mut self) -> Option<Self::Item> {
+ .emit_event(range.start, Some(HighlightEvent::HighlightStart(highlight)));
+ }
+
+- self.sort_layers();
++ sort_layers(&mut self.layers);
+ }
+ }
+ }
+@@ -2510,6 +2744,42 @@ fn pretty_print_tree_impl<W: fmt::Write>(
+ Ok(())
+ }
+
++struct QueryIterLayer<'a> {
++ cursor: QueryCursor,
++ captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>>,
++ layer: &'a LanguageLayer,
++}
++
++impl<'a> fmt::Debug for QueryIterLayer<'a> {
++ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
++ f.debug_struct("QueryIterLayer").finish()
++ }
++}
++
++#[derive(Debug)]
++pub struct QueryIter<'a> {
++ layers: Vec<QueryIterLayer<'a>>,
++}
++
++impl<'a> Iterator for QueryIter<'a> {
++ type Item = (&'a LanguageLayer, QueryMatch<'a, 'a>, usize);
++
++ fn next(&mut self) -> Option<Self::Item> {
++ // Sort the layers so that the first layer contains the next capture.
++ sort_layers(&mut self.layers);
++
++ // Emit the next capture from the lowest layer. If there are no more
++ // layers, terminate.
++ let layer = self.layers.get_mut(0)?;
++ let inner = layer.layer;
++ layer
++ .captures
++ .borrow_mut()
++ .next()
++ .map(|(match_, index)| (inner, match_, index))
++ }
++}
++
+ #[cfg(test)]
+ mod test {
+ use super::*;
+@@ -2539,7 +2809,7 @@ fn test_textobject_queries() {
+ let textobject = TextObjectQuery { query };
+ let mut cursor = QueryCursor::new();
+
+- let config = HighlightConfiguration::new(language, "", "", "").unwrap();
++ let config = HighlightConfiguration::new(language, "", "", "", "").unwrap();
+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
+
+ let root = syntax.tree().root_node();
+@@ -2601,6 +2871,7 @@ fn test_parser() {
+ language,
+ &std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm")
+ .unwrap(),
++ "", // rainbows.scm
+ &std::fs::read_to_string("../runtime/grammars/sources/rust/queries/injections.scm")
+ .unwrap(),
+ "", // locals.scm
+@@ -2703,7 +2974,7 @@ fn assert_pretty_print(
+ });
+ let language = get_language(language_name).unwrap();
+
+- let config = HighlightConfiguration::new(language, "", "", "").unwrap();
++ let config = HighlightConfiguration::new(language, "", "", "", "").unwrap();
+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
+
+ let root = syntax
+diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs
+index 8f921877..462cdf3f 100644
+--- a/helix-term/src/health.rs
++++ b/helix-term/src/health.rs
+@@ -12,11 +12,17 @@ pub enum TsFeature {
+ Highlight,
+ TextObject,
+ AutoIndent,
++ RainbowBrackets,
+ }
+
+ impl TsFeature {
+ pub fn all() -> &'static [Self] {
+- &[Self::Highlight, Self::TextObject, Self::AutoIndent]
++ &[
++ Self::Highlight,
++ Self::TextObject,
++ Self::AutoIndent,
++ Self::RainbowBrackets,
++ ]
+ }
+
+ pub fn runtime_filename(&self) -> &'static str {
+@@ -24,6 +30,7 @@ pub fn runtime_filename(&self) -> &'static str {
+ Self::Highlight => "highlights.scm",
+ Self::TextObject => "textobjects.scm",
+ Self::AutoIndent => "indents.scm",
++ Self::RainbowBrackets => "rainbows.scm",
+ }
+ }
+
+@@ -32,6 +39,7 @@ pub fn long_title(&self) -> &'static str {
+ Self::Highlight => "Syntax Highlighting",
+ Self::TextObject => "Treesitter Textobjects",
+ Self::AutoIndent => "Auto Indent",
++ Self::RainbowBrackets => "Rainbow Brackets",
+ }
+ }
+
+@@ -40,6 +48,7 @@ pub fn short_title(&self) -> &'static str {
+ Self::Highlight => "Highlight",
+ Self::TextObject => "Textobject",
+ Self::AutoIndent => "Indent",
++ Self::RainbowBrackets => "Rainbow",
+ }
+ }
+ }
+diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
+index 81f8fe22..63590e94 100644
+--- a/helix-term/src/ui/editor.rs
++++ b/helix-term/src/ui/editor.rs
+@@ -94,6 +94,11 @@ pub fn render_view(
+ let theme = &editor.theme;
+ let config = editor.config();
+
++ let should_render_rainbow_brackets = doc
++ .language_config()
++ .and_then(|lang_config| lang_config.rainbow_brackets)
++ .unwrap_or(config.rainbow_brackets);
++
+ let text_annotations = view.text_annotations(doc, Some(theme));
+ let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
+ let mut translated_positions: Vec<TranslatedPosition> = Vec::new();
+@@ -125,6 +130,12 @@ pub fn render_view(
+
+ let mut highlights =
+ Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
++ if should_render_rainbow_brackets {
++ highlights = Box::new(syntax::merge(
++ highlights,
++ Self::doc_rainbow_highlights(doc, view.offset.anchor, inner.height, theme),
++ ));
++ }
+ let overlay_highlights = Self::overlay_syntax_highlights(
+ doc,
+ view.offset.anchor,
+@@ -330,6 +341,48 @@ pub fn doc_syntax_highlights<'doc>(
+ }
+ }
+
++ pub fn doc_rainbow_highlights(
++ doc: &Document,
++ anchor: usize,
++ height: u16,
++ theme: &Theme,
++ ) -> Vec<(usize, std::ops::Range<usize>)> {
++ let syntax = match doc.syntax() {
++ Some(syntax) => syntax,
++ None => return Vec::new(),
++ };
++
++ let text = doc.text().slice(..);
++ let row = text.char_to_line(anchor.min(text.len_chars()));
++
++ // calculate viewport byte ranges
++ let last_line = doc.text().len_lines().saturating_sub(1);
++ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
++ let visible_start = text.line_to_byte(row.min(last_line));
++ let visible_end = text.line_to_byte(last_visible_line + 1);
++
++ // The calculation for the current nesting level for rainbow highlights
++ // depends on where we start the iterator from. For accuracy, we start
++ // the iterator further back than the viewport: at the start of the containing
++ // non-root syntax-tree node. Any spans that are off-screen are truncated when
++ // the spans are merged via [syntax::merge].
++ let syntax_node_start =
++ syntax::child_for_byte_range(syntax.tree().root_node(), visible_start..visible_start)
++ .map_or(visible_start, |node| node.byte_range().start);
++ let syntax_node_range = syntax_node_start..visible_end;
++
++ let mut spans = syntax.rainbow_spans(text, Some(syntax_node_range), theme.rainbow_length());
++
++ for (_highlight, range) in spans.iter_mut() {
++ let start = text.byte_to_char(ensure_grapheme_boundary_next_byte(text, range.start));
++ let end = text.byte_to_char(ensure_grapheme_boundary_next_byte(text, range.end));
++
++ *range = start..end;
++ }
++
++ spans
++ }
++
+ /// Get highlight spans for document diagnostics
+ pub fn doc_diagnostics_highlights(
+ doc: &Document,
+diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
+index 1ab5f976..c0410f7d 100644
+--- a/helix-view/src/editor.rs
++++ b/helix-view/src/editor.rs
+@@ -316,6 +316,8 @@ pub struct Config {
+ pub workspace_lsp_roots: Vec<PathBuf>,
+ /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
+ pub default_line_ending: LineEndingConfig,
++ /// Whether to render rainbow highlights. Defaults to `false`.
++ pub rainbow_brackets: bool,
+ }
+
+ #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+@@ -851,6 +853,7 @@ fn default() -> Self {
+ completion_replace: false,
+ workspace_lsp_roots: Vec::new(),
+ default_line_ending: LineEndingConfig::default(),
++ rainbow_brackets: false,
+ }
+ }
+ }
+@@ -1181,8 +1184,7 @@ fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
+ return;
+ }
+
+- let scopes = theme.scopes();
+- self.syn_loader.set_scopes(scopes.to_vec());
++ self.syn_loader.set_scopes(theme.scopes().to_vec());
+
+ match preview {
+ ThemeAction::Preview => {
+diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
+index bf3379ca..2b024384 100644
+--- a/helix-view/src/theme.rs
++++ b/helix-view/src/theme.rs
+@@ -216,17 +216,19 @@ pub struct Theme {
+ // tree-sitter highlight styles are stored in a Vec to optimize lookups
+ scopes: Vec<String>,
+ highlights: Vec<Style>,
++ rainbow_length: usize,
+ }
+
+ impl From<Value> for Theme {
+ fn from(value: Value) -> Self {
+ if let Value::Table(table) = value {
+- let (styles, scopes, highlights) = build_theme_values(table);
++ let (styles, scopes, highlights, rainbow_length) = build_theme_values(table);
+
+ Self {
+ styles,
+ scopes,
+ highlights,
++ rainbow_length,
+ ..Default::default()
+ }
+ } else {
+@@ -243,12 +245,13 @@ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ {
+ let values = Map::<String, Value>::deserialize(deserializer)?;
+
+- let (styles, scopes, highlights) = build_theme_values(values);
++ let (styles, scopes, highlights, rainbow_length) = build_theme_values(values);
+
+ Ok(Self {
+ styles,
+ scopes,
+ highlights,
++ rainbow_length,
+ ..Default::default()
+ })
+ }
+@@ -256,10 +259,11 @@ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
+ fn build_theme_values(
+ mut values: Map<String, Value>,
+-) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) {
++) -> (HashMap<String, Style>, Vec<String>, Vec<Style>, usize) {
+ let mut styles = HashMap::new();
+ let mut scopes = Vec::new();
+ let mut highlights = Vec::new();
++ let mut rainbow_length = 0;
+
+ // TODO: alert user of parsing failures in editor
+ let palette = values
+@@ -276,6 +280,27 @@ fn build_theme_values(
+ styles.reserve(values.len());
+ scopes.reserve(values.len());
+ highlights.reserve(values.len());
++
++ for (i, style) in values
++ .remove("rainbow")
++ .and_then(|value| match palette.parse_style_array(value) {
++ Ok(styles) => Some(styles),
++ Err(err) => {
++ warn!("{}", err);
++ None
++ }
++ })
++ .unwrap_or_else(default_rainbow)
++ .iter()
++ .enumerate()
++ {
++ let name = format!("rainbow.{}", i);
++ styles.insert(name.clone(), *style);
++ scopes.push(name);
++ highlights.push(*style);
++ rainbow_length += 1;
++ }
++
+ for (name, style_value) in values {
+ let mut style = Style::default();
+ if let Err(err) = palette.parse_style(&mut style, style_value) {
+@@ -288,7 +313,7 @@ fn build_theme_values(
+ highlights.push(style);
+ }
+
+- (styles, scopes, highlights)
++ (styles, scopes, highlights, rainbow_length)
+ }
+
+ impl Theme {
+@@ -349,6 +374,21 @@ pub fn is_16_color(&self) -> bool {
+ .all(|color| !matches!(color, Some(Color::Rgb(..))))
+ })
+ }
++
++ pub fn rainbow_length(&self) -> usize {
++ self.rainbow_length
++ }
++}
++
++fn default_rainbow() -> Vec<Style> {
++ vec![
++ Style::default().fg(Color::Red),
++ Style::default().fg(Color::Yellow),
++ Style::default().fg(Color::Green),
++ Style::default().fg(Color::Blue),
++ Style::default().fg(Color::Cyan),
++ Style::default().fg(Color::Magenta),
++ ]
+ }
+
+ struct ThemePalette {
+@@ -494,6 +534,24 @@ pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String>
+ }
+ Ok(())
+ }
++
++ /// Parses a TOML array into a [`Vec`] of [`Style`]. If the value cannot be
++ /// parsed as an array or if any style in the array cannot be parsed then an
++ /// error is returned.
++ pub fn parse_style_array(&self, value: Value) -> Result<Vec<Style>, String> {
++ let mut styles = Vec::new();
++
++ for v in value
++ .as_array()
++ .ok_or_else(|| format!("Theme: could not parse value as an array: '{}'", value))?
++ {
++ let mut style = Style::default();
++ self.parse_style(&mut style, v.clone())?;
++ styles.push(style);
++ }
++
++ Ok(styles)
++ }
+ }
+
+ impl TryFrom<Value> for ThemePalette {
+@@ -568,4 +626,51 @@ fn test_parse_style_table() {
+ .add_modifier(Modifier::BOLD)
+ );
+ }
++
++ #[test]
++ fn test_parse_valid_style_array() {
++ let theme = toml::toml! {
++ rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }]
++ };
++
++ let palette = ThemePalette::default();
++
++ let rainbow = theme.get("rainbow").unwrap();
++ let parse_result = palette.parse_style_array(rainbow.clone());
++
++ assert_eq!(
++ Ok(vec![
++ Style::default().fg(Color::Rgb(255, 0, 0)),
++ Style::default().fg(Color::Rgb(255, 165, 0)),
++ Style::default().fg(Color::Rgb(255, 240, 0)),
++ Style::default()
++ .fg(Color::Rgb(0, 255, 0))
++ .add_modifier(Modifier::BOLD),
++ ]),
++ parse_result
++ )
++ }
++
++ #[test]
++ fn test_parse_invalid_style_array() {
++ let palette = ThemePalette::default();
++
++ let theme = toml::toml! { invalid_hex_code = ["#f00"] };
++ let invalid_hex_code = theme.get("invalid_hex_code").unwrap();
++ let parse_result = palette.parse_style_array(invalid_hex_code.clone());
++
++ assert_eq!(
++ Err("Theme: malformed hexcode: #f00".to_string()),
++ parse_result
++ );
++
++ let theme = toml::toml! { not_an_array = { red = "#ff0000" } };
++ let not_an_array = theme.get("not_an_array").unwrap();
++ let parse_result = palette.parse_style_array(not_an_array.clone());
++
++ assert_eq!(
++ Err("Theme: could not parse value as an array: '{ red = \"#ff0000\" }'".to_string()),
++ parse_result
++ )
++ }
+ }
+diff --git a/xtask/src/querycheck.rs b/xtask/src/querycheck.rs
+index 454d0e5c..31de9321 100644
+--- a/xtask/src/querycheck.rs
++++ b/xtask/src/querycheck.rs
+@@ -11,6 +11,7 @@ pub fn query_check() -> Result<(), DynError> {
+ "injections.scm",
+ "textobjects.scm",
+ "indents.scm",
++ "rainbows.scm",
+ ];
+
+ for language in lang_config().language {
+--
+2.41.0
+
+
+From 28328c056dca18283faba5067ee713c6a4800760 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 19:04:35 -0700
+Subject: [PATCH 2/2] Add rainbow bracket queries
+
+---
+ book/src/generated/lang-support.md | 350 +++++++++++------------
+ runtime/queries/bash/rainbows.scm | 21 ++
+ runtime/queries/c/rainbows.scm | 29 ++
+ runtime/queries/clojure/rainbows.scm | 13 +
+ runtime/queries/common-lisp/rainbows.scm | 1 +
+ runtime/queries/cpp/rainbows.scm | 49 ++++
+ runtime/queries/css/rainbows.scm | 15 +
+ runtime/queries/ecma/rainbows.scm | 28 ++
+ runtime/queries/elixir/rainbows.scm | 24 ++
+ runtime/queries/erlang/rainbows.scm | 24 ++
+ runtime/queries/go/rainbows.scm | 33 +++
+ runtime/queries/html/rainbows.scm | 13 +
+ runtime/queries/java/rainbows.scm | 35 +++
+ runtime/queries/javascript/rainbows.scm | 1 +
+ runtime/queries/json/rainbows.scm | 9 +
+ runtime/queries/jsx/rainbows.scm | 10 +
+ runtime/queries/nix/rainbows.scm | 17 ++
+ runtime/queries/python/rainbows.scm | 30 ++
+ runtime/queries/racket/rainbows.scm | 1 +
+ runtime/queries/regex/rainbows.scm | 17 ++
+ runtime/queries/ruby/rainbows.scm | 28 ++
+ runtime/queries/rust/rainbows.scm | 60 ++++
+ runtime/queries/scheme/rainbows.scm | 12 +
+ runtime/queries/scss/rainbows.scm | 3 +
+ runtime/queries/starlark/rainbows.scm | 1 +
+ runtime/queries/toml/rainbows.scm | 12 +
+ runtime/queries/tsq/rainbows.scm | 12 +
+ runtime/queries/tsx/rainbows.scm | 2 +
+ runtime/queries/typescript/rainbows.scm | 19 ++
+ runtime/queries/xml/rainbows.scm | 29 ++
+ runtime/queries/yaml/rainbows.scm | 9 +
+ runtime/queries/zig/rainbows.scm | 42 +++
+ 32 files changed, 774 insertions(+), 175 deletions(-)
+ create mode 100644 runtime/queries/bash/rainbows.scm
+ create mode 100644 runtime/queries/c/rainbows.scm
+ create mode 100644 runtime/queries/clojure/rainbows.scm
+ create mode 100644 runtime/queries/common-lisp/rainbows.scm
+ create mode 100644 runtime/queries/cpp/rainbows.scm
+ create mode 100644 runtime/queries/css/rainbows.scm
+ create mode 100644 runtime/queries/ecma/rainbows.scm
+ create mode 100644 runtime/queries/elixir/rainbows.scm
+ create mode 100644 runtime/queries/erlang/rainbows.scm
+ create mode 100644 runtime/queries/go/rainbows.scm
+ create mode 100644 runtime/queries/html/rainbows.scm
+ create mode 100644 runtime/queries/java/rainbows.scm
+ create mode 100644 runtime/queries/javascript/rainbows.scm
+ create mode 100644 runtime/queries/json/rainbows.scm
+ create mode 100644 runtime/queries/jsx/rainbows.scm
+ create mode 100644 runtime/queries/nix/rainbows.scm
+ create mode 100644 runtime/queries/python/rainbows.scm
+ create mode 100644 runtime/queries/racket/rainbows.scm
+ create mode 100644 runtime/queries/regex/rainbows.scm
+ create mode 100644 runtime/queries/ruby/rainbows.scm
+ create mode 100644 runtime/queries/rust/rainbows.scm
+ create mode 100644 runtime/queries/scheme/rainbows.scm
+ create mode 100644 runtime/queries/scss/rainbows.scm
+ create mode 100644 runtime/queries/starlark/rainbows.scm
+ create mode 100644 runtime/queries/toml/rainbows.scm
+ create mode 100644 runtime/queries/tsq/rainbows.scm
+ create mode 100644 runtime/queries/tsx/rainbows.scm
+ create mode 100644 runtime/queries/typescript/rainbows.scm
+ create mode 100644 runtime/queries/xml/rainbows.scm
+ create mode 100644 runtime/queries/yaml/rainbows.scm
+ create mode 100644 runtime/queries/zig/rainbows.scm
+
+diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md
+index 2d0e83db..72155543 100644
+--- a/book/src/generated/lang-support.md
++++ b/book/src/generated/lang-support.md
+@@ -1,175 +1,175 @@
+-| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
+-| --- | --- | --- | --- | --- |
+-| astro | ✓ | | | |
+-| awk | ✓ | ✓ | | `awk-language-server` |
+-| bash | ✓ | | ✓ | `bash-language-server` |
+-| bass | ✓ | | | `bass` |
+-| beancount | ✓ | | | |
+-| bibtex | ✓ | | | `texlab` |
+-| bicep | ✓ | | | `bicep-langserver` |
+-| blueprint | ✓ | | | `blueprint-compiler` |
+-| c | ✓ | ✓ | ✓ | `clangd` |
+-| c-sharp | ✓ | ✓ | | `OmniSharp` |
+-| cabal | | | | |
+-| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
+-| capnp | ✓ | | ✓ | |
+-| clojure | ✓ | | | `clojure-lsp` |
+-| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
+-| comment | ✓ | | | |
+-| common-lisp | ✓ | | | `cl-lsp` |
+-| cpon | ✓ | | ✓ | |
+-| cpp | ✓ | ✓ | ✓ | `clangd` |
+-| crystal | ✓ | ✓ | | `crystalline` |
+-| css | ✓ | | | `vscode-css-language-server` |
+-| cue | ✓ | | | `cuelsp` |
+-| d | ✓ | ✓ | ✓ | `serve-d` |
+-| dart | ✓ | | ✓ | `dart` |
+-| devicetree | ✓ | | | |
+-| dhall | ✓ | ✓ | | `dhall-lsp-server` |
+-| diff | ✓ | | | |
+-| dockerfile | ✓ | | | `docker-langserver` |
+-| dot | ✓ | | | `dot-language-server` |
+-| dtd | ✓ | | | |
+-| edoc | ✓ | | | |
+-| eex | ✓ | | | |
+-| ejs | ✓ | | | |
+-| elixir | ✓ | ✓ | ✓ | `elixir-ls` |
+-| elm | ✓ | ✓ | | `elm-language-server` |
+-| elvish | ✓ | | | `elvish` |
+-| env | ✓ | | | |
+-| erb | ✓ | | | |
+-| erlang | ✓ | ✓ | | `erlang_ls` |
+-| esdl | ✓ | | | |
+-| fish | ✓ | ✓ | ✓ | |
+-| forth | ✓ | | | `forth-lsp` |
+-| fortran | ✓ | | ✓ | `fortls` |
+-| fsharp | ✓ | | | `fsautocomplete` |
+-| gdscript | ✓ | ✓ | ✓ | |
+-| git-attributes | ✓ | | | |
+-| git-commit | ✓ | ✓ | | |
+-| git-config | ✓ | | | |
+-| git-ignore | ✓ | | | |
+-| git-rebase | ✓ | | | |
+-| gleam | ✓ | ✓ | | `gleam` |
+-| glsl | ✓ | ✓ | ✓ | |
+-| go | ✓ | ✓ | ✓ | `gopls` |
+-| godot-resource | ✓ | | | |
+-| gomod | ✓ | | | `gopls` |
+-| gotmpl | ✓ | | | `gopls` |
+-| gowork | ✓ | | | `gopls` |
+-| graphql | ✓ | | | |
+-| hare | ✓ | | | |
+-| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
+-| haskell-persistent | ✓ | | | |
+-| hcl | ✓ | | ✓ | `terraform-ls` |
+-| heex | ✓ | ✓ | | `elixir-ls` |
+-| hosts | ✓ | | | |
+-| html | ✓ | | | `vscode-html-language-server` |
+-| hurl | ✓ | | ✓ | |
+-| idris | | | | `idris2-lsp` |
+-| iex | ✓ | | | |
+-| ini | ✓ | | | |
+-| java | ✓ | ✓ | | `jdtls` |
+-| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
+-| jsdoc | ✓ | | | |
+-| json | ✓ | | ✓ | `vscode-json-language-server` |
+-| jsonnet | ✓ | | | `jsonnet-language-server` |
+-| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
+-| julia | ✓ | ✓ | ✓ | `julia` |
+-| just | ✓ | ✓ | ✓ | |
+-| kdl | ✓ | | | |
+-| kotlin | ✓ | | | `kotlin-language-server` |
+-| latex | ✓ | ✓ | | `texlab` |
+-| lean | ✓ | | | `lean` |
+-| ledger | ✓ | | | |
+-| llvm | ✓ | ✓ | ✓ | |
+-| llvm-mir | ✓ | ✓ | ✓ | |
+-| llvm-mir-yaml | ✓ | | ✓ | |
+-| lua | ✓ | ✓ | ✓ | `lua-language-server` |
+-| make | ✓ | | | |
+-| markdoc | ✓ | | | `markdoc-ls` |
+-| markdown | ✓ | | | `marksman` |
+-| markdown.inline | ✓ | | | |
+-| matlab | ✓ | ✓ | ✓ | |
+-| mermaid | ✓ | | | |
+-| meson | ✓ | | ✓ | |
+-| mint | | | | `mint` |
+-| msbuild | ✓ | | ✓ | |
+-| nasm | ✓ | ✓ | | |
+-| nickel | ✓ | | ✓ | `nls` |
+-| nim | ✓ | ✓ | ✓ | `nimlangserver` |
+-| nix | ✓ | | | `nil` |
+-| nu | ✓ | | | |
+-| ocaml | ✓ | | ✓ | `ocamllsp` |
+-| ocaml-interface | ✓ | | | `ocamllsp` |
+-| odin | ✓ | | ✓ | `ols` |
+-| opencl | ✓ | ✓ | ✓ | `clangd` |
+-| openscad | ✓ | | | `openscad-lsp` |
+-| org | ✓ | | | |
+-| pascal | ✓ | ✓ | | `pasls` |
+-| passwd | ✓ | | | |
+-| pem | ✓ | | | |
+-| perl | ✓ | ✓ | ✓ | `perlnavigator` |
+-| php | ✓ | ✓ | ✓ | `intelephense` |
+-| po | ✓ | ✓ | | |
+-| ponylang | ✓ | ✓ | ✓ | |
+-| prisma | ✓ | | | `prisma-language-server` |
+-| prolog | | | | `swipl` |
+-| protobuf | ✓ | | ✓ | |
+-| prql | ✓ | | | |
+-| purescript | ✓ | | | `purescript-language-server` |
+-| python | ✓ | ✓ | ✓ | `pylsp` |
+-| qml | ✓ | | ✓ | `qmlls` |
+-| r | ✓ | | | `R` |
+-| racket | ✓ | | | `racket` |
+-| regex | ✓ | | | |
+-| rego | ✓ | | | `regols` |
+-| rescript | ✓ | ✓ | | `rescript-language-server` |
+-| rmarkdown | ✓ | | ✓ | `R` |
+-| robot | ✓ | | | `robotframework_ls` |
+-| ron | ✓ | | ✓ | |
+-| rst | ✓ | | | |
+-| ruby | ✓ | ✓ | ✓ | `solargraph` |
+-| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
+-| sage | ✓ | ✓ | | |
+-| scala | ✓ | | ✓ | `metals` |
+-| scheme | ✓ | | | |
+-| scss | ✓ | | | `vscode-css-language-server` |
+-| slint | ✓ | | ✓ | `slint-lsp` |
+-| smithy | ✓ | | | `cs` |
+-| sml | ✓ | | | |
+-| solidity | ✓ | | | `solc` |
+-| sql | ✓ | | | |
+-| sshclientconfig | ✓ | | | |
+-| starlark | ✓ | ✓ | | |
+-| svelte | ✓ | | ✓ | `svelteserver` |
+-| sway | ✓ | ✓ | ✓ | `forc` |
+-| swift | ✓ | | | `sourcekit-lsp` |
+-| t32 | ✓ | | | |
+-| tablegen | ✓ | ✓ | ✓ | |
+-| task | ✓ | | | |
+-| tfvars | ✓ | | ✓ | `terraform-ls` |
+-| toml | ✓ | | | `taplo` |
+-| tsq | ✓ | | | |
+-| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
+-| twig | ✓ | | | |
+-| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
+-| typst | ✓ | | | `typst-lsp` |
+-| ungrammar | ✓ | | | |
+-| uxntal | ✓ | | | |
+-| v | ✓ | ✓ | ✓ | `v` |
+-| vala | ✓ | | | `vala-language-server` |
+-| verilog | ✓ | ✓ | | `svlangserver` |
+-| vhdl | ✓ | | | `vhdl_ls` |
+-| vhs | ✓ | | | |
+-| vue | ✓ | | | `vue-language-server` |
+-| wast | ✓ | | | |
+-| wat | ✓ | | | |
+-| webc | ✓ | | | |
+-| wgsl | ✓ | | | `wgsl_analyzer` |
+-| wit | ✓ | | ✓ | |
+-| xit | ✓ | | | |
+-| xml | ✓ | | ✓ | |
+-| yaml | ✓ | | ✓ | `yaml-language-server` |
+-| yuck | ✓ | | | |
+-| zig | ✓ | ✓ | ✓ | `zls` |
++| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Rainbow Brackets | Default LSP |
++| --- | --- | --- | --- | --- | --- |
++| astro | ✓ | | | | |
++| awk | ✓ | ✓ | | | `awk-language-server` |
++| bash | ✓ | | ✓ | ✓ | `bash-language-server` |
++| bass | ✓ | | | | `bass` |
++| beancount | ✓ | | | | |
++| bibtex | ✓ | | | | `texlab` |
++| bicep | ✓ | | | | `bicep-langserver` |
++| blueprint | ✓ | | | | `blueprint-compiler` |
++| c | ✓ | ✓ | ✓ | ✓ | `clangd` |
++| c-sharp | ✓ | ✓ | | | `OmniSharp` |
++| cabal | | | | | |
++| cairo | ✓ | ✓ | ✓ | | `cairo-language-server` |
++| capnp | ✓ | | ✓ | | |
++| clojure | ✓ | | | ✓ | `clojure-lsp` |
++| cmake | ✓ | ✓ | ✓ | | `cmake-language-server` |
++| comment | ✓ | | | | |
++| common-lisp | ✓ | | | ✓ | `cl-lsp` |
++| cpon | ✓ | | ✓ | | |
++| cpp | ✓ | ✓ | ✓ | ✓ | `clangd` |
++| crystal | ✓ | ✓ | | | `crystalline` |
++| css | ✓ | | | ✓ | `vscode-css-language-server` |
++| cue | ✓ | | | | `cuelsp` |
++| d | ✓ | ✓ | ✓ | | `serve-d` |
++| dart | ✓ | | ✓ | | `dart` |
++| devicetree | ✓ | | | | |
++| dhall | ✓ | ✓ | | | `dhall-lsp-server` |
++| diff | ✓ | | | | |
++| dockerfile | ✓ | | | | `docker-langserver` |
++| dot | ✓ | | | | `dot-language-server` |
++| dtd | ✓ | | | | |
++| edoc | ✓ | | | | |
++| eex | ✓ | | | | |
++| ejs | ✓ | | | | |
++| elixir | ✓ | ✓ | ✓ | ✓ | `elixir-ls` |
++| elm | ✓ | ✓ | | | `elm-language-server` |
++| elvish | ✓ | | | | `elvish` |
++| env | ✓ | | | | |
++| erb | ✓ | | | | |
++| erlang | ✓ | ✓ | | ✓ | `erlang_ls` |
++| esdl | ✓ | | | | |
++| fish | ✓ | ✓ | ✓ | | |
++| forth | ✓ | | | | `forth-lsp` |
++| fortran | ✓ | | ✓ | | `fortls` |
++| fsharp | ✓ | | | | `fsautocomplete` |
++| gdscript | ✓ | ✓ | ✓ | | |
++| git-attributes | ✓ | | | | |
++| git-commit | ✓ | ✓ | | | |
++| git-config | ✓ | | | | |
++| git-ignore | ✓ | | | | |
++| git-rebase | ✓ | | | | |
++| gleam | ✓ | ✓ | | | `gleam` |
++| glsl | ✓ | ✓ | ✓ | | |
++| go | ✓ | ✓ | ✓ | ✓ | `gopls` |
++| godot-resource | ✓ | | | | |
++| gomod | ✓ | | | | `gopls` |
++| gotmpl | ✓ | | | | `gopls` |
++| gowork | ✓ | | | | `gopls` |
++| graphql | ✓ | | | | |
++| hare | ✓ | | | | |
++| haskell | ✓ | ✓ | | | `haskell-language-server-wrapper` |
++| haskell-persistent | ✓ | | | | |
++| hcl | ✓ | | ✓ | | `terraform-ls` |
++| heex | ✓ | ✓ | | | `elixir-ls` |
++| hosts | ✓ | | | | |
++| html | ✓ | | | ✓ | `vscode-html-language-server` |
++| hurl | ✓ | | ✓ | | |
++| idris | | | | | `idris2-lsp` |
++| iex | ✓ | | | | |
++| ini | ✓ | | | | |
++| java | ✓ | ✓ | | ✓ | `jdtls` |
++| javascript | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
++| jsdoc | ✓ | | | | |
++| json | ✓ | | ✓ | ✓ | `vscode-json-language-server` |
++| jsonnet | ✓ | | | | `jsonnet-language-server` |
++| jsx | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
++| julia | ✓ | ✓ | ✓ | | `julia` |
++| just | ✓ | ✓ | ✓ | | |
++| kdl | ✓ | | | | |
++| kotlin | ✓ | | | | `kotlin-language-server` |
++| latex | ✓ | ✓ | | | `texlab` |
++| lean | ✓ | | | | `lean` |
++| ledger | ✓ | | | | |
++| llvm | ✓ | ✓ | ✓ | | |
++| llvm-mir | ✓ | ✓ | ✓ | | |
++| llvm-mir-yaml | ✓ | | ✓ | | |
++| lua | ✓ | ✓ | ✓ | | `lua-language-server` |
++| make | ✓ | | | | |
++| markdoc | ✓ | | | | `markdoc-ls` |
++| markdown | ✓ | | | | `marksman` |
++| markdown.inline | ✓ | | | | |
++| matlab | ✓ | ✓ | ✓ | | |
++| mermaid | ✓ | | | | |
++| meson | ✓ | | ✓ | | |
++| mint | | | | | `mint` |
++| msbuild | ✓ | | ✓ | | |
++| nasm | ✓ | ✓ | | | |
++| nickel | ✓ | | ✓ | | `nls` |
++| nim | ✓ | ✓ | ✓ | | `nimlangserver` |
++| nix | ✓ | | | ✓ | `nil` |
++| nu | ✓ | | | | |
++| ocaml | ✓ | | ✓ | | `ocamllsp` |
++| ocaml-interface | ✓ | | | | `ocamllsp` |
++| odin | ✓ | | ✓ | | `ols` |
++| opencl | ✓ | ✓ | ✓ | | `clangd` |
++| openscad | ✓ | | | | `openscad-lsp` |
++| org | ✓ | | | | |
++| pascal | ✓ | ✓ | | | `pasls` |
++| passwd | ✓ | | | | |
++| pem | ✓ | | | | |
++| perl | ✓ | ✓ | ✓ | | `perlnavigator` |
++| php | ✓ | ✓ | ✓ | | `intelephense` |
++| po | ✓ | ✓ | | | |
++| ponylang | ✓ | ✓ | ✓ | | |
++| prisma | ✓ | | | | `prisma-language-server` |
++| prolog | | | | | `swipl` |
++| protobuf | ✓ | | ✓ | | |
++| prql | ✓ | | | | |
++| purescript | ✓ | | | | `purescript-language-server` |
++| python | ✓ | ✓ | ✓ | ✓ | `pylsp` |
++| qml | ✓ | | ✓ | | `qmlls` |
++| r | ✓ | | | | `R` |
++| racket | ✓ | | | ✓ | `racket` |
++| regex | ✓ | | | ✓ | |
++| rego | ✓ | | | | `regols` |
++| rescript | ✓ | ✓ | | | `rescript-language-server` |
++| rmarkdown | ✓ | | ✓ | | `R` |
++| robot | ✓ | | | | `robotframework_ls` |
++| ron | ✓ | | ✓ | | |
++| rst | ✓ | | | | |
++| ruby | ✓ | ✓ | ✓ | ✓ | `solargraph` |
++| rust | ✓ | ✓ | ✓ | ✓ | `rust-analyzer` |
++| sage | ✓ | ✓ | | | |
++| scala | ✓ | | ✓ | | `metals` |
++| scheme | ✓ | | | ✓ | |
++| scss | ✓ | | | ✓ | `vscode-css-language-server` |
++| slint | ✓ | | ✓ | | `slint-lsp` |
++| smithy | ✓ | | | | `cs` |
++| sml | ✓ | | | | |
++| solidity | ✓ | | | | `solc` |
++| sql | ✓ | | | | |
++| sshclientconfig | ✓ | | | | |
++| starlark | ✓ | ✓ | | ✓ | |
++| svelte | ✓ | | ✓ | | `svelteserver` |
++| sway | ✓ | ✓ | ✓ | | `forc` |
++| swift | ✓ | | | | `sourcekit-lsp` |
++| t32 | ✓ | | | | |
++| tablegen | ✓ | ✓ | ✓ | | |
++| task | ✓ | | | | |
++| tfvars | ✓ | | ✓ | | `terraform-ls` |
++| toml | ✓ | | | ✓ | `taplo` |
++| tsq | ✓ | | | ✓ | |
++| tsx | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
++| twig | ✓ | | | | |
++| typescript | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
++| typst | ✓ | | | | `typst-lsp` |
++| ungrammar | ✓ | | | | |
++| uxntal | ✓ | | | | |
++| v | ✓ | ✓ | ✓ | | `v` |
++| vala | ✓ | | | | `vala-language-server` |
++| verilog | ✓ | ✓ | | | `svlangserver` |
++| vhdl | ✓ | | | | `vhdl_ls` |
++| vhs | ✓ | | | | |
++| vue | ✓ | | | | `vue-language-server` |
++| wast | ✓ | | | | |
++| wat | ✓ | | | | |
++| webc | ✓ | | | | |
++| wgsl | ✓ | | | | `wgsl_analyzer` |
++| wit | ✓ | | ✓ | | |
++| xit | ✓ | | | | |
++| xml | ✓ | | ✓ | ✓ | |
++| yaml | ✓ | | ✓ | ✓ | `yaml-language-server` |
++| yuck | ✓ | | | | |
++| zig | ✓ | ✓ | ✓ | ✓ | `zls` |
+diff --git a/runtime/queries/bash/rainbows.scm b/runtime/queries/bash/rainbows.scm
+new file mode 100644
+index 00000000..fd2a9d3a
+--- /dev/null
++++ b/runtime/queries/bash/rainbows.scm
+@@ -0,0 +1,21 @@
++[
++ (function_definition)
++ (compound_statement)
++ (subshell)
++ (test_command)
++ (subscript)
++ (parenthesized_expression)
++ (array)
++ (expansion_flags)
++ (expansion)
++ (command_substitution)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "((" "))"
++ "${" "$("
++ "{" "}"
++ "[" "]"
++ "[[" "]]"
++] @rainbow.bracket
+diff --git a/runtime/queries/c/rainbows.scm b/runtime/queries/c/rainbows.scm
+new file mode 100644
+index 00000000..1f80868a
+--- /dev/null
++++ b/runtime/queries/c/rainbows.scm
+@@ -0,0 +1,29 @@
++[
++ (preproc_params)
++ (preproc_defined)
++ (argument_list)
++ (attribute_specifier)
++ (ms_declspec_modifier)
++ (declaration_list)
++ (parenthesized_declarator)
++ (parenthesized_expression)
++ (abstract_parenthesized_declarator)
++ (array_declarator)
++ (compound_statement)
++ (initializer_list)
++ (compound_literal_expression)
++ (enumerator_list)
++ (field_declaration_list)
++ (parameter_list)
++ (for_statement)
++ (macro_type_specifier)
++ (subscript_expression)
++ (subscript_designator)
++ (cast_expression)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "{" "}"
++ "[" "]"
++] @rainbow.bracket
+diff --git a/runtime/queries/clojure/rainbows.scm b/runtime/queries/clojure/rainbows.scm
+new file mode 100644
+index 00000000..99dc8bc3
+--- /dev/null
++++ b/runtime/queries/clojure/rainbows.scm
+@@ -0,0 +1,13 @@
++[
++ (list_lit)
++ (map_lit)
++ (vec_lit)
++ (anon_fn_lit)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "#"
++ "{" "}"
++ "[" "]"
++] @rainbow.bracket
+diff --git a/runtime/queries/common-lisp/rainbows.scm b/runtime/queries/common-lisp/rainbows.scm
+new file mode 100644
+index 00000000..e11eb788
+--- /dev/null
++++ b/runtime/queries/common-lisp/rainbows.scm
+@@ -0,0 +1 @@
++; inherits: scheme
+diff --git a/runtime/queries/cpp/rainbows.scm b/runtime/queries/cpp/rainbows.scm
+new file mode 100644
+index 00000000..ff4882c2
+--- /dev/null
++++ b/runtime/queries/cpp/rainbows.scm
+@@ -0,0 +1,49 @@
++[
++ ; c
++ (preproc_params)
++ (preproc_defined)
++ (argument_list)
++ (attribute_specifier)
++ (ms_declspec_modifier)
++ (declaration_list)
++ (parenthesized_declarator)
++ (parenthesized_expression)
++ (abstract_parenthesized_declarator)
++ (array_declarator)
++ (compound_statement)
++ (initializer_list)
++ (compound_literal_expression)
++ (enumerator_list)
++ (field_declaration_list)
++ (parameter_list)
++ (for_statement)
++ ; (macro_type_specifier) - not part of cpp
++ (subscript_expression)
++ (subscript_designator)
++ (cast_expression)
++
++ ; cpp
++ (decltype)
++ (explicit_function_specifier)
++ (template_parameter_list)
++ (template_argument_list)
++ (parameter_list)
++ (argument_list)
++ (structured_binding_declarator)
++ (noexcept)
++ (throw_specifier)
++ (static_assert_declaration)
++ (condition_clause)
++ (for_range_loop)
++ (new_declarator)
++ (delete_expression "[" "]")
++ (lambda_capture_specifier)
++ (sizeof_expression)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "{" "}"
++ "[" "]"
++ "<" ">"
++] @rainbow.bracket
+diff --git a/runtime/queries/css/rainbows.scm b/runtime/queries/css/rainbows.scm
+new file mode 100644
+index 00000000..66b60d51
+--- /dev/null
++++ b/runtime/queries/css/rainbows.scm
+@@ -0,0 +1,15 @@
++[
++ (keyframe_block_list)
++ (block)
++ (attribute_selector)
++ (feature_query)
++ (parenthesized_query)
++ (selector_query)
++ (parenthesized_value)
++ (arguments)
++] @rainbow.scope
++
++[
++ "{" "}"
++ "(" ")"
++] @rainbow.bracket
+diff --git a/runtime/queries/ecma/rainbows.scm b/runtime/queries/ecma/rainbows.scm
+new file mode 100644
+index 00000000..50f9f813
+--- /dev/null
++++ b/runtime/queries/ecma/rainbows.scm
+@@ -0,0 +1,28 @@
++[
++ (export_clause)
++ (named_imports)
++ (statement_block)
++ (for_statement)
++ (for_in_statement)
++ (switch_body)
++ (catch_clause "(" ")")
++ (parenthesized_expression)
++ (object)
++ (object_pattern)
++ (array)
++ (array_pattern)
++ (subscript_expression)
++ (template_substitution)
++ (arguments)
++ (class_body)
++ (formal_parameters)
++ (computed_property_name)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "${" "{" "}"
++ "[" "]"
++] @rainbow.bracket
++
++(regex "/" @rainbow.bracket) @rainbow.scope
+diff --git a/runtime/queries/elixir/rainbows.scm b/runtime/queries/elixir/rainbows.scm
+new file mode 100644
+index 00000000..01d3da7a
+--- /dev/null
++++ b/runtime/queries/elixir/rainbows.scm
+@@ -0,0 +1,24 @@
++[
++ (block)
++ (interpolation)
++ (list)
++ (tuple)
++ (bitstring)
++ (map)
++ ; short-hand function captures like &(&1 + &2)
++ (unary_operator
++ operator: "&")
++ (arguments "(" ")")
++ (access_call)
++ (sigil)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "%"
++ "{" "}"
++ "[" "]"
++ "<<" ">>"
++ "#{"
++ "|"
++] @rainbow.bracket
+diff --git a/runtime/queries/erlang/rainbows.scm b/runtime/queries/erlang/rainbows.scm
+new file mode 100644
+index 00000000..5092c998
+--- /dev/null
++++ b/runtime/queries/erlang/rainbows.scm
+@@ -0,0 +1,24 @@
++[
++ ; ()
++ (arguments "(" ")")
++ (parenthesized_expression)
++ (function_type)
++ ; #{}
++ (record)
++ (map)
++ ; {}
++ (map_update)
++ (tuple)
++ ; <<>>
++ (bitstring)
++ ; []
++ (list)
++] @rainbow.scope
++
++[
++ "#"
++ "{" "}"
++ "(" ")"
++ "[" "]"
++ "<<" ">>"
++] @rainbow.bracket
+diff --git a/runtime/queries/go/rainbows.scm b/runtime/queries/go/rainbows.scm
+new file mode 100644
+index 00000000..81004bf8
+--- /dev/null
++++ b/runtime/queries/go/rainbows.scm
+@@ -0,0 +1,33 @@
++[
++ (import_spec_list)
++ (const_declaration)
++ (var_declaration)
++ (type_parameter_list)
++ (parameter_list)
++ (type_declaration)
++ (parenthesized_type)
++ (type_arguments)
++ (array_type)
++ (implicit_length_array_type)
++ (slice_type)
++ (field_declaration_list)
++ (interface_type)
++ (map_type)
++ (block)
++ (expression_switch_statement)
++ (type_switch_statement)
++ (select_statement)
++ (parenthesized_expression)
++ (argument_list)
++ (index_expression)
++ (slice_expression)
++ (type_assertion_expression)
++ (type_conversion_expression)
++ (literal_value)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "[" "]"
++ "{" "}"
++] @rainbow.bracket
+diff --git a/runtime/queries/html/rainbows.scm b/runtime/queries/html/rainbows.scm
+new file mode 100644
+index 00000000..66e62e95
+--- /dev/null
++++ b/runtime/queries/html/rainbows.scm
+@@ -0,0 +1,13 @@
++[
++ (doctype)
++ (erroneous_end_tag)
++] @rainbow.scope
++
++([
++ (element)
++ (script_element)
++ (style_element)
++ ] @rainbow.scope
++ (#set! rainbow.include-children))
++
++["<" ">" "<!" "</" "/>"] @rainbow.bracket
+diff --git a/runtime/queries/java/rainbows.scm b/runtime/queries/java/rainbows.scm
+new file mode 100644
+index 00000000..699b899c
+--- /dev/null
++++ b/runtime/queries/java/rainbows.scm
+@@ -0,0 +1,35 @@
++[
++ (cast_expression)
++ (inferred_parameters)
++ (dimensions_expr)
++ (parenthesized_expression)
++ (array_access)
++ (argument_list)
++ (type_arguments)
++ (dimensions)
++ (block)
++ (switch_block)
++ (catch_clause)
++ (resource_specification)
++ (for_statement)
++ (enhanced_for_statement)
++ (annotation_argument_list)
++ (element_value_array_initializer)
++ (module_body)
++ (enum_body)
++ (type_parameters)
++ (class_body)
++ (constructor_body)
++ (annotation_type_body)
++ (annotation_type_element_declaration)
++ (interface_body)
++ (array_initializer)
++ (formal_parameters)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "{" "}"
++ "[" "]"
++ "<" ">"
++] @rainbow.bracket
+diff --git a/runtime/queries/javascript/rainbows.scm b/runtime/queries/javascript/rainbows.scm
+new file mode 100644
+index 00000000..04328f09
+--- /dev/null
++++ b/runtime/queries/javascript/rainbows.scm
+@@ -0,0 +1 @@
++; inherits: ecma
+diff --git a/runtime/queries/json/rainbows.scm b/runtime/queries/json/rainbows.scm
+new file mode 100644
+index 00000000..5c21bdcc
+--- /dev/null
++++ b/runtime/queries/json/rainbows.scm
+@@ -0,0 +1,9 @@
++[
++ (object)
++ (array)
++] @rainbow.scope
++
++[
++ "[" "]"
++ "{" "}"
++] @rainbow.bracket
+diff --git a/runtime/queries/jsx/rainbows.scm b/runtime/queries/jsx/rainbows.scm
+new file mode 100644
+index 00000000..cf4a7e6a
+--- /dev/null
++++ b/runtime/queries/jsx/rainbows.scm
+@@ -0,0 +1,10 @@
++; inherits: ecma
++
++[
++ (jsx_expression)
++] @rainbow.scope
++
++(jsx_fragment ["<" "/" ">"] @rainbow.bracket) @rainbow.scope
++(jsx_opening_element ["<" ">"] @rainbow.bracket) @rainbow.scope
++(jsx_closing_element ["<" "/" ">"] @rainbow.bracket) @rainbow.scope
++(jsx_self_closing_element ["<" "/" ">"] @rainbow.bracket) @rainbow.scope
+diff --git a/runtime/queries/nix/rainbows.scm b/runtime/queries/nix/rainbows.scm
+new file mode 100644
+index 00000000..2df51393
+--- /dev/null
++++ b/runtime/queries/nix/rainbows.scm
+@@ -0,0 +1,17 @@
++[
++ (formals)
++ (parenthesized_expression)
++ (attrset_expression)
++ (let_attrset_expression)
++ (rec_attrset_expression)
++ (inherit_from)
++ (interpolation)
++ (list_expression)
++] @rainbow.scope
++
++[
++ "${"
++ "{" "}"
++ "(" ")"
++ "[" "]"
++] @rainbow.bracket
+diff --git a/runtime/queries/python/rainbows.scm b/runtime/queries/python/rainbows.scm
+new file mode 100644
+index 00000000..ce3efe2d
+--- /dev/null
++++ b/runtime/queries/python/rainbows.scm
+@@ -0,0 +1,30 @@
++[
++ (future_import_statement)
++ (import_from_statement)
++ (with_clause)
++ (parameters)
++ (parenthesized_list_splat)
++ (argument_list)
++ (tuple_pattern)
++ (list_pattern)
++ (subscript)
++ (list)
++ (set)
++ (tuple)
++ (dictionary)
++ (dictionary_comprehension)
++ (set_comprehension)
++ (list_comprehension)
++ (generator_expression)
++ (parenthesized_expression)
++ (interpolation)
++ (format_expression)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "{" "}"
++ "[" "]"
++] @rainbow.bracket
++
++(string ["{{" "}}"] @rainbow.bracket) @rainbow.scope
+diff --git a/runtime/queries/racket/rainbows.scm b/runtime/queries/racket/rainbows.scm
+new file mode 100644
+index 00000000..e11eb788
+--- /dev/null
++++ b/runtime/queries/racket/rainbows.scm
+@@ -0,0 +1 @@
++; inherits: scheme
+diff --git a/runtime/queries/regex/rainbows.scm b/runtime/queries/regex/rainbows.scm
+new file mode 100644
+index 00000000..a9eb1cff
+--- /dev/null
++++ b/runtime/queries/regex/rainbows.scm
+@@ -0,0 +1,17 @@
++[
++ (lookahead_assertion)
++ (character_class)
++ (anonymous_capturing_group)
++ (named_capturing_group)
++ (non_capturing_group)
++ (count_quantifier)
++ (character_class_escape)
++] @rainbow.scope
++
++[
++ "(?" "(?:"
++ "(?<" ">"
++ "(" ")"
++ "[" "]"
++ "{" "}"
++] @rainbow.bracket
+diff --git a/runtime/queries/ruby/rainbows.scm b/runtime/queries/ruby/rainbows.scm
+new file mode 100644
+index 00000000..e67edfb8
+--- /dev/null
++++ b/runtime/queries/ruby/rainbows.scm
+@@ -0,0 +1,28 @@
++[
++ (begin_block)
++ (end_block)
++ (singleton_method)
++ (block_parameters)
++ (parenthesized_statements)
++ (element_reference)
++ (argument_list "(" ")")
++ (block)
++ (destructured_left_assignment)
++ (interpolation)
++ (string_array)
++ (symbol_array)
++ (regex)
++ (array)
++ (hash)
++ (method_parameters)
++] @rainbow.scope
++
++[
++ "#{"
++ "{" "}"
++ "(" ")"
++ "%w(" "%i("
++ "[" "]"
++ "|"
++ "/"
++] @rainbow.bracket
+diff --git a/runtime/queries/rust/rainbows.scm b/runtime/queries/rust/rainbows.scm
+new file mode 100644
+index 00000000..0656047b
+--- /dev/null
++++ b/runtime/queries/rust/rainbows.scm
+@@ -0,0 +1,60 @@
++[
++ ; {/}
++ (declaration_list)
++ (field_declaration_list)
++ (field_initializer_list)
++ (enum_variant_list)
++ (block)
++ (match_block)
++ (use_list)
++ (struct_pattern)
++
++ ; (/)
++ (ordered_field_declaration_list)
++ (arguments)
++ (parameters)
++ (tuple_type)
++ (tuple_expression)
++ (tuple_pattern)
++ (tuple_struct_pattern)
++ (unit_type)
++ (unit_expression)
++ (visibility_modifier)
++ (parenthesized_expression)
++ (token_repetition_pattern)
++
++ ; </>
++ (type_parameters)
++ (type_arguments)
++ (bracketed_type)
++ (for_lifetimes)
++
++ ; [/]
++ (array_type)
++ (array_expression)
++ (index_expression)
++ (slice_pattern)
++
++ ; attributes #[]
++ (attribute_item)
++ (inner_attribute_item)
++
++ ; macros
++ (token_tree_pattern)
++ (macro_definition)
++
++ ; closures
++ (closure_parameters)
++] @rainbow.scope
++
++; attributes like `#[serde(rename_all = "kebab-case")]`
++(attribute arguments: (token_tree) @rainbow.scope)
++
++[
++ "#"
++ "[" "]"
++ "(" ")"
++ "{" "}"
++ "<" ">"
++ "|"
++] @rainbow.bracket
+diff --git a/runtime/queries/scheme/rainbows.scm b/runtime/queries/scheme/rainbows.scm
+new file mode 100644
+index 00000000..f948772c
+--- /dev/null
++++ b/runtime/queries/scheme/rainbows.scm
+@@ -0,0 +1,12 @@
++[
++ (list)
++ (vector)
++ (byte_vector)
++] @rainbow.scope
++
++[
++ "#(" "#vu8("
++ "(" ")"
++ "[" "]"
++ "{" "}"
++] @rainbow.bracket
+diff --git a/runtime/queries/scss/rainbows.scm b/runtime/queries/scss/rainbows.scm
+new file mode 100644
+index 00000000..f0c648f3
+--- /dev/null
++++ b/runtime/queries/scss/rainbows.scm
+@@ -0,0 +1,3 @@
++; inherits: css
++
++(parameters) @rainbow.scope
+diff --git a/runtime/queries/starlark/rainbows.scm b/runtime/queries/starlark/rainbows.scm
+new file mode 100644
+index 00000000..0b920cbf
+--- /dev/null
++++ b/runtime/queries/starlark/rainbows.scm
+@@ -0,0 +1 @@
++; inherits: python
+diff --git a/runtime/queries/toml/rainbows.scm b/runtime/queries/toml/rainbows.scm
+new file mode 100644
+index 00000000..1f61c8ac
+--- /dev/null
++++ b/runtime/queries/toml/rainbows.scm
+@@ -0,0 +1,12 @@
++[
++ (table_array_element)
++ (table)
++ (array)
++ (inline_table)
++] @rainbow.scope
++
++[
++ "[[" "]]"
++ "[" "]"
++ "{" "}"
++] @rainbow.bracket
+diff --git a/runtime/queries/tsq/rainbows.scm b/runtime/queries/tsq/rainbows.scm
+new file mode 100644
+index 00000000..b1785fa8
+--- /dev/null
++++ b/runtime/queries/tsq/rainbows.scm
+@@ -0,0 +1,12 @@
++[
++ (group)
++ (named_node)
++ (wildcard_node)
++ (predicate)
++ (alternation)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "[" "]"
++] @rainbow.bracket
+diff --git a/runtime/queries/tsx/rainbows.scm b/runtime/queries/tsx/rainbows.scm
+new file mode 100644
+index 00000000..64c2fe36
+--- /dev/null
++++ b/runtime/queries/tsx/rainbows.scm
+@@ -0,0 +1,2 @@
++; inherits: typescript
++; inherits: jsx
+diff --git a/runtime/queries/typescript/rainbows.scm b/runtime/queries/typescript/rainbows.scm
+new file mode 100644
+index 00000000..919061aa
+--- /dev/null
++++ b/runtime/queries/typescript/rainbows.scm
+@@ -0,0 +1,19 @@
++; inherits: ecma
++
++[
++ (import_require_clause)
++ (enum_body)
++ (lookup_type)
++ (parenthesized_type)
++ (object_type)
++ (type_parameters)
++ (index_signature)
++ (array_type)
++ (tuple_type)
++] @rainbow.scope
++
++(type_arguments ["<" ">"] @rainbow.bracket) @rainbow.scope
++
++[
++ "{|" "|}"
++] @rainbow.bracket
+diff --git a/runtime/queries/xml/rainbows.scm b/runtime/queries/xml/rainbows.scm
+new file mode 100644
+index 00000000..0ff9c7fa
+--- /dev/null
++++ b/runtime/queries/xml/rainbows.scm
+@@ -0,0 +1,29 @@
++[
++ (processing_instructions)
++ (cdata_sect)
++ (xml_decl)
++ (doctype_decl)
++ (element_decl)
++ (element_choice)
++ (element_seq)
++ (mixed)
++ (attlist_decl)
++ (notation_type)
++ (enumeration)
++ (ge_decl)
++ (pe_decl)
++ (notation_decl)
++] @rainbow.scope
++
++((element) @rainbow.scope
++ (#set! rainbow.include-children))
++
++[
++ "<?" "?>"
++ "<" ">"
++ "</" "/>"
++ "<!"
++ "(" ")"
++ ")*"
++ "[" "]"
++] @rainbow.bracket
+diff --git a/runtime/queries/yaml/rainbows.scm b/runtime/queries/yaml/rainbows.scm
+new file mode 100644
+index 00000000..d810accc
+--- /dev/null
++++ b/runtime/queries/yaml/rainbows.scm
+@@ -0,0 +1,9 @@
++[
++ (flow_sequence)
++ (flow_mapping)
++] @rainbow.scope
++
++[
++ "[" "]"
++ "{" "}"
++] @rainbow.bracket
+diff --git a/runtime/queries/zig/rainbows.scm b/runtime/queries/zig/rainbows.scm
+new file mode 100644
+index 00000000..af823e6d
+--- /dev/null
++++ b/runtime/queries/zig/rainbows.scm
+@@ -0,0 +1,42 @@
++[
++ ; zig
++ (ArrayTypeStart)
++ ; using ()
++ (AsmExpr)
++ (AsmOutputItem)
++ (ByteAlign)
++ (CallConv)
++ (ContainerDeclType)
++ (ErrorSetDecl)
++ (FnCallArguments)
++ (ForPrefix)
++ (GroupedExpr)
++ (IfPrefix)
++ (ParamDeclList)
++ (SwitchExpr)
++ (WhileContinueExpr)
++ (WhilePrefix)
++ ; for align expressions
++ (PtrTypeStart)
++
++ ; using {}
++ (Block)
++ (BlockExpr)
++ (FormatSequence)
++ (InitList)
++
++ ; using []
++ (SliceTypeStart)
++ (SuffixOp)
++
++ ; zig uses || for captures
++ (Payload "|" @rainbow.bracket)
++ (PtrPayload "|" @rainbow.bracket)
++ (PtrIndexPayload "|" @rainbow.bracket)
++] @rainbow.scope
++
++[
++ "(" ")"
++ "{" "}"
++ "[" "]"
++] @rainbow.bracket
+--
+2.41.0
+
diff --git a/0013-Add-rainbow-indentation-guides.patch b/0013-Add-rainbow-indentation-guides.patch
new file mode 100644
index 00000000..532b6905
--- /dev/null
+++ b/0013-Add-rainbow-indentation-guides.patch
@@ -0,0 +1,190 @@
+From 35f26da37440f8729982f9ee543a5bb24ad276a2 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 19:17:27 -0700
+Subject: [PATCH] Add rainbow indentation guides
+
+ref: https://github.com/helix-editor/helix/pull/4493
+---
+ book/src/configuration.md | 12 +++++-----
+ helix-term/src/ui/document.rs | 42 ++++++++++++++++++++++++++---------
+ helix-view/src/editor.rs | 10 +++++++++
+ helix-view/src/theme.rs | 4 ++++
+ 4 files changed, 53 insertions(+), 15 deletions(-)
+
+diff --git a/book/src/configuration.md b/book/src/configuration.md
+index 32a4aac4..6609c30e 100644
+--- a/book/src/configuration.md
++++ b/book/src/configuration.md
+@@ -261,11 +261,12 @@ ### `[editor.indent-guides]` Section
+
+ Options for rendering vertical indent guides.
+
+-| Key | Description | Default |
+-| --- | --- | --- |
+-| `render` | Whether to render indent guides | `false` |
+-| `character` | Literal character to use for rendering the indent guide | `│` |
+-| `skip-levels` | Number of indent levels to skip | `0` |
++| Key | Description | Default |
++| --- | --- | --- |
++| `render` | Whether to render indent indent-guides | `false` |
++| `character` | Literal character to use for rendering the indent guide | `│` |
++| `skip-levels` | Number of indent levels to skip | `0` |
++| `rainbow-option` | Enum to set rainbow indentations. Options: `normal`, `dim` and `none`| `none` |
+
+ Example:
+
+@@ -274,6 +275,7 @@ ### `[editor.indent-guides]` Section
+ render = true
+ character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽"
+ skip-levels = 1
++rainbow-option = "normal"
+ ```
+
+ ### `[editor.gutters]` Section
+diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs
+index 80da1c54..f406a6f9 100644
+--- a/helix-term/src/ui/document.rs
++++ b/helix-term/src/ui/document.rs
+@@ -7,9 +7,9 @@
+ use helix_core::syntax::HighlightEvent;
+ use helix_core::text_annotations::TextAnnotations;
+ use helix_core::{visual_offset_from_block, Position, RopeSlice};
+-use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
++use helix_view::editor::{RainbowIndentOptions, WhitespaceConfig, WhitespaceRenderValue};
+ use helix_view::graphics::Rect;
+-use helix_view::theme::Style;
++use helix_view::theme::{Modifier, Style};
+ use helix_view::view::ViewPosition;
+ use helix_view::Document;
+ use helix_view::Theme;
+@@ -310,6 +310,8 @@ pub struct TextRenderer<'a> {
+ pub whitespace_style: Style,
+ pub indent_guide_char: String,
+ pub indent_guide_style: Style,
++ pub indent_guide_rainbow: RainbowIndentOptions,
++ pub theme: &'a Theme,
+ pub newline: String,
+ pub nbsp: String,
+ pub space: String,
+@@ -326,7 +328,7 @@ impl<'a> TextRenderer<'a> {
+ pub fn new(
+ surface: &'a mut Surface,
+ doc: &Document,
+- theme: &Theme,
++ theme: &'a Theme,
+ col_offset: usize,
+ viewport: Rect,
+ ) -> TextRenderer<'a> {
+@@ -363,12 +365,19 @@ pub fn new(
+ };
+
+ let text_style = theme.get("ui.text");
++ let basic_style = text_style.patch(
++ theme
++ .try_get("ui.virtual.indent-guide")
++ .unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
++ );
+
+ let indent_width = doc.indent_style.indent_width(tab_width) as u16;
+
+ TextRenderer {
+ surface,
+ indent_guide_char: editor_config.indent_guides.character.into(),
++ indent_guide_rainbow: editor_config.indent_guides.rainbow_option.clone(),
++ theme,
+ newline,
+ nbsp,
+ space,
+@@ -379,11 +388,7 @@ pub fn new(
+ starting_indent: col_offset / indent_width as usize
+ + (col_offset % indent_width as usize != 0) as usize
+ + editor_config.indent_guides.skip_levels as usize,
+- indent_guide_style: text_style.patch(
+- theme
+- .try_get("ui.virtual.indent-guide")
+- .unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
+- ),
++ indent_guide_style: basic_style,
+ text_style,
+ draw_indent_guides: editor_config.indent_guides.render,
+ viewport,
+@@ -477,8 +482,25 @@ pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) {
+ as u16;
+ let y = self.viewport.y + row;
+ debug_assert!(self.surface.in_bounds(x, y));
+- self.surface
+- .set_string(x, y, &self.indent_guide_char, self.indent_guide_style);
++ match self.indent_guide_rainbow {
++ RainbowIndentOptions::None => {
++ self.surface
++ .set_string(x, y, &self.indent_guide_char, self.indent_guide_style)
++ }
++ RainbowIndentOptions::Dim => {
++ let new_style = self
++ .indent_guide_style
++ .patch(self.theme.get_rainbow(i))
++ .add_modifier(Modifier::DIM);
++ self.surface
++ .set_string(x, y, &self.indent_guide_char, new_style);
++ }
++ RainbowIndentOptions::Normal => {
++ let new_style = self.indent_guide_style.patch(self.theme.get_rainbow(i));
++ self.surface
++ .set_string(x, y, &self.indent_guide_char, new_style);
++ }
++ };
+ }
+ }
+ }
+diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
+index c0410f7d..543c7bde 100644
+--- a/helix-view/src/editor.rs
++++ b/helix-view/src/editor.rs
+@@ -743,12 +743,21 @@ fn default() -> Self {
+ }
+ }
+
++#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
++#[serde(rename_all = "kebab-case")]
++pub enum RainbowIndentOptions {
++ None,
++ Dim,
++ Normal,
++}
++
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+ #[serde(default, rename_all = "kebab-case")]
+ pub struct IndentGuidesConfig {
+ pub render: bool,
+ pub character: char,
+ pub skip_levels: u8,
++ pub rainbow_option: RainbowIndentOptions,
+ }
+
+ impl Default for IndentGuidesConfig {
+@@ -757,6 +766,7 @@ fn default() -> Self {
+ skip_levels: 0,
+ render: false,
+ character: '│',
++ rainbow_option: RainbowIndentOptions::None,
+ }
+ }
+ }
+diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
+index 2b024384..25b0f197 100644
+--- a/helix-view/src/theme.rs
++++ b/helix-view/src/theme.rs
+@@ -378,6 +378,10 @@ pub fn is_16_color(&self) -> bool {
+ pub fn rainbow_length(&self) -> usize {
+ self.rainbow_length
+ }
++
++ pub fn get_rainbow(&self, index: usize) -> Style {
++ self.highlights[index % self.rainbow_length]
++ }
+ }
+
+ fn default_rainbow() -> Vec<Style> {
+--
+2.41.0
+
diff --git a/0014-Add-unbind-default-keys-config-option.patch b/0014-Add-unbind-default-keys-config-option.patch
new file mode 100644
index 00000000..88499b25
--- /dev/null
+++ b/0014-Add-unbind-default-keys-config-option.patch
@@ -0,0 +1,106 @@
+From 45258008f5fce264d2c6abcad4e224c3d236c267 Mon Sep 17 00:00:00 2001
+From: JJ <git@toki.la>
+Date: Sat, 15 Jul 2023 19:31:25 -0700
+Subject: [PATCH] Add unbind-default-keys config option
+
+ref: https://github.com/helix-editor/helix/pull/2733
+---
+ book/src/remapping.md | 19 +++++++++++++++++++
+ helix-term/src/config.rs | 12 ++++++++++--
+ helix-term/src/keymap.rs | 7 +++++--
+ 3 files changed, 34 insertions(+), 4 deletions(-)
+
+diff --git a/book/src/remapping.md b/book/src/remapping.md
+index d762c6ad..cc27eed7 100644
+--- a/book/src/remapping.md
++++ b/book/src/remapping.md
+@@ -75,5 +75,24 @@ ## Special keys and modifiers
+
+ Keys can be disabled by binding them to the `no_op` command.
+
++
++To remove all default bindings, `unbind-default-keys = true` can be added to the top level configuration.
++
++```toml
++unbind-default-keys = true
++
++# Only these normal mode bindings will be used
++[keys.normal]
++n = "normal_mode"
++t = "goto_definition"
++
++# remember to add bindings to return to normal mode
++[keys.select]
++esc = "normal_mode"
++
++[keys.insert]
++esc = "normal_mode"
++```
++
+ A list of commands is available in the [Keymap](https://docs.helix-editor.com/keymap.html) documentation
+ and 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/helix-term/src/config.rs b/helix-term/src/config.rs
+index f37b03ec..aa59dc8d 100644
+--- a/helix-term/src/config.rs
++++ b/helix-term/src/config.rs
+@@ -20,6 +20,8 @@ pub struct Config {
+ #[serde(deny_unknown_fields)]
+ pub struct ConfigRaw {
+ pub theme: Option<String>,
++ #[serde(default)]
++ pub unbind_default_keys: bool,
+ pub keys: Option<HashMap<Mode, KeyTrie>>,
+ pub editor: Option<toml::Value>,
+ }
+@@ -66,7 +68,10 @@ pub fn load(
+ local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
+ let res = match (global_config, local_config) {
+ (Ok(global), Ok(local)) => {
+- let mut keys = keymap::default();
++ let mut keys = match local.unbind_default_keys {
++ true => HashMap::default(),
++ false => keymap::default(),
++ };
+ if let Some(global_keys) = global.keys {
+ merge_keys(&mut keys, global_keys)
+ }
+@@ -96,7 +101,10 @@ pub fn load(
+ return Err(ConfigLoadError::BadConfig(err))
+ }
+ (Ok(config), Err(_)) | (Err(_), Ok(config)) => {
+- let mut keys = keymap::default();
++ let mut keys = match config.unbind_default_keys {
++ true => HashMap::default(),
++ false => keymap::default(),
++ };
+ if let Some(keymap) = config.keys {
+ merge_keys(&mut keys, keymap);
+ }
+diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
+index 5a72a35a..8cb6ddf8 100644
+--- a/helix-term/src/keymap.rs
++++ b/helix-term/src/keymap.rs
+@@ -309,7 +309,10 @@ pub fn sticky(&self) -> Option<&KeyTrieNode> {
+ pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
+ // TODO: remove the sticky part and look up manually
+ let keymaps = &*self.map();
+- let keymap = &keymaps[&mode];
++ let keymap = match keymaps.get(&mode) {
++ Some(keymap) => keymap,
++ None => return KeymapResult::NotFound,
++ };
+
+ if key!(Esc) == key {
+ if !self.state.is_empty() {
+@@ -364,7 +367,7 @@ fn default() -> Self {
+ }
+ }
+
+-/// Merge default config keys with user overwritten keys for custom user config.
++/// Merge existing config keys with user overwritten keys.
+ pub fn merge_keys(dst: &mut HashMap<Mode, KeyTrie>, mut delta: HashMap<Mode, KeyTrie>) {
+ for (mode, keys) in dst {
+ keys.merge_nodes(
+--
+2.41.0
+