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
|
@ -270,6 +270,8 @@ actions!(
|
|||
ContextMenuLast,
|
||||
ContextMenuNext,
|
||||
ContextMenuPrevious,
|
||||
ConvertIndentationToSpaces,
|
||||
ConvertIndentationToTabs,
|
||||
ConvertToKebabCase,
|
||||
ConvertToLowerCamelCase,
|
||||
ConvertToLowerCase,
|
||||
|
|
|
@ -10080,7 +10080,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| lines.sort())
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
|
||||
}
|
||||
|
||||
pub fn sort_lines_case_insensitive(
|
||||
|
@ -10089,7 +10089,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
lines.sort_by_key(|line| line.to_lowercase())
|
||||
})
|
||||
}
|
||||
|
@ -10100,7 +10100,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
let mut seen = HashSet::default();
|
||||
lines.retain(|line| seen.insert(line.to_lowercase()));
|
||||
})
|
||||
|
@ -10112,7 +10112,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
let mut seen = HashSet::default();
|
||||
lines.retain(|line| seen.insert(*line));
|
||||
})
|
||||
|
@ -10555,20 +10555,20 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_lines(window, cx, |lines| lines.reverse())
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.reverse())
|
||||
}
|
||||
|
||||
pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
|
||||
}
|
||||
|
||||
fn manipulate_lines<Fn>(
|
||||
fn manipulate_lines<M>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
mut manipulate: M,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<&str>),
|
||||
M: FnMut(&str) -> LineManipulationResult,
|
||||
{
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
|
||||
|
@ -10601,18 +10601,18 @@ impl Editor {
|
|||
.text_for_range(start_point..end_point)
|
||||
.collect::<String>();
|
||||
|
||||
let mut lines = text.split('\n').collect_vec();
|
||||
let LineManipulationResult {
|
||||
new_text,
|
||||
line_count_before,
|
||||
line_count_after,
|
||||
} = manipulate(&text);
|
||||
|
||||
let lines_before = lines.len();
|
||||
callback(&mut lines);
|
||||
let lines_after = lines.len();
|
||||
|
||||
edits.push((start_point..end_point, lines.join("\n")));
|
||||
edits.push((start_point..end_point, new_text));
|
||||
|
||||
// Selections must change based on added and removed line count
|
||||
let start_row =
|
||||
MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32);
|
||||
let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32);
|
||||
let end_row = MultiBufferRow(start_row.0 + line_count_after.saturating_sub(1) as u32);
|
||||
new_selections.push(Selection {
|
||||
id: selection.id,
|
||||
start: start_row,
|
||||
|
@ -10621,10 +10621,10 @@ impl Editor {
|
|||
reversed: selection.reversed,
|
||||
});
|
||||
|
||||
if lines_after > lines_before {
|
||||
added_lines += lines_after - lines_before;
|
||||
} else if lines_before > lines_after {
|
||||
removed_lines += lines_before - lines_after;
|
||||
if line_count_after > line_count_before {
|
||||
added_lines += line_count_after - line_count_before;
|
||||
} else if line_count_before > line_count_after {
|
||||
removed_lines += line_count_before - line_count_after;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10669,6 +10669,171 @@ impl Editor {
|
|||
})
|
||||
}
|
||||
|
||||
fn manipulate_immutable_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<&str>),
|
||||
{
|
||||
self.manipulate_lines(window, cx, |text| {
|
||||
let mut lines: Vec<&str> = text.split('\n').collect();
|
||||
let line_count_before = lines.len();
|
||||
|
||||
callback(&mut lines);
|
||||
|
||||
LineManipulationResult {
|
||||
new_text: lines.join("\n"),
|
||||
line_count_before,
|
||||
line_count_after: lines.len(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn manipulate_mutable_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<Cow<'_, str>>),
|
||||
{
|
||||
self.manipulate_lines(window, cx, |text| {
|
||||
let mut lines: Vec<Cow<str>> = text.split('\n').map(Cow::from).collect();
|
||||
let line_count_before = lines.len();
|
||||
|
||||
callback(&mut lines);
|
||||
|
||||
LineManipulationResult {
|
||||
new_text: lines.join("\n"),
|
||||
line_count_before,
|
||||
line_count_after: lines.len(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_indentation_to_spaces(
|
||||
&mut self,
|
||||
_: &ConvertIndentationToSpaces,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
let tab_size = settings.tab_size.get() as usize;
|
||||
|
||||
self.manipulate_mutable_lines(window, cx, |lines| {
|
||||
// Allocates a reasonably sized scratch buffer once for the whole loop
|
||||
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
|
||||
// Avoids recomputing spaces that could be inserted many times
|
||||
let space_cache: Vec<Vec<char>> = (1..=tab_size)
|
||||
.map(|n| IndentSize::spaces(n as u32).chars().collect())
|
||||
.collect();
|
||||
|
||||
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
|
||||
let mut chars = line.as_ref().chars();
|
||||
let mut col = 0;
|
||||
let mut changed = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
' ' => {
|
||||
reindented_line.push(' ');
|
||||
col += 1;
|
||||
}
|
||||
'\t' => {
|
||||
// \t are converted to spaces depending on the current column
|
||||
let spaces_len = tab_size - (col % tab_size);
|
||||
reindented_line.extend(&space_cache[spaces_len - 1]);
|
||||
col += spaces_len;
|
||||
changed = true;
|
||||
}
|
||||
_ => {
|
||||
// If we dont append before break, the character is consumed
|
||||
reindented_line.push(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
reindented_line.clear();
|
||||
continue;
|
||||
}
|
||||
// Append the rest of the line and replace old reference with new one
|
||||
reindented_line.extend(chars);
|
||||
*line = Cow::Owned(reindented_line.clone());
|
||||
reindented_line.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_indentation_to_tabs(
|
||||
&mut self,
|
||||
_: &ConvertIndentationToTabs,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
let tab_size = settings.tab_size.get() as usize;
|
||||
|
||||
self.manipulate_mutable_lines(window, cx, |lines| {
|
||||
// Allocates a reasonably sized buffer once for the whole loop
|
||||
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
|
||||
// Avoids recomputing spaces that could be inserted many times
|
||||
let space_cache: Vec<Vec<char>> = (1..=tab_size)
|
||||
.map(|n| IndentSize::spaces(n as u32).chars().collect())
|
||||
.collect();
|
||||
|
||||
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
|
||||
let mut chars = line.chars();
|
||||
let mut spaces_count = 0;
|
||||
let mut first_non_indent_char = None;
|
||||
let mut changed = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
' ' => {
|
||||
// Keep track of spaces. Append \t when we reach tab_size
|
||||
spaces_count += 1;
|
||||
changed = true;
|
||||
if spaces_count == tab_size {
|
||||
reindented_line.push('\t');
|
||||
spaces_count = 0;
|
||||
}
|
||||
}
|
||||
'\t' => {
|
||||
reindented_line.push('\t');
|
||||
spaces_count = 0;
|
||||
}
|
||||
_ => {
|
||||
// Dont append it yet, we might have remaining spaces
|
||||
first_non_indent_char = Some(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
reindented_line.clear();
|
||||
continue;
|
||||
}
|
||||
// Remaining spaces that didn't make a full tab stop
|
||||
if spaces_count > 0 {
|
||||
reindented_line.extend(&space_cache[spaces_count - 1]);
|
||||
}
|
||||
// If we consume an extra character that was not indentation, add it back
|
||||
if let Some(extra_char) = first_non_indent_char {
|
||||
reindented_line.push(extra_char);
|
||||
}
|
||||
// Append the rest of the line and replace old reference with new one
|
||||
reindented_line.extend(chars);
|
||||
*line = Cow::Owned(reindented_line.clone());
|
||||
reindented_line.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_to_upper_case(
|
||||
&mut self,
|
||||
_: &ConvertToUpperCase,
|
||||
|
@ -22941,6 +23106,12 @@ pub struct LineHighlight {
|
|||
pub type_id: Option<TypeId>,
|
||||
}
|
||||
|
||||
struct LineManipulationResult {
|
||||
pub new_text: String,
|
||||
pub line_count_before: usize,
|
||||
pub line_count_after: usize,
|
||||
}
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
row: u32,
|
||||
status: &DiffHunkStatus,
|
||||
|
|
|
@ -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, |_| {});
|
||||
|
|
|
@ -230,6 +230,8 @@ impl EditorElement {
|
|||
register_action(editor, window, Editor::reverse_lines);
|
||||
register_action(editor, window, Editor::shuffle_lines);
|
||||
register_action(editor, window, Editor::toggle_case);
|
||||
register_action(editor, window, Editor::convert_indentation_to_spaces);
|
||||
register_action(editor, window, Editor::convert_indentation_to_tabs);
|
||||
register_action(editor, window, Editor::convert_to_upper_case);
|
||||
register_action(editor, window, Editor::convert_to_lower_case);
|
||||
register_action(editor, window, Editor::convert_to_title_case);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue