diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 8a7173f8af24375804d50185f5b958c8f08b5e0b..57c799d8fd4f74478f5fdf469ff142e0c26e4503 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -735,6 +735,20 @@ "tab": "editor::ComposeCompletion" } }, + { + "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", + "use_key_equivalents": true, + "bindings": { + "tab": "editor::NextSnippetTabstop" + } + }, + { + "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", + "use_key_equivalents": true, + "bindings": { + "shift-tab": "editor::PreviousSnippetTabstop" + } + }, // Bindings for accepting edit predictions // // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 96095ed84971a8c70e05caffb64a5a2315ac3738..9d23eeb8cde071e20e5d3e4d7f873b1f668501b2 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -805,6 +805,20 @@ "tab": "editor::ComposeCompletion" } }, + { + "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", + "use_key_equivalents": true, + "bindings": { + "tab": "editor::NextSnippetTabstop" + } + }, + { + "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", + "use_key_equivalents": true, + "bindings": { + "shift-tab": "editor::PreviousSnippetTabstop" + } + }, { "context": "Editor && edit_prediction", "bindings": { diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 3050c7343e4d465f04bec302e9c1d956292635ab..3fe5778e5c1219ee2b5fc9691ac876ec61debe06 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -739,6 +739,20 @@ "tab": "editor::ComposeCompletion" } }, + { + "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", + "use_key_equivalents": true, + "bindings": { + "tab": "editor::NextSnippetTabstop" + } + }, + { + "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", + "use_key_equivalents": true, + "bindings": { + "shift-tab": "editor::PreviousSnippetTabstop" + } + }, // Bindings for accepting edit predictions // // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 38ae42c3814fa09e50a92dcc20f0a34bad82ea40..276f20a7aacc9315f27a929876984342edc8d394 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -621,6 +621,8 @@ actions!( NextEditPrediction, /// Scrolls to the next screen. NextScreen, + /// Goes to the next snippet tabstop if one exists. + NextSnippetTabstop, /// Opens the context menu at cursor position. OpenContextMenu, /// Opens excerpts from the current file. @@ -654,6 +656,8 @@ actions!( Paste, /// Navigates to the previous edit prediction. PreviousEditPrediction, + /// Goes to the previous snippet tabstop if one exists. + PreviousSnippetTabstop, /// Redoes the last undone edit. Redo, /// Redoes the last selection change. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 19a063d720bb87e605ede1d493795450215f47d7..3bbd366795c4eab5a3febee2520213788799c451 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2511,6 +2511,18 @@ impl Editor { key_context.add("renaming"); } + if let Some(snippet_stack) = self.snippet_stack.last() { + key_context.add("in_snippet"); + + if snippet_stack.active_index > 0 { + key_context.add("has_previous_tabstop"); + } + + if snippet_stack.active_index < snippet_stack.ranges.len().saturating_sub(1) { + key_context.add("has_next_tabstop"); + } + } + match self.context_menu.borrow().as_ref() { Some(CodeContextMenu::Completions(menu)) => { if menu.visible() { @@ -10046,6 +10058,42 @@ impl Editor { self.outdent(&Outdent, window, cx); } + pub fn next_snippet_tabstop( + &mut self, + _: &NextSnippetTabstop, + window: &mut Window, + cx: &mut Context, + ) { + if self.mode.is_single_line() || self.snippet_stack.is_empty() { + cx.propagate(); + return; + } + + if self.move_to_next_snippet_tabstop(window, cx) { + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + return; + } + cx.propagate(); + } + + pub fn previous_snippet_tabstop( + &mut self, + _: &PreviousSnippetTabstop, + window: &mut Window, + cx: &mut Context, + ) { + if self.mode.is_single_line() || self.snippet_stack.is_empty() { + cx.propagate(); + return; + } + + if self.move_to_prev_snippet_tabstop(window, cx) { + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + return; + } + cx.propagate(); + } + pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { if self.mode.is_single_line() { cx.propagate(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2428032a4aae54d746cad50ac676e449cd79d9ce..3709709c71fd1355014fce3f48681c632df7e18d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -11137,6 +11137,129 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + fn assert_state(editor: &mut Editor, cx: &mut Context, marked_text: &str) { + let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); + assert_eq!(editor.text(cx), expected_text); + assert_eq!( + editor + .selections + .ranges::(&editor.display_snapshot(cx)), + selection_ranges + ); + } + + let (text, insertion_ranges) = marked_text_ranges( + indoc! {" + ˇ + "}, + false, + ); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + _ = editor.update_in(cx, |editor, window, cx| { + let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap(); + + editor + .insert_snippet(&insertion_ranges, snippet, window, cx) + .unwrap(); + + assert_state( + editor, + cx, + indoc! {" + type «» = ;• + "}, + ); + + assert!( + editor.context_menu_visible(), + "Context menu should be visible for placeholder choices" + ); + + editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx); + + assert_state( + editor, + cx, + indoc! {" + type = «»;• + "}, + ); + + assert!( + !editor.context_menu_visible(), + "Context menu should be hidden after moving to next tabstop" + ); + + editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx); + + assert_state( + editor, + cx, + indoc! {" + type = ; ˇ + "}, + ); + + editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx); + + assert_state( + editor, + cx, + indoc! {" + type = ; ˇ + "}, + ); + }); + + _ = editor.update_in(cx, |editor, window, cx| { + editor.select_all(&SelectAll, window, cx); + editor.backspace(&Backspace, window, cx); + + let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap(); + let insertion_ranges = editor + .selections + .all(&editor.display_snapshot(cx)) + .iter() + .map(|s| s.range()) + .collect::>(); + + editor + .insert_snippet(&insertion_ranges, snippet, window, cx) + .unwrap(); + + assert_state(editor, cx, "fn «» = value;•"); + + assert!( + editor.context_menu_visible(), + "Context menu should be visible for placeholder choices" + ); + + editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx); + + assert_state(editor, cx, "fn = «valueˇ»;•"); + + editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx); + + assert_state(editor, cx, "fn «» = value;•"); + + assert!( + editor.context_menu_visible(), + "Context menu should be visible again after returning to first tabstop" + ); + + editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx); + + assert_state(editor, cx, "fn «» = value;•"); + }); +} + #[gpui::test] async fn test_snippets(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 968e248759dcb19bc2f2ccd628a49df5a0afc7f0..7854b89d1494cacbc8fed5eba83e2461bbf45bac 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -232,6 +232,8 @@ impl EditorElement { register_action(editor, window, Editor::blame_hover); register_action(editor, window, Editor::delete); register_action(editor, window, Editor::tab); + register_action(editor, window, Editor::next_snippet_tabstop); + register_action(editor, window, Editor::previous_snippet_tabstop); register_action(editor, window, Editor::backtab); register_action(editor, window, Editor::indent); register_action(editor, window, Editor::outdent);