Implement indent conversion editor commands (#32340)
## Description of Feature or Change Zed currently lacks a built-in way to convert a file’s indentation style on the fly. While it's possible to change indentation behavior via global or language-specific settings, these changes are persistent and broad in scope as they apply to all files or all files of a given language. We believe this could be improved for quick one-off adjustments to specific files. This PR introduces two new editor commands: `Editor::convert_indentation_to_spaces` and `Editor::convert_indentation_to_tabs`. These commands allow users to convert the indentation of either the entire buffer or a selection of lines, to spaces or tabs. Indentation levels are preserved, and any mixed whitespace lines are properly normalized. This feature is inspired by VS Code’s "Convert Indentation to Tabs/Spaces" commands, but offers faster execution and supports selection-based conversion, making it more flexible for quick formatting changes. ## Implementation Details To enable selection-based indentation conversion, we initially considered reusing the existing `Editor::manipulate_lines` function, which handles selections for line-based manipulations. However, this method was designed specifically for operations like sorting or reversing lines, and does not allow modifications to the line contents themselves. To address this limitation, we refactored the method into a more flexible version: `Editor::manipulate_generic_lines`. This new method passes a reference to the selected text directly into a callback, giving the callback full control over how to process and construct the resulting lines. The callback returns a `String` containing the modified text, as well as the number of lines before and after the transformation. These counts are computed using `.len()` on the line vectors during manipulation, which is more efficient than calculating them after the fact. ```rust fn manipulate_generic_lines<M>( &mut self, window: &mut Window, cx: &mut Context<Self>, mut manipulate: M, ) where M: FnMut(&str) -> (String, usize, usize), { // ... Get text from buffer.text_for_range() ... let (new_text, lines_before, lines_after) = manipulate(&text); // ... ``` We now introduce two specialized methods: `Editor::manipulate_mutable_lines` and `Editor::manipulate_immutable_lines`. Each editor command selects the appropriate method based on whether it needs to modify line contents or simply reorder them. This distinction is important for performance: when line contents remain unchanged, working with an immutable reference as `&mut Vec<&str>` is both faster and more memory-efficient than using an owned `&mut Vec<String>`. ## Demonstration https://github.com/user-attachments/assets/e50b37ea-a128-4c2a-b252-46c3c4530d97 Release Notes: - Added `editor::ConvertIndentationToSpaces` and `editor::ConvertIndentationToTabs` actions to change editor indents --------- Co-authored-by: Pedro Silveira <pedroruanosilveira@tecnico.ulisboa.pt>
This commit is contained in:
parent
4396ac9dd6
commit
c979452c2d
5 changed files with 608 additions and 49 deletions
|
@ -3976,7 +3976,7 @@ async fn test_custom_newlines_cause_no_false_positive_diffs(
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||
async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
@ -4021,8 +4021,8 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
|||
|
||||
// Skip testing shuffle_line()
|
||||
|
||||
// From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
|
||||
// Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
|
||||
// From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
|
||||
// Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
|
||||
|
||||
// Don't manipulate when cursor is on single line, but expand the selection
|
||||
cx.set_state(indoc! {"
|
||||
|
@ -4089,7 +4089,7 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
|||
bbˇ»b
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
|
||||
e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«aaa
|
||||
|
@ -4103,7 +4103,7 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
|||
bbbˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.manipulate_lines(window, cx, |lines| {
|
||||
e.manipulate_immutable_lines(window, cx, |lines| {
|
||||
lines.pop();
|
||||
})
|
||||
});
|
||||
|
@ -4117,7 +4117,7 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
|||
bbbˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.manipulate_lines(window, cx, |lines| {
|
||||
e.manipulate_immutable_lines(window, cx, |lines| {
|
||||
lines.drain(..);
|
||||
})
|
||||
});
|
||||
|
@ -4217,7 +4217,7 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
|
||||
async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
@ -4277,7 +4277,7 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
|
|||
aaaˇ»aa
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.manipulate_lines(window, cx, |lines| lines.push("added line"))
|
||||
e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«2
|
||||
|
@ -4298,7 +4298,7 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
|
|||
aaaˇ»aa
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.manipulate_lines(window, cx, |lines| {
|
||||
e.manipulate_immutable_lines(window, cx, |lines| {
|
||||
lines.pop();
|
||||
})
|
||||
});
|
||||
|
@ -4309,6 +4309,222 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
|
|||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(3)
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// MULTI SELECTION
|
||||
// Ln.1 "«" tests empty lines
|
||||
// Ln.9 tests just leading whitespace
|
||||
cx.set_state(indoc! {"
|
||||
«
|
||||
abc // No indentationˇ»
|
||||
«\tabc // 1 tabˇ»
|
||||
\t\tabc « ˇ» // 2 tabs
|
||||
\t ab«c // Tab followed by space
|
||||
\tabc // Space followed by tab (3 spaces should be the result)
|
||||
\t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
|
||||
abˇ»ˇc ˇ ˇ // Already space indented«
|
||||
\t
|
||||
\tabc\tdef // Only the leading tab is manipulatedˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«
|
||||
abc // No indentation
|
||||
abc // 1 tab
|
||||
abc // 2 tabs
|
||||
abc // Tab followed by space
|
||||
abc // Space followed by tab (3 spaces should be the result)
|
||||
abc // Mixed indentation (tab conversion depends on the column)
|
||||
abc // Already space indented
|
||||
|
||||
abc\tdef // Only the leading tab is manipulatedˇ»
|
||||
"});
|
||||
|
||||
// Test on just a few lines, the others should remain unchanged
|
||||
// Only lines (3, 5, 10, 11) should change
|
||||
cx.set_state(indoc! {"
|
||||
|
||||
abc // No indentation
|
||||
\tabcˇ // 1 tab
|
||||
\t\tabc // 2 tabs
|
||||
\t abcˇ // Tab followed by space
|
||||
\tabc // Space followed by tab (3 spaces should be the result)
|
||||
\t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
|
||||
abc // Already space indented
|
||||
«\t
|
||||
\tabc\tdef // Only the leading tab is manipulatedˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
abc // No indentation
|
||||
« abc // 1 tabˇ»
|
||||
\t\tabc // 2 tabs
|
||||
« abc // Tab followed by spaceˇ»
|
||||
\tabc // Space followed by tab (3 spaces should be the result)
|
||||
\t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
|
||||
abc // Already space indented
|
||||
«
|
||||
abc\tdef // Only the leading tab is manipulatedˇ»
|
||||
"});
|
||||
|
||||
// SINGLE SELECTION
|
||||
// Ln.1 "«" tests empty lines
|
||||
// Ln.9 tests just leading whitespace
|
||||
cx.set_state(indoc! {"
|
||||
«
|
||||
abc // No indentation
|
||||
\tabc // 1 tab
|
||||
\t\tabc // 2 tabs
|
||||
\t abc // Tab followed by space
|
||||
\tabc // Space followed by tab (3 spaces should be the result)
|
||||
\t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
|
||||
abc // Already space indented
|
||||
\t
|
||||
\tabc\tdef // Only the leading tab is manipulatedˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«
|
||||
abc // No indentation
|
||||
abc // 1 tab
|
||||
abc // 2 tabs
|
||||
abc // Tab followed by space
|
||||
abc // Space followed by tab (3 spaces should be the result)
|
||||
abc // Mixed indentation (tab conversion depends on the column)
|
||||
abc // Already space indented
|
||||
|
||||
abc\tdef // Only the leading tab is manipulatedˇ»
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(3)
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// MULTI SELECTION
|
||||
// Ln.1 "«" tests empty lines
|
||||
// Ln.11 tests just leading whitespace
|
||||
cx.set_state(indoc! {"
|
||||
«
|
||||
abˇ»ˇc // No indentation
|
||||
abc ˇ ˇ // 1 space (< 3 so dont convert)
|
||||
abc « // 2 spaces (< 3 so dont convert)
|
||||
abc // 3 spaces (convert)
|
||||
abc ˇ» // 5 spaces (1 tab + 2 spaces)
|
||||
«\tˇ»\t«\tˇ»abc // Already tab indented
|
||||
«\t abc // Tab followed by space
|
||||
\tabc // Space followed by tab (should be consumed due to tab)
|
||||
\t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
|
||||
\tˇ» «\t
|
||||
abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«
|
||||
abc // No indentation
|
||||
abc // 1 space (< 3 so dont convert)
|
||||
abc // 2 spaces (< 3 so dont convert)
|
||||
\tabc // 3 spaces (convert)
|
||||
\t abc // 5 spaces (1 tab + 2 spaces)
|
||||
\t\t\tabc // Already tab indented
|
||||
\t abc // Tab followed by space
|
||||
\tabc // Space followed by tab (should be consumed due to tab)
|
||||
\t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
|
||||
\t\t\t
|
||||
\tabc \t // Only the leading spaces should be convertedˇ»
|
||||
"});
|
||||
|
||||
// Test on just a few lines, the other should remain unchanged
|
||||
// Only lines (4, 8, 11, 12) should change
|
||||
cx.set_state(indoc! {"
|
||||
|
||||
abc // No indentation
|
||||
abc // 1 space (< 3 so dont convert)
|
||||
abc // 2 spaces (< 3 so dont convert)
|
||||
« abc // 3 spaces (convert)ˇ»
|
||||
abc // 5 spaces (1 tab + 2 spaces)
|
||||
\t\t\tabc // Already tab indented
|
||||
\t abc // Tab followed by space
|
||||
\tabc ˇ // Space followed by tab (should be consumed due to tab)
|
||||
\t\t \tabc // Mixed indentation
|
||||
\t \t \t \tabc // Mixed indentation
|
||||
\t \tˇ
|
||||
« abc \t // Only the leading spaces should be convertedˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
abc // No indentation
|
||||
abc // 1 space (< 3 so dont convert)
|
||||
abc // 2 spaces (< 3 so dont convert)
|
||||
«\tabc // 3 spaces (convert)ˇ»
|
||||
abc // 5 spaces (1 tab + 2 spaces)
|
||||
\t\t\tabc // Already tab indented
|
||||
\t abc // Tab followed by space
|
||||
«\tabc // Space followed by tab (should be consumed due to tab)ˇ»
|
||||
\t\t \tabc // Mixed indentation
|
||||
\t \t \t \tabc // Mixed indentation
|
||||
«\t\t\t
|
||||
\tabc \t // Only the leading spaces should be convertedˇ»
|
||||
"});
|
||||
|
||||
// SINGLE SELECTION
|
||||
// Ln.1 "«" tests empty lines
|
||||
// Ln.11 tests just leading whitespace
|
||||
cx.set_state(indoc! {"
|
||||
«
|
||||
abc // No indentation
|
||||
abc // 1 space (< 3 so dont convert)
|
||||
abc // 2 spaces (< 3 so dont convert)
|
||||
abc // 3 spaces (convert)
|
||||
abc // 5 spaces (1 tab + 2 spaces)
|
||||
\t\t\tabc // Already tab indented
|
||||
\t abc // Tab followed by space
|
||||
\tabc // Space followed by tab (should be consumed due to tab)
|
||||
\t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
|
||||
\t \t
|
||||
abc \t // Only the leading spaces should be convertedˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«
|
||||
abc // No indentation
|
||||
abc // 1 space (< 3 so dont convert)
|
||||
abc // 2 spaces (< 3 so dont convert)
|
||||
\tabc // 3 spaces (convert)
|
||||
\t abc // 5 spaces (1 tab + 2 spaces)
|
||||
\t\t\tabc // Already tab indented
|
||||
\t abc // Tab followed by space
|
||||
\tabc // Space followed by tab (should be consumed due to tab)
|
||||
\t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
|
||||
\t\t\t
|
||||
\tabc \t // Only the leading spaces should be convertedˇ»
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_case(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue