diff --git a/book/src/configuration.md b/book/src/configuration.md index 6e2bb24ad96f9..f81cf2289ac91 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -57,6 +57,7 @@ on unix operating systems. | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | | `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` | +| `supertab` | If set to a command name, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run the given command. If there is only whitespace to the left, then it inserts a tab as normal. One can emulate a tabout behavior with this with the `move_parent_node_end` command. | `None` | ### `[editor.statusline]` Section diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d8ca9926af25c..dfd12ec8463bb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2963,6 +2963,8 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) { } pub mod insert { + use std::str::FromStr; + use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option; pub type PostHook = fn(&mut Context, char); @@ -3102,16 +3104,44 @@ pub mod insert { } pub fn insert_tab(cx: &mut Context) { + let (view, doc) = current_ref!(cx.editor); + let view_id = view.id; + + if let Some(ref cmd) = cx.editor.config().supertab { + 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); + log::debug!("left: {:?}", left); + left.chars().all(|c| c.is_whitespace()) + }); + + if !cursors_after_whitespace { + let cmd = match MappableCommand::from_str(cmd) { + Err(err) => { + cx.editor.set_error(err.to_string()); + return; + } + Ok(cmd) => cmd, + }; + + cmd.execute(cx); + return; + } + } + let (view, doc) = current!(cx.editor); + // TODO: round out to nearest indentation level (for example a line with 3 spaces should // indent by one to reach 4 spaces). - let indent = Tendril::from(doc.indent_style.as_str()); let transaction = Transaction::insert( doc.text(), - &doc.selection(view.id).clone().cursors(doc.text().slice(..)), + &doc.selection(view_id).clone().cursors(doc.text().slice(..)), indent, ); + apply_transaction(&transaction, doc, view); } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index b9c1f9ded2e10..ef1048e30ab6a 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -249,6 +249,17 @@ impl Component for Menu { compositor.pop(); })); + let config = cx.editor.config(); + + // Ignore tab key when supertab is turned on in order not to interfere + // with it. (Is there a better way to do this?) + if config.supertab.is_some() + && config.auto_completion + && (event == key!(Tab) || event == shift!(Tab)) + { + 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 3737488bc359d..8c09dfd8dee2a 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -250,3 +250,249 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_supertab_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 + // supertab 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)# + } + "}), + ), + ]; + + for test in tests { + test_with_config( + Args { + files: vec![(PathBuf::from("foo.rs"), Position::default())], + ..Default::default() + }, + Config { + editor: helix_view::editor::Config { + supertab: Some("move_parent_node_end".into()), + ..helpers::test_editor_config() + }, + ..helpers::test_config() + }, + helpers::test_syntax_conf(None), + test, + ) + .await?; + } + + Ok(()) +} diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index dc059bb0f882a..f625329b26520 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -232,15 +232,19 @@ pub fn temp_file_with_contents>( /// Generates a config with defaults more suitable for integration tests pub fn test_config() -> Config { merge_keys(Config { - editor: helix_view::editor::Config { - lsp: LspConfig { - enable: false, - ..Default::default() - }, + editor: test_editor_config(), + ..Default::default() + }) +} + +pub fn test_editor_config() -> helix_view::editor::Config { + helix_view::editor::Config { + lsp: LspConfig { + enable: false, ..Default::default() }, ..Default::default() - }) + } } /// Replaces all LF chars with the system's appropriate line feed diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 19585d838177f..326c5d3156f7e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -178,6 +178,9 @@ pub struct Config { pub indent_guides: IndentGuidesConfig, /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, + /// An alternative command to run when tab is pressed and the cursor has + /// text other than whitespace to its left on the current line. + pub supertab: Option, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -633,6 +636,7 @@ impl Default for Config { bufferline: BufferLine::default(), indent_guides: IndentGuidesConfig::default(), color_modes: false, + supertab: None, } } }