Detailed changes
@@ -1655,6 +1655,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
[[package]]
name = "copilot"
version = "0.1.0"
@@ -2145,7 +2154,7 @@ version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
- "convert_case",
+ "convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version 0.4.0",
@@ -2333,6 +2342,7 @@ dependencies = [
"clock",
"collections",
"context_menu",
+ "convert_case 0.6.0",
"copilot",
"ctor",
"db",
@@ -47,6 +47,7 @@ workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow.workspace = true
+convert_case = "0.6.0"
futures.workspace = true
indoc = "1.0.4"
itertools = "0.10"
@@ -56,12 +57,12 @@ ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
+rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
-rand.workspace = true
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true }
@@ -28,6 +28,7 @@ use blink_manager::BlinkManager;
use client::{ClickhouseEvent, TelemetrySettings};
use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use convert_case::{Case, Casing};
use copilot::Copilot;
pub use display_map::DisplayPoint;
use display_map::*;
@@ -231,6 +232,13 @@ actions!(
SortLinesCaseInsensitive,
ReverseLines,
ShuffleLines,
+ ConvertToUpperCase,
+ ConvertToLowerCase,
+ ConvertToTitleCase,
+ ConvertToSnakeCase,
+ ConvertToKebabCase,
+ ConvertToUpperCamelCase,
+ ConvertToLowerCamelCase,
Transpose,
Cut,
Copy,
@@ -353,6 +361,13 @@ 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::convert_to_title_case);
+ cx.add_action(Editor::convert_to_snake_case);
+ cx.add_action(Editor::convert_to_kebab_case);
+ cx.add_action(Editor::convert_to_upper_camel_case);
+ cx.add_action(Editor::convert_to_lower_camel_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 +4321,97 @@ 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())
+ }
+
+ pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_case(Case::Title))
+ }
+
+ pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_case(Case::Snake))
+ }
+
+ pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_case(Case::Kebab))
+ }
+
+ pub fn convert_to_upper_camel_case(
+ &mut self,
+ _: &ConvertToUpperCamelCase,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel))
+ }
+
+ pub fn convert_to_lower_camel_case(
+ &mut self,
+ _: &ConvertToLowerCamelCase,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_text(cx, |text| text.to_case(Case::Camel))
+ }
+
+ 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();
+ let mut selection_adjustment = 0i32;
+
+ 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 old_length = text.len() as i32;
+ let text = callback(&text);
+
+ new_selections.push(Selection {
+ start: (start as i32 - selection_adjustment) as usize,
+ end: ((start + text.len()) as i32 - selection_adjustment) as usize,
+ goal: SelectionGoal::None,
+ ..selection
+ });
+
+ selection_adjustment += old_length - text.len() as i32;
+
+ 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;
@@ -2698,6 +2698,84 @@ 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()
+
+ // 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 «BEAUTIFULˇ» «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ˇ»
+ "});
+
+ // Test to make sure we don't crash when text shrinks
+ cx.set_state(indoc! {"
+ aaa_bbbˇ
+ "});
+ cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+ cx.assert_editor_state(indoc! {"
+ «aaaBbbˇ»
+ "});
+
+ // Test to make sure we all aware of the fact that each word can grow and shrink
+ // Final selections should be aware of this fact
+ cx.set_state(indoc! {"
+ aaa_bˇbb bbˇb_ccc ˇccc_ddd
+ "});
+ cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+ cx.assert_editor_state(indoc! {"
+ «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
+ "});
+}
+
#[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -4088,6 +4088,7 @@ impl AnyWindowHandle {
self.update(cx, |cx| cx.remove_window())
}
+ #[cfg(any(test, feature = "test-support"))]
pub fn simulate_activation(&self, cx: &mut TestAppContext) {
self.update(cx, |cx| {
let other_window_ids = cx
@@ -4105,6 +4106,7 @@ impl AnyWindowHandle {
});
}
+ #[cfg(any(test, feature = "test-support"))]
pub fn simulate_deactivation(&self, cx: &mut TestAppContext) {
self.update(cx, |cx| {
cx.window_changed_active_status(self.window_id, false);
@@ -182,8 +182,8 @@ impl CachedLspAdapter {
self.adapter.workspace_configuration(cx)
}
- pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
- self.adapter.process_diagnostics(params).await
+ pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
+ self.adapter.process_diagnostics(params)
}
pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
@@ -262,7 +262,7 @@ pub trait LspAdapter: 'static + Send + Sync {
container_dir: PathBuf,
) -> Option<LanguageServerBinary>;
- async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+ fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
@@ -1487,12 +1487,6 @@ impl Language {
None
}
- pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
- for adapter in &self.adapters {
- adapter.process_diagnostics(diagnostics).await;
- }
- }
-
pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
for adapter in &self.adapters {
adapter.process_completion(completion).await;
@@ -1756,7 +1750,7 @@ impl LspAdapter for Arc<FakeLspAdapter> {
unreachable!();
}
- async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+ fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
self.disk_based_diagnostics_sources.clone()
@@ -2769,24 +2769,21 @@ impl Project {
language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({
let adapter = adapter.clone();
- move |mut params, cx| {
+ move |mut params, mut cx| {
let this = this;
let adapter = adapter.clone();
- cx.spawn(|mut cx| async move {
- adapter.process_diagnostics(&mut params).await;
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| {
- this.update_diagnostics(
- server_id,
- params,
- &adapter.disk_based_diagnostic_sources,
- cx,
- )
- .log_err();
- });
- }
- })
- .detach();
+ adapter.process_diagnostics(&mut params);
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| {
+ this.update_diagnostics(
+ server_id,
+ params,
+ &adapter.disk_based_diagnostic_sources,
+ cx,
+ )
+ .log_err();
+ });
+ }
}
})
.detach();
@@ -294,7 +294,7 @@ mod tests {
}
#[test]
- fn test_path_suffix() {
+ fn test_icon_suffix() {
// No dots in name
let path = Path::new("/a/b/c/file_name.rs");
assert_eq!(path.icon_suffix(), Some("rs"));
@@ -1,5 +1,6 @@
name = "Shell Script"
-path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"]
+path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin"]
+line_comment = "# "
first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
brackets = [
{ start = "[", end = "]", close = true, newline = false },
@@ -102,7 +102,7 @@ impl LspAdapter for RustLspAdapter {
Some("rust-analyzer/flycheck".into())
}
- async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
+ fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
lazy_static! {
static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
}
@@ -310,7 +310,7 @@ mod tests {
},
],
};
- RustLspAdapter.process_diagnostics(&mut params).await;
+ RustLspAdapter.process_diagnostics(&mut params);
assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
@@ -1,5 +1,5 @@
name = "TOML"
-path_suffixes = ["toml"]
+path_suffixes = ["Cargo.lock", "toml"]
line_comment = "# "
autoclose_before = ",]}"
brackets = [