editor: Add action to move between snippet tabstop positions v2 (#42127)
Closes https://github.com/zed-industries/zed/issues/41407 This PR fixes the issues that caused #41407 to be reverted in #42008. Namely that the action context didn't take into account if a snippet could move backwards or forwards, and the action shared the same key mapping as `editor::MoveToPreviousWordStart` and `editor::MoveToNextWordEnd`. I changed the default key mapping for the move to snippet tabstop to tab and shift-tab to match the default behavior of other editors. Release Notes: - Editor: Add actions to move between snippet tabstop positions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Self>,
|
||||
) {
|
||||
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<Self>,
|
||||
) {
|
||||
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<Self>) {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
Reference in New Issue
Block a user