Skip to content

Commit 15e07d4

Browse files
dead10ckthe-mikedavis
authored andcommitted
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.
1 parent 93acb53 commit 15e07d4

File tree

8 files changed

+338
-10
lines changed

8 files changed

+338
-10
lines changed

book/src/configuration.md

+8
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
347347
max-indent-retain = 0
348348
wrap-indicator = "" # set wrap-indicator to "" to hide it
349349
```
350+
351+
### `[editor.smart-tab]` Section
352+
353+
354+
| Key | Description | Default |
355+
|------------|-------------|---------|
356+
| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
357+
| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |

helix-term/src/application.rs

+3-6
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,9 @@ use crate::{
3333
};
3434

3535
use log::{debug, error, warn};
36-
use std::{
37-
collections::btree_map::Entry,
38-
io::{stdin, stdout},
39-
path::Path,
40-
sync::Arc,
41-
};
36+
#[cfg(not(feature = "integration"))]
37+
use std::io::stdout;
38+
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
4239

4340
use anyhow::{Context, Error};
4441

helix-term/src/commands.rs

+36-2
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ impl MappableCommand {
365365
extend_to_line_end, "Extend to line end",
366366
extend_to_line_end_newline, "Extend to line end",
367367
signature_help, "Show signature help",
368+
smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
368369
insert_tab, "Insert tab char",
369370
insert_newline, "Insert newline char",
370371
delete_char_backward, "Delete previous char",
@@ -2521,6 +2522,10 @@ fn insert_mode(cx: &mut Context) {
25212522
.transform(|range| Range::new(range.to(), range.from()));
25222523

25232524
doc.set_selection(view.id, selection);
2525+
2526+
// [TODO] temporary workaround until we're not using the idle timer to
2527+
// trigger auto completions any more
2528+
cx.editor.clear_idle_timer();
25242529
}
25252530

25262531
// inserts at the end of each selection
@@ -3444,6 +3449,7 @@ pub mod insert {
34443449
}
34453450

34463451
use helix_core::auto_pairs;
3452+
use helix_view::editor::SmartTabConfig;
34473453

34483454
pub fn insert_char(cx: &mut Context, c: char) {
34493455
let (view, doc) = current_ref!(cx.editor);
@@ -3469,6 +3475,31 @@ pub mod insert {
34693475
}
34703476
}
34713477

3478+
pub fn smart_tab(cx: &mut Context) {
3479+
let (view, doc) = current_ref!(cx.editor);
3480+
let view_id = view.id;
3481+
3482+
if matches!(
3483+
cx.editor.config().smart_tab,
3484+
Some(SmartTabConfig { enable: true, .. })
3485+
) {
3486+
let cursors_after_whitespace = doc.selection(view_id).ranges().iter().all(|range| {
3487+
let cursor = range.cursor(doc.text().slice(..));
3488+
let current_line_num = doc.text().char_to_line(cursor);
3489+
let current_line_start = doc.text().line_to_char(current_line_num);
3490+
let left = doc.text().slice(current_line_start..cursor);
3491+
left.chars().all(|c| c.is_whitespace())
3492+
});
3493+
3494+
if !cursors_after_whitespace {
3495+
move_parent_node_end(cx);
3496+
return;
3497+
}
3498+
}
3499+
3500+
insert_tab(cx);
3501+
}
3502+
34723503
pub fn insert_tab(cx: &mut Context) {
34733504
let (view, doc) = current!(cx.editor);
34743505
// 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) {
46264657
);
46274658

46284659
doc.set_selection(view.id, selection);
4660+
4661+
// [TODO] temporary workaround until we're not using the idle timer to
4662+
// trigger auto completions any more
4663+
editor.clear_idle_timer();
46294664
}
46304665
};
46314666

4632-
motion(cx.editor);
4633-
cx.editor.last_motion = Some(Motion(Box::new(motion)));
4667+
cx.editor.apply_motion(motion);
46344668
}
46354669

46364670
pub fn move_parent_node_end(cx: &mut Context) {

helix-term/src/config.rs

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ impl Config {
109109
)?,
110110
}
111111
}
112+
112113
// these are just two io errors return the one for the global config
113114
(Err(err), Err(_)) => return Err(err),
114115
};

helix-term/src/keymap/default.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
373373
"C-h" | "backspace" | "S-backspace" => delete_char_backward,
374374
"C-d" | "del" => delete_char_forward,
375375
"C-j" | "ret" => insert_newline,
376-
"tab" => insert_tab,
376+
"tab" => smart_tab,
377+
"S-tab" => insert_tab,
377378

378379
"up" => move_visual_line_up,
379380
"down" => move_visual_line_down,

helix-term/src/ui/menu.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub use tui::widgets::{Cell, Row};
1111
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
1212
use fuzzy_matcher::FuzzyMatcher;
1313

14-
use helix_view::{graphics::Rect, Editor};
14+
use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
1515
use tui::layout::Constraint;
1616

1717
pub trait Item {
@@ -247,6 +247,21 @@ impl<T: Item + 'static> Component for Menu<T> {
247247
compositor.pop();
248248
}));
249249

250+
// Ignore tab key when supertab is turned on in order not to interfere
251+
// with it. (Is there a better way to do this?)
252+
if (event == key!(Tab) || event == shift!(Tab))
253+
&& cx.editor.config().auto_completion
254+
&& matches!(
255+
cx.editor.config().smart_tab,
256+
Some(SmartTabConfig {
257+
enable: true,
258+
supersede_menu: true,
259+
})
260+
)
261+
{
262+
return EventResult::Ignored(None);
263+
}
264+
250265
match event {
251266
// esc or ctrl-c aborts the completion and closes the menu
252267
key!(Esc) | ctrl!('c') => {

0 commit comments

Comments
 (0)