Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smart tab (aka tabout) #4443

Merged
merged 2 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it
```

### `[editor.smart-tab]` Section


| Key | Description | Default |
|------------|-------------|---------|
| `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` |
| `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` |
81 changes: 80 additions & 1 deletion helix-core/src/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
syntax::LanguageConfiguration,
text_annotations::TextAnnotations,
textobject::TextObject,
visual_offset_from_block, Range, RopeSlice,
visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
};

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -556,6 +556,85 @@ pub fn goto_treesitter_object(
last_range
}

fn find_parent_start(mut node: Node) -> Option<Node> {
let start = node.start_byte();

while node.start_byte() >= start || !node.is_named() {
node = node.parent()?;
}

Some(node)
}

pub fn move_parent_node_end(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
dir: Direction,
movement: Movement,
) -> Selection {
let tree = syntax.tree();

selection.transform(|range| {
let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to());

let mut node = match tree
.root_node()
.named_descendant_for_byte_range(start_from, start_to)
{
Some(node) => node,
None => {
log::debug!(
"no descendant found for byte range: {} - {}",
start_from,
start_to
);
return range;
}
};

let mut end_head = match dir {
// moving forward, we always want to move one past the end of the
// current node, so use the end byte of the current node, which is an exclusive
// end of the range
Direction::Forward => text.byte_to_char(node.end_byte()),

// moving backward, we want the cursor to land on the start char of
// the current node, or if it is already at the start of a node, to traverse up to
// the parent
Direction::Backward => {
let end_head = text.byte_to_char(node.start_byte());

// if we're already on the beginning, look up to the parent
if end_head == range.cursor(text) {
node = find_parent_start(node).unwrap_or(node);
text.byte_to_char(node.start_byte())
} else {
end_head
}
}
};

if movement == Movement::Move {
// preserve direction of original range
if range.direction() == Direction::Forward {
Range::new(end_head, end_head + 1)
} else {
Range::new(end_head + 1, end_head)
}
} else {
// if we end up with a forward range, then adjust it to be one past
// where we want
if end_head >= range.anchor {
end_head += 1;
}

Range::new(range.anchor, end_head)
}
})
}

#[cfg(test)]
mod test {
use ropey::Rope;
Expand Down
9 changes: 3 additions & 6 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
78 changes: 78 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ impl MappableCommand {
move_prev_long_word_start, "Move to start of previous long word",
move_next_long_word_end, "Move to end of next long word",
move_prev_long_word_end, "Move to end of previous long word",
move_parent_node_end, "Move to end of the parent node",
move_parent_node_start, "Move to beginning of the parent node",
extend_next_word_start, "Extend to start of next word",
extend_prev_word_start, "Extend to start of previous word",
extend_next_word_end, "Extend to end of next word",
Expand All @@ -255,6 +257,8 @@ impl MappableCommand {
extend_prev_long_word_start, "Extend to start of previous long word",
extend_next_long_word_end, "Extend to end of next long word",
extend_prev_long_word_end, "Extend to end of prev long word",
extend_parent_node_end, "Extend to end of the parent node",
extend_parent_node_start, "Extend to beginning of the parent node",
find_till_char, "Move till next occurrence of char",
find_next_char, "Move to next occurrence of char",
extend_till_char, "Extend till next occurrence of char",
Expand Down Expand Up @@ -361,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",
Expand Down Expand Up @@ -2517,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
Expand Down Expand Up @@ -3440,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);
Expand All @@ -3465,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
Expand Down Expand Up @@ -4605,6 +4640,49 @@ fn select_prev_sibling(cx: &mut Context) {
select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
}

fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
let motion = move |editor: &mut Editor| {
let (view, doc) = current!(editor);

if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let current_selection = doc.selection(view.id);

let selection = movement::move_parent_node_end(
syntax,
text,
current_selection.clone(),
dir,
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();
}
};

cx.editor.apply_motion(motion);
}

pub fn move_parent_node_end(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Forward, Movement::Move)
}

pub fn move_parent_node_start(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Backward, Movement::Move)
}

pub fn extend_parent_node_end(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Forward, Movement::Extend)
}

pub fn extend_parent_node_start(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Backward, Movement::Extend)
}

fn match_brackets(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let is_select = cx.editor.mode == Mode::Select;
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
Expand Down
8 changes: 7 additions & 1 deletion helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"A-i" | "A-down" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,
"A-e" => move_parent_node_end,
"A-b" => move_parent_node_start,

"%" => select_all,
"x" => extend_line_below,
Expand Down Expand Up @@ -336,6 +338,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"B" => extend_prev_long_word_start,
"E" => extend_next_long_word_end,

"A-e" => extend_parent_node_end,
"A-b" => extend_parent_node_start,

"n" => extend_search_next,
"N" => extend_search_prev,

Expand Down Expand Up @@ -368,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,
Expand Down
17 changes: 16 additions & 1 deletion helix-term/src/ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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') => {
Expand Down
1 change: 1 addition & 0 deletions helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use helix_term::application::Application;

use super::*;

mod movement;
mod write;

#[tokio::test(flavor = "multi_thread")]
Expand Down
Loading