Detailed changes
@@ -9132,7 +9132,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(
@@ -9141,7 +9141,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())
})
}
@@ -9152,7 +9152,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()));
})
@@ -9164,7 +9164,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));
})
@@ -9606,20 +9606,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);
@@ -9652,18 +9652,14 @@ 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,
@@ -9672,10 +9668,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;
}
}
@@ -9720,6 +9716,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,
@@ -21157,6 +21318,13 @@ 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,
@@ -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);