@@ -231,6 +231,8 @@ actions!(
SortLinesCaseInsensitive,
ReverseLines,
ShuffleLines,
+ ConvertToUpperCase,
+ ConvertToLowerCase,
Transpose,
Cut,
Copy,
@@ -353,6 +355,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::sort_lines_case_insensitive);
cx.add_action(Editor::reverse_lines);
cx.add_action(Editor::shuffle_lines);
+ cx.add_action(Editor::convert_to_upper_case);
+ cx.add_action(Editor::convert_to_lower_case);
cx.add_action(Editor::delete_to_previous_word_start);
cx.add_action(Editor::delete_to_previous_subword_start);
cx.add_action(Editor::delete_to_next_word_end);
@@ -4306,6 +4310,68 @@ impl Editor {
});
}
+ pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_uppercase())
+ }
+
+ pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_lowercase())
+ }
+
+ fn manipulate_text<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+ where
+ Fn: FnMut(&str) -> String,
+ {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = self.buffer.read(cx).snapshot(cx);
+
+ let mut new_selections = Vec::new();
+ let mut edits = Vec::new();
+ for selection in self.selections.all::<usize>(cx) {
+ let selection_is_empty = selection.is_empty();
+
+ let (start, end) = if selection_is_empty {
+ let word_range = movement::surrounding_word(
+ &display_map,
+ selection.start.to_display_point(&display_map),
+ );
+ let start = word_range.start.to_offset(&display_map, Bias::Left);
+ let end = word_range.end.to_offset(&display_map, Bias::Left);
+ (start, end)
+ } else {
+ (selection.start, selection.end)
+ };
+
+ let text = buffer.text_for_range(start..end).collect::<String>();
+ let text = callback(&text);
+
+ if selection_is_empty {
+ new_selections.push(selection);
+ } else {
+ new_selections.push(Selection {
+ start,
+ end: start + text.len(),
+ goal: SelectionGoal::None,
+ ..selection
+ });
+ }
+
+ edits.push((start..end, text));
+ }
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(new_selections);
+ });
+
+ this.request_autoscroll(Autoscroll::fit(), cx);
+ });
+ }
+
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
@@ -2695,6 +2695,65 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
"});
}
+#[gpui::test]
+async fn test_manipulate_text(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ // Test convert_to_upper_case()
+ cx.set_state(indoc! {"
+ «hello worldˇ»
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ «HELLO WORLDˇ»
+ "});
+
+ // Test convert_to_lower_case()
+ cx.set_state(indoc! {"
+ «HELLO WORLDˇ»
+ "});
+ cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
+ cx.assert_editor_state(indoc! {"
+ «hello worldˇ»
+ "});
+
+ // From here on out, test more complex cases of manipulate_text() with a single driver method: convert_to_upper_case()
+
+ // Test no selection case - should affect words cursors are in
+ // Cursor at beginning, middle, and end of word
+ cx.set_state(indoc! {"
+ ˇhello big beauˇtiful worldˇ
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ˇHELLO big BEAUˇTIFUL WORLDˇ
+ "});
+
+ // Test multiple selections on a single line and across multiple lines
+ cx.set_state(indoc! {"
+ «Theˇ» quick «brown
+ foxˇ» jumps «overˇ»
+ the «lazyˇ» dog
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ «THEˇ» quick «BROWN
+ FOXˇ» jumps «OVERˇ»
+ the «LAZYˇ» dog
+ "});
+
+ // Test case where text length grows
+ cx.set_state(indoc! {"
+ «tschüߡ»
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ «TSCHÜSSˇ»
+ "});
+}
+
#[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) {
init_test(cx, |_| {});