Detailed changes
@@ -731,6 +731,14 @@
"tab": "editor::ComposeCompletion"
}
},
+ {
+ "context": "Editor && in_snippet",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-right": "editor::NextSnippetTabstop",
+ "alt-left": "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
@@ -801,6 +801,14 @@
"tab": "editor::ComposeCompletion"
}
},
+ {
+ "context": "Editor && in_snippet",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-right": "editor::NextSnippetTabstop",
+ "alt-left": "editor::PreviousSnippetTabstop"
+ }
+ },
{
"context": "Editor && edit_prediction",
"bindings": {
@@ -736,6 +736,14 @@
"tab": "editor::ComposeCompletion"
}
},
+ {
+ "context": "Editor && in_snippet",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-right": "editor::NextSnippetTabstop",
+ "alt-left": "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
@@ -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.
@@ -2439,6 +2439,10 @@ impl Editor {
key_context.add("renaming");
}
+ if !self.snippet_stack.is_empty() {
+ key_context.add("in_snippet");
+ }
+
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(menu)) => {
if menu.visible() {
@@ -9947,6 +9951,38 @@ impl Editor {
self.outdent(&Outdent, window, cx);
}
+ pub fn next_snippet_tabstop(
+ &mut self,
+ _: &NextSnippetTabstop,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.mode.is_single_line() || self.snippet_stack.is_empty() {
+ return;
+ }
+
+ if self.move_to_next_snippet_tabstop(window, cx) {
+ self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
+ return;
+ }
+ }
+
+ pub fn previous_snippet_tabstop(
+ &mut self,
+ _: &PreviousSnippetTabstop,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.mode.is_single_line() || self.snippet_stack.is_empty() {
+ return;
+ }
+
+ if self.move_to_prev_snippet_tabstop(window, cx) {
+ self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
+ return;
+ }
+ }
+
pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
cx.propagate();
@@ -11066,6 +11066,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<Editor>, 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::<usize>(&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::<Vec<_>>();
+
+ 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, |_| {});
@@ -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);