diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 592c043c..77e5f6bc 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -26,7 +26,7 @@ serde = "1.0.203" serde_json = "1" smallvec = "1.13.2" smol = "1" -unicode-segmentation = "1.11.0" +unicode-segmentation = "1.12.0" usvg = { version = "0.44.0", default-features = false, features = [ "system-fonts", "text", diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 5a1299c3..11211cbd 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -42,6 +42,8 @@ actions!( Delete, DeleteToBeginningOfLine, DeleteToEndOfLine, + DeleteToPreviousWordStart, + DeleteToNextWordEnd, Enter, Up, Down, @@ -58,6 +60,8 @@ actions!( SelectToEndOfLine, SelectToStart, SelectToEnd, + SelectToPreviousWordStart, + SelectToNextWordEnd, ShowCharacterPalette, Copy, Cut, @@ -68,6 +72,8 @@ actions!( MoveToEndOfLine, MoveToStart, MoveToEnd, + MoveToPreviousWord, + MoveToNextWord, TextChanged, ] ); @@ -90,6 +96,14 @@ pub fn init(cx: &mut App) { KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)), + #[cfg(not(target_os = "macos"))] + KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)), + #[cfg(not(target_os = "macos"))] + KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)), KeyBinding::new("enter", Enter, Some(CONTEXT)), KeyBinding::new("up", Up, Some(CONTEXT)), KeyBinding::new("down", Down, Some(CONTEXT)), @@ -112,6 +126,14 @@ pub fn init(cx: &mut App) { #[cfg(target_os = "macos")] KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] + KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)), + #[cfg(not(target_os = "macos"))] + KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)), + #[cfg(not(target_os = "macos"))] + KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)), + #[cfg(target_os = "macos")] KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)), @@ -146,6 +168,14 @@ pub fn init(cx: &mut App) { #[cfg(target_os = "macos")] KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)), #[cfg(target_os = "macos")] + KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)), + #[cfg(not(target_os = "macos"))] + KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)), + #[cfg(not(target_os = "macos"))] + KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)), + #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)), @@ -659,6 +689,26 @@ impl TextInput { self.move_to(end, window, cx); } + fn move_to_previous_word( + &mut self, + _: &MoveToPreviousWord, + window: &mut Window, + cx: &mut Context, + ) { + let offset = self.previous_start_of_word(); + self.move_to(offset, window, cx); + } + + fn move_to_next_word( + &mut self, + _: &MoveToNextWord, + window: &mut Window, + cx: &mut Context, + ) { + let offset = self.next_end_of_word(); + self.move_to(offset, window, cx); + } + fn select_to_start(&mut self, _: &SelectToStart, window: &mut Window, cx: &mut Context) { self.select_to(0, window, cx); } @@ -688,6 +738,47 @@ impl TextInput { self.select_to(self.next_boundary(offset), window, cx); } + fn select_to_previous_word( + &mut self, + _: &SelectToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + let offset = self.previous_start_of_word(); + self.select_to(offset, window, cx); + } + + fn select_to_next_word( + &mut self, + _: &SelectToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + let offset = self.next_end_of_word(); + self.select_to(offset, window, cx); + } + + /// Return the start offset of the previous word. + fn previous_start_of_word(&mut self) -> usize { + let offset = self.selected_range.start; + let prev_str = &self.text[..offset].to_string(); + UnicodeSegmentation::split_word_bound_indices(prev_str as &str) + .filter(|(_, s)| !s.trim_start().is_empty()) + .next_back() + .map(|(i, _)| i) + .unwrap_or(0) + } + + /// Return the next end offset of the next word. + fn next_end_of_word(&mut self) -> usize { + let offset = self.cursor_offset(); + let next_str = &self.text[offset..].to_string(); + UnicodeSegmentation::split_word_bound_indices(next_str as &str) + .find(|(_, s)| !s.trim_start().is_empty()) + .map(|(i, s)| offset + i + s.len()) + .unwrap_or(self.text.len()) + } + /// Get start of line fn start_of_line(&mut self, window: &mut Window, cx: &mut Context) -> usize { if self.is_single_line() { @@ -787,6 +878,38 @@ impl TextInput { self.pause_blink_cursor(cx); } + fn delete_previous_word( + &mut self, + _: &DeleteToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + let offset = self.previous_start_of_word(); + self.replace_text_in_range( + Some(self.range_to_utf16(&(offset..self.cursor_offset()))), + "", + window, + cx, + ); + self.pause_blink_cursor(cx); + } + + fn delete_next_word( + &mut self, + _: &DeleteToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + let offset = self.next_end_of_word(); + self.replace_text_in_range( + Some(self.range_to_utf16(&(self.cursor_offset()..offset))), + "", + window, + cx, + ); + self.pause_blink_cursor(cx); + } + fn enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context) { if self.is_multi_line() { let is_eof = self.selected_range.end == self.text.len(); @@ -1533,6 +1656,8 @@ impl Render for TextInput { .on_action(cx.listener(Self::delete)) .on_action(cx.listener(Self::delete_to_beginning_of_line)) .on_action(cx.listener(Self::delete_to_end_of_line)) + .on_action(cx.listener(Self::delete_previous_word)) + .on_action(cx.listener(Self::delete_next_word)) .on_action(cx.listener(Self::enter)) }) .on_action(cx.listener(Self::left)) @@ -1548,10 +1673,14 @@ impl Render for TextInput { .on_action(cx.listener(Self::select_all)) .on_action(cx.listener(Self::select_to_start_of_line)) .on_action(cx.listener(Self::select_to_end_of_line)) + .on_action(cx.listener(Self::select_to_previous_word)) + .on_action(cx.listener(Self::select_to_next_word)) .on_action(cx.listener(Self::home)) .on_action(cx.listener(Self::end)) .on_action(cx.listener(Self::move_to_start)) .on_action(cx.listener(Self::move_to_end)) + .on_action(cx.listener(Self::move_to_previous_word)) + .on_action(cx.listener(Self::move_to_next_word)) .on_action(cx.listener(Self::select_to_start)) .on_action(cx.listener(Self::select_to_end)) .on_action(cx.listener(Self::show_character_palette))