snippets: Preserve leading whitespace (#31933)

Closes #18481

Release Notes:

- Snippet insertions now preserve leading whitespace instead of using
language-specific auto-indentation.
This commit is contained in:
Michael Sloan 2025-06-02 20:37:06 -06:00 committed by GitHub
parent feeda7fa37
commit 56d4c0af9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 119 additions and 98 deletions

View file

@ -8929,7 +8929,10 @@ impl Editor {
.iter()
.cloned()
.map(|range| (range, snippet_text.clone()));
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
let autoindent_mode = AutoindentMode::Block {
original_indent_columns: Vec::new(),
};
buffer.edit(edits, Some(autoindent_mode), cx);
let snapshot = &*buffer.read(cx);
let snippet = &snippet;

View file

@ -8513,108 +8513,123 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
async fn test_snippets(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let (text, insertion_ranges) = marked_text_ranges(
indoc! {"
a.ˇ b
a.ˇ b
a.ˇ b
"},
false,
);
let mut cx = EditorTestContext::new(cx).await;
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
cx.set_state(indoc! {"
a.ˇ b
a.ˇ b
a.ˇ b
"});
editor.update_in(cx, |editor, window, cx| {
cx.update_editor(|editor, window, cx| {
let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
let insertion_ranges = editor
.selections
.all(cx)
.iter()
.map(|s| s.range().clone())
.collect::<Vec<_>>();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
fn assert(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>(cx), selection_ranges);
}
assert(
editor,
cx,
indoc! {"
a.f(«one», two, «three») b
a.f(«one», two, «three») b
a.f(«one», two, «three») b
"},
);
// Can't move earlier than the first tab stop
assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(«one», two, «three») b
a.f(«one», two, «three») b
a.f(«one», two, «three») b
"},
);
assert!(editor.move_to_next_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(one, «two», three) b
a.f(one, «two», three) b
a.f(one, «two», three) b
"},
);
editor.move_to_prev_snippet_tabstop(window, cx);
assert(
editor,
cx,
indoc! {"
a.f(«one», two, «three») b
a.f(«one», two, «three») b
a.f(«one», two, «three») b
"},
);
assert!(editor.move_to_next_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(one, «two», three) b
a.f(one, «two», three) b
a.f(one, «two», three) b
"},
);
assert!(editor.move_to_next_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"},
);
// As soon as the last tab stop is reached, snippet state is gone
editor.move_to_prev_snippet_tabstop(window, cx);
assert(
editor,
cx,
indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"},
);
});
cx.assert_editor_state(indoc! {"
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
"});
// Can't move earlier than the first tab stop
cx.update_editor(|editor, window, cx| {
assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
});
cx.assert_editor_state(indoc! {"
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"});
// As soon as the last tab stop is reached, snippet state is gone
cx.update_editor(|editor, window, cx| {
assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
});
cx.assert_editor_state(indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"});
}
#[gpui::test]
async fn test_snippet_indentation(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_editor(|editor, window, cx| {
let snippet = Snippet::parse(indoc! {"
/*
* Multiline comment with leading indentation
*
* $1
*/
$0"})
.unwrap();
let insertion_ranges = editor
.selections
.all(cx)
.iter()
.map(|s| s.range().clone())
.collect::<Vec<_>>();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
});
cx.assert_editor_state(indoc! {"
/*
* Multiline comment with leading indentation
*
* ˇ
*/
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
/*
* Multiline comment with leading indentation
*
*
*/
ˇ"});
}
#[gpui::test]

View file

@ -532,7 +532,9 @@ impl EditorTestContext {
#[track_caller]
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
generate_marked_text(&self.buffer_text(), &expected_selections, true)
.replace(" \n", "\n");
self.assert_selections(expected_selections, expected_marked_text)
}
@ -561,7 +563,8 @@ impl EditorTestContext {
) {
let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
generate_marked_text(&self.buffer_text(), &actual_selections, true)
.replace(" \n", "\n");
if expected_selections != actual_selections {
pretty_assertions::assert_eq!(
actual_marked_text,