Skip to content

Commit

Permalink
feat: supertab
Browse files Browse the repository at this point in the history
Implement "supertab", which optionally makes the `insert_tab` command
run a different command when the cursor has non-whitespace to its left,
i.e. you are in the middle of the line's content, and not trying to
indent.
  • Loading branch information
dead10ck committed Dec 21, 2022
1 parent 1e3e94e commit 90644c7
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 8 deletions.
1 change: 1 addition & 0 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 32 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Transaction>;
pub type PostHook = fn(&mut Context, char);
Expand Down Expand Up @@ -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);
}

Expand Down
11 changes: 11 additions & 0 deletions helix-term/src/ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,17 @@ impl<T: Item + 'static> Component for Menu<T> {
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') => {
Expand Down
246 changes: 246 additions & 0 deletions helix-term/tests/test/commands/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
// supertab 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)#
}
"}),
),
];

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(())
}
16 changes: 10 additions & 6 deletions helix-term/tests/test/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,19 @@ pub fn temp_file_with_contents<S: AsRef<str>>(
/// 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
Expand Down
4 changes: 4 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -633,6 +636,7 @@ impl Default for Config {
bufferline: BufferLine::default(),
indent_guides: IndentGuidesConfig::default(),
color_modes: false,
supertab: None,
}
}
}
Expand Down

0 comments on commit 90644c7

Please sign in to comment.