on master help sign in
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.

commit15e07d4db893aec7b9e117c1f88400a68ba98ae2 authorSkyler Hawthorne <skyler@dead10ck.com> dateSat 8 Oct 2022, 18:14 (4 years ago) parent93acb53
8 files changed+338−10
book/src/configuration.md+8 helix-term/src/application.rs+3−6 helix-term/src/commands.rs+36−2 helix-term/src/config.rs+1 helix-term/src/keymap/default.rs+2−1 helix-term/src/ui/menu.rs+16−1 helix-term/tests/test/commands/movement.rs+253 helix-view/src/editor.rs+19
book/src/configuration.md+8
@@ -347,3 +347,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
347347max-indent-retain = 0
348348wrap-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
@@ -33,12 +33,9 @@ use crate::{
3333};
3434
3535use 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
4340use anyhow::{Context, Error};
4441
helix-term/src/commands.rs+36−2
@@ -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
46364670pub fn move_parent_node_end(cx: &mut Context) {
helix-term/src/config.rs+1
@@ -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
@@ -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
@@ -11,7 +11,7 @@ pub use tui::widgets::{Cell, Row};
1111use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
1212use fuzzy_matcher::FuzzyMatcher;
1313
14−use helix_view::{graphics::Rect, Editor};
14+use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
1515use tui::layout::Constraint;
1616
1717pub 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') => {
helix-term/tests/test/commands/movement.rs+253
@@ -197,3 +197,256 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
197197
198198 Ok(())
199199}
200+
201+#[tokio::test(flavor = "multi_thread")]
202+async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
203+ let tests = vec![
204+ // single cursor stays single cursor, first goes to end of current
205+ // node, then parent
206+ (
207+ helpers::platform_line(indoc! {r##"
208+ fn foo() {
209+ let result = if true {
210+ "yes"
211+ } else {
212+ "no#["|]#
213+ }
214+ }
215+ "##}),
216+ "i<tab>",
217+ helpers::platform_line(indoc! {"\
218+ fn foo() {
219+ let result = if true {
220+ \"yes\"
221+ } else {
222+ \"no\"#[|\n]#
223+ }
224+ }
225+ "}),
226+ ),
227+ (
228+ helpers::platform_line(indoc! {"\
229+ fn foo() {
230+ let result = if true {
231+ \"yes\"
232+ } else {
233+ \"no\"#[\n|]#
234+ }
235+ }
236+ "}),
237+ "i<tab>",
238+ helpers::platform_line(indoc! {"\
239+ fn foo() {
240+ let result = if true {
241+ \"yes\"
242+ } else {
243+ \"no\"
244+ }#[|\n]#
245+ }
246+ "}),
247+ ),
248+ // appending to the end of a line should still look at the current
249+ // line, not the next one
250+ (
251+ helpers::platform_line(indoc! {"\
252+ fn foo() {
253+ let result = if true {
254+ \"yes\"
255+ } else {
256+ \"no#[\"|]#
257+ }
258+ }
259+ "}),
260+ "a<tab>",
261+ helpers::platform_line(indoc! {"\
262+ fn foo() {
263+ let result = if true {
264+ \"yes\"
265+ } else {
266+ \"no\"
267+ }#[\n|]#
268+ }
269+ "}),
270+ ),
271+ // before cursor is all whitespace, so insert tab
272+ (
273+ helpers::platform_line(indoc! {"\
274+ fn foo() {
275+ let result = if true {
276+ \"yes\"
277+ } else {
278+ #[\"no\"|]#
279+ }
280+ }
281+ "}),
282+ "i<tab>",
283+ helpers::platform_line(indoc! {"\
284+ fn foo() {
285+ let result = if true {
286+ \"yes\"
287+ } else {
288+ #[|\"no\"]#
289+ }
290+ }
291+ "}),
292+ ),
293+ // if selection spans multiple lines, it should still only look at the
294+ // line on which the head is
295+ (
296+ helpers::platform_line(indoc! {"\
297+ fn foo() {
298+ let result = if true {
299+ #[\"yes\"
300+ } else {
301+ \"no\"|]#
302+ }
303+ }
304+ "}),
305+ "a<tab>",
306+ helpers::platform_line(indoc! {"\
307+ fn foo() {
308+ let result = if true {
309+ \"yes\"
310+ } else {
311+ \"no\"
312+ }#[\n|]#
313+ }
314+ "}),
315+ ),
316+ (
317+ helpers::platform_line(indoc! {"\
318+ fn foo() {
319+ let result = if true {
320+ #[\"yes\"
321+ } else {
322+ \"no\"|]#
323+ }
324+ }
325+ "}),
326+ "i<tab>",
327+ helpers::platform_line(indoc! {"\
328+ fn foo() {
329+ let result = if true {
330+ #[|\"yes\"
331+ } else {
332+ \"no\"]#
333+ }
334+ }
335+ "}),
336+ ),
337+ (
338+ helpers::platform_line(indoc! {"\
339+ fn foo() {
340+ #[l|]#et result = if true {
341+ #(\"yes\"
342+ } else {
343+ \"no\"|)#
344+ }
345+ }
346+ "}),
347+ "i<tab>",
348+ helpers::platform_line(indoc! {"\
349+ fn foo() {
350+ #[|l]#et result = if true {
351+ #(|\"yes\"
352+ } else {
353+ \"no\")#
354+ }
355+ }
356+ "}),
357+ ),
358+ (
359+ helpers::platform_line(indoc! {"\
360+ fn foo() {
361+ let result = if true {
362+ \"yes\"#[\n|]#
363+ } else {
364+ \"no\"#(\n|)#
365+ }
366+ }
367+ "}),
368+ "i<tab>",
369+ helpers::platform_line(indoc! {"\
370+ fn foo() {
371+ let result = if true {
372+ \"yes\"
373+ }#[| ]#else {
374+ \"no\"
375+ }#(|\n)#
376+ }
377+ "}),
378+ ),
379+ (
380+ helpers::platform_line(indoc! {"\
381+ fn foo() {
382+ let result = if true {
383+ #[\"yes\"|]#
384+ } else {
385+ #(\"no\"|)#
386+ }
387+ }
388+ "}),
389+ "i<tab>",
390+ helpers::platform_line(indoc! {"\
391+ fn foo() {
392+ let result = if true {
393+ #[|\"yes\"]#
394+ } else {
395+ #(|\"no\")#
396+ }
397+ }
398+ "}),
399+ ),
400+ // if any cursors are not preceded by all whitespace, then do the
401+ // smart_tab action
402+ (
403+ helpers::platform_line(indoc! {"\
404+ fn foo() {
405+ let result = if true {
406+ #[\"yes\"\n|]#
407+ } else {
408+ \"no#(\"\n|)#
409+ }
410+ }
411+ "}),
412+ "i<tab>",
413+ helpers::platform_line(indoc! {"\
414+ fn foo() {
415+ let result = if true {
416+ \"yes\"
417+ }#[| ]#else {
418+ \"no\"
419+ }#(|\n)#
420+ }
421+ "}),
422+ ),
423+ // Ctrl-tab always inserts a tab
424+ (
425+ helpers::platform_line(indoc! {"\
426+ fn foo() {
427+ let result = if true {
428+ #[\"yes\"\n|]#
429+ } else {
430+ \"no#(\"\n|)#
431+ }
432+ }
433+ "}),
434+ "i<S-tab>",
435+ helpers::platform_line(indoc! {"\
436+ fn foo() {
437+ let result = if true {
438+ #[|\"yes\"\n]#
439+ } else {
440+ \"no #(|\"\n)#
441+ }
442+ }
443+ "}),
444+ ),
445+ ];
446+
447+ for test in tests {
448+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
449+ }
450+
451+ Ok(())
452+}
helix-view/src/editor.rs+19
@@ -287,6 +287,24 @@ pub struct Config {
287287 pub workspace_lsp_roots: Vec<PathBuf>,
288288 /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
289289 pub default_line_ending: LineEndingConfig,
290+ /// Enables smart tab
291+ pub smart_tab: Option<SmartTabConfig>,
292+}
293+
294+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
295+#[serde(rename_all = "kebab-case", default)]
296+pub struct SmartTabConfig {
297+ pub enable: bool,
298+ pub supersede_menu: bool,
299+}
300+
301+impl Default for SmartTabConfig {
302+ fn default() -> Self {
303+ SmartTabConfig {
304+ enable: true,
305+ supersede_menu: false,
306+ }
307+ }
290308}
291309
292310#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -820,6 +838,7 @@ impl Default for Config {
820838 completion_replace: false,
821839 workspace_lsp_roots: Vec::new(),
822840 default_line_ending: LineEndingConfig::default(),
841+ smart_tab: Some(SmartTabConfig::default()),
823842 }
824843 }
825844}