Fix title/camelCase commands stripping leading indentation Fixes (#50523)

Fixes: #48945

Description:

The convert:to-title-case, convert:to-upper-camel-case, and
convert:to-lower-camel-case editor commands were stripping leading
whitespace from each line of a multi-line selection.

Root cause: The conversion functions split on whitespace using
.split_whitespace() and then joined the resulting words, discarding any
leading spaces/tabs before the first word on each line.

Fix: Each line now preserves its leading whitespace by capturing and
re-prepending it before applying the case conversion.

Tests: Added test cases covering multi-line selections with indentation
for all three commands.

Video : 


[bug1fix.webm](https://github.com/user-attachments/assets/f4d25c55-bc6d-44e6-a989-7d9b4bc59ac9)

Release Notes:

- Fixed trailing whitespace handling on text case changes
This commit is contained in:
Om Chillure authored and GitHub committed 2026-03-12 19:33:48 +00:00
1 parent 47b253fdaa
commit 06b9155491
2 files changed
+97 -10

No files matched your search

+26 -10
View File
@@ -12438,9 +12438,7 @@ impl Editor {
cx: &mut Context<Self>,
) {
self.manipulate_text(window, cx, |text| {
text.split('\n')
.map(|line| line.to_case(Case::Title))
.join("\n")
Self::convert_text_case(text, Case::Title)
})
}
@@ -12450,7 +12448,9 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_text(window, cx, |text| text.to_case(Case::Snake))
self.manipulate_text(window, cx, |text| {
Self::convert_text_case(text, Case::Snake)
})
}
pub fn convert_to_kebab_case(
@@ -12459,7 +12459,9 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_text(window, cx, |text| text.to_case(Case::Kebab))
self.manipulate_text(window, cx, |text| {
Self::convert_text_case(text, Case::Kebab)
})
}
pub fn convert_to_upper_camel_case(
@@ -12469,9 +12471,7 @@ impl Editor {
cx: &mut Context<Self>,
) {
self.manipulate_text(window, cx, |text| {
text.split('\n')
.map(|line| line.to_case(Case::UpperCamel))
.join("\n")
Self::convert_text_case(text, Case::UpperCamel)
})
}
@@ -12481,7 +12481,9 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_text(window, cx, |text| text.to_case(Case::Camel))
self.manipulate_text(window, cx, |text| {
Self::convert_text_case(text, Case::Camel)
})
}
pub fn convert_to_opposite_case(
@@ -12509,7 +12511,9 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_text(window, cx, |text| text.to_case(Case::Sentence))
self.manipulate_text(window, cx, |text| {
Self::convert_text_case(text, Case::Sentence)
})
}
pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context<Self>) {
@@ -12540,6 +12544,18 @@ impl Editor {
})
}
fn convert_text_case(text: &str, case: Case) -> String {
text.lines()
.map(|line| {
let trimmed_start = line.trim_start();
let leading = &line[..line.len() - trimmed_start.len()];
let trimmed = trimmed_start.trim_end();
let trailing = &trimmed_start[trimmed.len()..];
format!("{}{}{}", leading, trimmed.to_case(case), trailing)
})
.join("\n")
}
pub fn convert_to_rot47(
&mut self,
_: &ConvertToRot47,
+71
View File
@@ -6268,6 +6268,77 @@ async fn test_manipulate_text(cx: &mut TestAppContext) {
«HeLlO, wOrLD!ˇ»
"});
// Test that case conversions backed by `to_case` preserve leading/trailing whitespace.
cx.set_state(indoc! {"
« hello worldˇ»
"});
cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
cx.assert_editor_state(indoc! {"
« Hello Worldˇ»
"});
cx.set_state(indoc! {"
« hello worldˇ»
"});
cx.update_editor(|e, window, cx| {
e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
});
cx.assert_editor_state(indoc! {"
« HelloWorldˇ»
"});
cx.set_state(indoc! {"
« hello worldˇ»
"});
cx.update_editor(|e, window, cx| {
e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
});
cx.assert_editor_state(indoc! {"
« helloWorldˇ»
"});
cx.set_state(indoc! {"
« hello worldˇ»
"});
cx.update_editor(|e, window, cx| e.convert_to_snake_case(&ConvertToSnakeCase, window, cx));
cx.assert_editor_state(indoc! {"
« hello_worldˇ»
"});
cx.set_state(indoc! {"
« hello worldˇ»
"});
cx.update_editor(|e, window, cx| e.convert_to_kebab_case(&ConvertToKebabCase, window, cx));
cx.assert_editor_state(indoc! {"
« hello-worldˇ»
"});
cx.set_state(indoc! {"
« hello worldˇ»
"});
cx.update_editor(|e, window, cx| {
e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
});
cx.assert_editor_state(indoc! {"
« Hello worldˇ»
"});
cx.set_state(indoc! {"
« hello world\t\tˇ»
"});
cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
cx.assert_editor_state(indoc! {"
« Hello World\t\tˇ»
"});
cx.set_state(indoc! {"
« hello world\t\tˇ»
"});
cx.update_editor(|e, window, cx| e.convert_to_snake_case(&ConvertToSnakeCase, window, cx));
cx.assert_editor_state(indoc! {"
« hello_world\t\tˇ»
"});
// Test selections with `line_mode() = true`.
cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
cx.set_state(indoc! {"