mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 09:26:19 +04:00
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.
This commit is contained in:
parent
93acb53812
commit
15e07d4db8
@ -347,3 +347,11 @@ ### `[editor.soft-wrap]` Section
|
||||
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` |
|
||||
|
@ -33,12 +33,9 @@
|
||||
};
|
||||
|
||||
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};
|
||||
|
||||
|
@ -365,6 +365,7 @@ pub fn doc(&self) -> &str {
|
||||
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 @@ fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
|
||||
}
|
||||
|
||||
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 fn insert_char(cx: &mut Context, c: char) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -109,6 +109,7 @@ pub fn load(
|
||||
)?,
|
||||
}
|
||||
}
|
||||
|
||||
// these are just two io errors return the one for the global config
|
||||
(Err(err), Err(_)) => return Err(err),
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -11,7 +11,7 @@
|
||||
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 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
||||
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') => {
|
||||
|
@ -197,3 +197,256 @@ fn foo() {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
@ -287,6 +287,24 @@ pub struct Config {
|
||||
pub workspace_lsp_roots: Vec<PathBuf>,
|
||||
/// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
|
||||
pub default_line_ending: LineEndingConfig,
|
||||
/// Enables smart tab
|
||||
pub smart_tab: Option<SmartTabConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "kebab-case", default)]
|
||||
pub struct SmartTabConfig {
|
||||
pub enable: bool,
|
||||
pub supersede_menu: bool,
|
||||
}
|
||||
|
||||
impl Default for SmartTabConfig {
|
||||
fn default() -> Self {
|
||||
SmartTabConfig {
|
||||
enable: true,
|
||||
supersede_menu: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -820,6 +838,7 @@ fn default() -> Self {
|
||||
completion_replace: false,
|
||||
workspace_lsp_roots: Vec::new(),
|
||||
default_line_ending: LineEndingConfig::default(),
|
||||
smart_tab: Some(SmartTabConfig::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user