From 15e07d4db893aec7b9e117c1f88400a68ba98ae2 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 8 Oct 2022 18:14:49 -0400 Subject: feat: smart_tab Implement `smart_tab`, which optionally makes the tab key run the `move_parent_node_start` command when the cursor has non- whitespace to its left. --- helix-term/src/application.rs | 9 +- helix-term/src/commands.rs | 38 ++++- helix-term/src/config.rs | 1 + helix-term/src/keymap/default.rs | 3 +- helix-term/src/ui/menu.rs | 17 +- helix-term/tests/test/commands/movement.rs | 253 +++++++++++++++++++++++++++++ 6 files changed, 311 insertions(+), 10 deletions(-) (limited to 'helix-term') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index dc461198..a97ae503 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -33,12 +33,9 @@ use crate::{ }; use log::{debug, error, warn}; -use std::{ - collections::btree_map::Entry, - io::{stdin, stdout}, - path::Path, - sync::Arc, -}; +#[cfg(not(feature = "integration"))] +use std::io::stdout; +use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc}; use anyhow::{Context, Error}; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fc01eec7..7fb17ed8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -365,6 +365,7 @@ impl MappableCommand { extend_to_line_end, "Extend to line end", extend_to_line_end_newline, "Extend to line end", signature_help, "Show signature help", + smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.", insert_tab, "Insert tab char", insert_newline, "Insert newline char", delete_char_backward, "Delete previous char", @@ -2521,6 +2522,10 @@ fn insert_mode(cx: &mut Context) { .transform(|range| Range::new(range.to(), range.from())); doc.set_selection(view.id, selection); + + // [TODO] temporary workaround until we're not using the idle timer to + // trigger auto completions any more + cx.editor.clear_idle_timer(); } // inserts at the end of each selection @@ -3444,6 +3449,7 @@ pub mod insert { } use helix_core::auto_pairs; + use helix_view::editor::SmartTabConfig; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current_ref!(cx.editor); @@ -3469,6 +3475,31 @@ pub mod insert { } } + pub fn smart_tab(cx: &mut Context) { + let (view, doc) = current_ref!(cx.editor); + let view_id = view.id; + + if matches!( + cx.editor.config().smart_tab, + Some(SmartTabConfig { enable: true, .. }) + ) { + let cursors_after_whitespace = doc.selection(view_id).ranges().iter().all(|range| { + let cursor = range.cursor(doc.text().slice(..)); + let current_line_num = doc.text().char_to_line(cursor); + let current_line_start = doc.text().line_to_char(current_line_num); + let left = doc.text().slice(current_line_start..cursor); + left.chars().all(|c| c.is_whitespace()) + }); + + if !cursors_after_whitespace { + move_parent_node_end(cx); + return; + } + } + + insert_tab(cx); + } + pub fn insert_tab(cx: &mut Context) { let (view, doc) = current!(cx.editor); // TODO: round out to nearest indentation level (for example a line with 3 spaces should @@ -4626,11 +4657,14 @@ fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) { ); doc.set_selection(view.id, selection); + + // [TODO] temporary workaround until we're not using the idle timer to + // trigger auto completions any more + editor.clear_idle_timer(); } }; - motion(cx.editor); - cx.editor.last_motion = Some(Motion(Box::new(motion))); + cx.editor.apply_motion(motion); } pub fn move_parent_node_end(cx: &mut Context) { diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index f37b03ec..bcba8d8e 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -109,6 +109,7 @@ impl Config { )?, } } + // these are just two io errors return the one for the global config (Err(err), Err(_)) => return Err(err), }; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 419e376f..763ed4ae 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -373,7 +373,8 @@ pub fn default() -> HashMap { "C-h" | "backspace" | "S-backspace" => delete_char_backward, "C-d" | "del" => delete_char_forward, "C-j" | "ret" => insert_newline, - "tab" => insert_tab, + "tab" => smart_tab, + "S-tab" => insert_tab, "up" => move_visual_line_up, "down" => move_visual_line_down, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index bdad2e40..c73e7bed 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -11,7 +11,7 @@ pub use tui::widgets::{Cell, Row}; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; -use helix_view::{graphics::Rect, Editor}; +use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item { @@ -247,6 +247,21 @@ impl Component for Menu { compositor.pop(); })); + // Ignore tab key when supertab is turned on in order not to interfere + // with it. (Is there a better way to do this?) + if (event == key!(Tab) || event == shift!(Tab)) + && cx.editor.config().auto_completion + && matches!( + cx.editor.config().smart_tab, + Some(SmartTabConfig { + enable: true, + supersede_menu: true, + }) + ) + { + return EventResult::Ignored(None); + } + match event { // esc or ctrl-c aborts the completion and closes the menu key!(Esc) | ctrl!('c') => { diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 03dc7ba9..5be68837 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -197,3 +197,256 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { + let tests = vec![ + // single cursor stays single cursor, first goes to end of current + // node, then parent + ( + helpers::platform_line(indoc! {r##" + fn foo() { + let result = if true { + "yes" + } else { + "no#["|]# + } + } + "##}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no\"#[|\n]# + } + } + "}), + ), + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no\"#[\n|]# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no\" + }#[|\n]# + } + "}), + ), + // appending to the end of a line should still look at the current + // line, not the next one + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no#[\"|]# + } + } + "}), + "a", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no\" + }#[\n|]# + } + "}), + ), + // before cursor is all whitespace, so insert tab + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + #[\"no\"|]# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + #[|\"no\"]# + } + } + "}), + ), + // if selection spans multiple lines, it should still only look at the + // line on which the head is + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[\"yes\" + } else { + \"no\"|]# + } + } + "}), + "a", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no\" + }#[\n|]# + } + "}), + ), + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[\"yes\" + } else { + \"no\"|]# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[|\"yes\" + } else { + \"no\"]# + } + } + "}), + ), + ( + helpers::platform_line(indoc! {"\ + fn foo() { + #[l|]#et result = if true { + #(\"yes\" + } else { + \"no\"|)# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + #[|l]#et result = if true { + #(|\"yes\" + } else { + \"no\")# + } + } + "}), + ), + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\"#[\n|]# + } else { + \"no\"#(\n|)# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + }#[| ]#else { + \"no\" + }#(|\n)# + } + "}), + ), + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[\"yes\"|]# + } else { + #(\"no\"|)# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[|\"yes\"]# + } else { + #(|\"no\")# + } + } + "}), + ), + // if any cursors are not preceded by all whitespace, then do the + // smart_tab action + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[\"yes\"\n|]# + } else { + \"no#(\"\n|)# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + }#[| ]#else { + \"no\" + }#(|\n)# + } + "}), + ), + // Ctrl-tab always inserts a tab + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[\"yes\"\n|]# + } else { + \"no#(\"\n|)# + } + } + "}), + "i", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + #[|\"yes\"\n]# + } else { + \"no #(|\"\n)# + } + } + "}), + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} -- cgit v1.2.3-70-g09d2