aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
authorSkyler Hawthorne2022-10-08 22:14:49 +0000
committerMichael Davis2023-08-01 14:41:42 +0000
commit15e07d4db893aec7b9e117c1f88400a68ba98ae2 (patch)
treef3d78f1904c596af44e79d0f850c33c3f518f2fb /helix-term
parent93acb538121cab36712f40f26fa287df93817de5 (diff)
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.
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/src/application.rs9
-rw-r--r--helix-term/src/commands.rs38
-rw-r--r--helix-term/src/config.rs1
-rw-r--r--helix-term/src/keymap/default.rs3
-rw-r--r--helix-term/src/ui/menu.rs17
-rw-r--r--helix-term/tests/test/commands/movement.rs253
6 files changed, 311 insertions, 10 deletions
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<Mode, KeyTrie> {
"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<T: Item + 'static> Component for Menu<T> {
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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<tab>",
+ 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<S-tab>",
+ 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(())
+}