From 456a390166de9cc159e78bc190769b7eea16ef78 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 19 Jul 2022 14:51:22 +0200 Subject: [PATCH 01/47] Add character palette menu item --- assets/keymaps/default.json | 3 ++- crates/editor/src/editor.rs | 6 ++++++ crates/gpui/src/app.rs | 9 +++++++++ crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/event.rs | 18 ++++++++++-------- crates/gpui/src/platform/mac/window.rs | 9 +++++++++ crates/gpui/src/platform/test.rs | 2 ++ crates/zed/src/menus.rs | 4 ++++ 8 files changed, 43 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 11893847ade8d51398974e557bb7052927d004b1..a8b907663398aae270655b18d3341a404f5f616a 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -113,7 +113,8 @@ } ], "pageup": "editor::PageUp", - "pagedown": "editor::PageDown" + "pagedown": "editor::PageDown", + "ctrl-cmd-space": "editor::ShowCharacterPalette" } }, { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 846c05a412d53256667a92d146d4211cf84c3127..ac7b8a18668e7d02a18cb5f54d86b0bf2e456492 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -189,6 +189,7 @@ actions!( Tab, TabPrev, ToggleComments, + ShowCharacterPalette, SelectLargerSyntaxNode, SelectSmallerSyntaxNode, GoToDefinition, @@ -313,6 +314,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::open_excerpts); cx.add_action(Editor::jump); cx.add_action(Editor::restart_language_server); + cx.add_action(Editor::show_character_palette); cx.add_async_action(Editor::confirm_completion); cx.add_async_action(Editor::confirm_code_action); cx.add_async_action(Editor::rename); @@ -5014,6 +5016,10 @@ impl Editor { } } + fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { + cx.show_character_palette(); + } + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 075339bba1bf54ae9ce918a6f80769300d1b5bba..c85dda16ca2b017b3190db9d60475c86b7d894b4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1195,6 +1195,11 @@ impl MutableAppContext { .set_menus(menus, &self.keystroke_matcher); } + fn show_character_palette(&self, window_id: usize) { + let (_, window) = &self.presenters_and_platform_windows[&window_id]; + window.show_character_palette(); + } + fn prompt( &self, window_id: usize, @@ -3489,6 +3494,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.platform() } + pub fn show_character_palette(&self) { + self.app.show_character_palette(self.window_id); + } + pub fn prompt( &self, level: PromptLevel, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index cf508a5634a4341ee65573f6613fb93087d2bb6d..44348a34c2826c40ab467fc0465bd6a428935021 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -99,6 +99,7 @@ pub trait Window: WindowContext { fn activate(&self); fn set_title(&mut self, title: &str); fn set_edited(&mut self, edited: bool); + fn show_character_palette(&self); } pub trait WindowContext { diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 5e23859675cbc173254428f376f832207a2b3c00..b6cc066b2e255a84cc08ca7d79f6bffc16ebc23e 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -12,10 +12,19 @@ use cocoa::{ }; use std::{borrow::Cow, ffi::CStr, os::raw::c_char}; +const BACKSPACE_KEY: u16 = 0x7f; +const SPACE_KEY: u16 = b' ' as u16; +const ENTER_KEY: u16 = 0x0d; +const NUMPAD_ENTER_KEY: u16 = 0x03; +const ESCAPE_KEY: u16 = 0x1b; +const TAB_KEY: u16 = 0x09; +const SHIFT_TAB_KEY: u16 = 0x19; + pub fn key_to_native(key: &str) -> Cow { use cocoa::appkit::*; let code = match key { - "backspace" => 0x7F, + "space" => SPACE_KEY, + "backspace" => BACKSPACE_KEY, "up" => NSUpArrowFunctionKey, "down" => NSDownArrowFunctionKey, "left" => NSLeftArrowFunctionKey, @@ -243,13 +252,6 @@ unsafe fn get_key_text( let mut input = None; let first_char = unmodified_chars.chars().next()?; use cocoa::appkit::*; - const BACKSPACE_KEY: u16 = 0x7f; - const ENTER_KEY: u16 = 0x0d; - const NUMPAD_ENTER_KEY: u16 = 0x03; - const ESCAPE_KEY: u16 = 0x1b; - const TAB_KEY: u16 = 0x09; - const SHIFT_TAB_KEY: u16 = 0x19; - const SPACE_KEY: u16 = b' ' as u16; #[allow(non_upper_case_globals)] let unmodified_chars = match first_char as u16 { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 1b3cde95384116841c8291e0bde81f7e7083ffc9..23f4dcfa60e3a5b3c2e6f21c75983c9cb738cc18 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1,3 +1,4 @@ +use super::{geometry::RectFExt, renderer::Renderer}; use crate::{ executor, geometry::{ @@ -448,6 +449,14 @@ impl platform::Window for Window { // so we have to move it again. self.0.borrow().move_traffic_light(); } + + fn show_character_palette(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let window = self.0.borrow().native_window; + let _: () = msg_send![app, orderFrontCharacterPalette: window]; + } + } } impl platform::WindowContext for Window { diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index a58bd603f277a93575ec3fe534538c670f03d9a3..e1f39d046dd7725c7a0f2e29947bf2094fe2a640 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -274,6 +274,8 @@ impl super::Window for Window { fn on_should_close(&mut self, callback: Box bool>) { self.should_close_handler = Some(callback); } + + fn show_character_palette(&self) {} } pub fn platform() -> Platform { diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index d4d9b401e652218858e451aea2d29258a875f585..5aa53d7526349b168c18e4404b80c13a26bfbf4f 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -131,6 +131,10 @@ pub fn menus() -> Vec> { name: "Toggle Line Comment", action: Box::new(editor::ToggleComments), }, + MenuItem::Action { + name: "Emoji & Symbols", + action: Box::new(editor::ShowCharacterPalette), + }, ], }, Menu { From 7757fbe241e90525c0e618bbf0989d9d23aaa555 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 19 Jul 2022 14:52:02 +0200 Subject: [PATCH 02/47] Implement `NSTextInputClient` protocol on window using no-ops --- crates/gpui/src/platform/mac/window.rs | 80 +++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 23f4dcfa60e3a5b3c2e6f21c75983c9cb738cc18..8e7510e6aa88bc2951f1695d333cb46661c7a168 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -16,7 +16,7 @@ use cocoa::{ NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask, }, base::{id, nil}, - foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString}, + foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger}, quartzcore::AutoresizingMask, }; use core_graphics::display::CGRect; @@ -42,13 +42,29 @@ use std::{ time::Duration, }; -use super::{geometry::RectFExt, renderer::Renderer}; - const WINDOW_STATE_IVAR: &'static str = "windowState"; static mut WINDOW_CLASS: *const Class = ptr::null(); static mut VIEW_CLASS: *const Class = ptr::null(); +#[repr(C)] +#[derive(Copy, Clone)] +struct NSRange { + pub location: NSUInteger, + pub length: NSUInteger, +} + +unsafe impl objc::Encode for NSRange { + fn encode() -> objc::Encoding { + let encoding = format!( + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str() + ); + unsafe { objc::Encoding::from_str(&encoding) } + } +} + #[allow(non_upper_case_globals)] const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; @@ -164,6 +180,37 @@ unsafe fn build_classes() { display_layer as extern "C" fn(&Object, Sel, id), ); + decl.add_protocol(Protocol::get("NSTextInputClient").unwrap()); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), + ); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range + as extern "C" fn(&Object, Sel, NSRange, id) -> id, + ); + decl.register() }; } @@ -875,6 +922,33 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) { } } +extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { + unsafe { msg_send![class!(NSArray), array] } +} + +extern "C" fn has_marked_text(_: &Object, _: Sel) -> BOOL { + false as BOOL +} + +extern "C" fn selected_range(_: &Object, _: Sel) -> NSRange { + NSRange { + location: 0, + length: 0, + } +} + +extern "C" fn first_rect_for_character_range(_: &Object, _: Sel, _: NSRange, _: id) -> NSRect { + NSRect::new(NSPoint::new(0., 0.), NSSize::new(20., 20.)) +} + +extern "C" fn insert_text(_: &Object, _: Sel, _: id, _: NSRange) {} + +extern "C" fn set_marked_text(_: &Object, _: Sel, _: id, _: NSRange, _: NSRange) {} + +extern "C" fn attributed_substring_for_proposed_range(_: &Object, _: Sel, _: NSRange, _: id) -> id { + unsafe { msg_send![class!(NSAttributedString), alloc] } +} + async fn synthetic_drag( window_state: Weak>, drag_id: usize, From 2ea0b89e7cfaa367e3f1c66f20f034b01f6b009c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 20 Jul 2022 15:07:09 +0200 Subject: [PATCH 03/47] WIP --- crates/gpui/src/platform.rs | 19 ++ crates/gpui/src/platform/mac/window.rs | 313 ++++++++++++++++++++++--- crates/gpui/src/platform/test.rs | 2 + 3 files changed, 303 insertions(+), 31 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 44348a34c2826c40ab467fc0465bd6a428935021..3797843144c03dce3e247d67be0489cf3b3948ed 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -26,6 +26,7 @@ use serde::Deserialize; use std::{ any::Any, fmt::{self, Display}, + ops::Range, path::{Path, PathBuf}, rc::Rc, str::FromStr, @@ -88,6 +89,23 @@ pub trait Dispatcher: Send + Sync { fn run_on_main_thread(&self, task: Runnable); } +pub trait InputHandler { + fn select(&mut self, range: Range); + fn selected_range(&self) -> Option>; + fn set_composition( + &mut self, + marked_text: &str, + new_selected_range: Option>, + replacement_range: Option>, + ); + fn commit(&mut self, text: &str, replacement_range: Option>); + fn cancel_composition(&mut self); + fn finish_composition(&mut self); + fn unmark(&mut self); + fn marked_range(&self) -> Option>; + fn text_for_range(&self, range: Range) -> Option; +} + pub trait Window: WindowContext { fn as_any_mut(&mut self) -> &mut dyn Any; fn on_event(&mut self, callback: Box bool>); @@ -95,6 +113,7 @@ pub trait Window: WindowContext { fn on_resize(&mut self, callback: Box); fn on_should_close(&mut self, callback: Box bool>); fn on_close(&mut self, callback: Box); + fn set_input_handler(&mut self, input_handler: Box); fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); fn set_title(&mut self, title: &str); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 8e7510e6aa88bc2951f1695d333cb46661c7a168..6b827a771194e042910d0501a52561cce78bd80d 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -7,7 +7,8 @@ use crate::{ }, keymap::Keystroke, platform::{self, Event, WindowBounds, WindowContext}, - KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, Scene, + InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, + Scene, }; use block::ConcreteBlock; use cocoa::{ @@ -16,7 +17,9 @@ use cocoa::{ NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask, }, base::{id, nil}, - foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger}, + foundation::{ + NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger, + }, quartzcore::AutoresizingMask, }; use core_graphics::display::CGRect; @@ -34,9 +37,13 @@ use smol::Timer; use std::{ any::Any, cell::{Cell, RefCell}, + cmp, convert::TryInto, - ffi::c_void, - mem, ptr, + ffi::{c_void, CStr}, + mem, + ops::Range, + os::raw::c_char, + ptr, rc::{Rc, Weak}, sync::Arc, time::Duration, @@ -48,12 +55,44 @@ static mut WINDOW_CLASS: *const Class = ptr::null(); static mut VIEW_CLASS: *const Class = ptr::null(); #[repr(C)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] struct NSRange { pub location: NSUInteger, pub length: NSUInteger, } +impl NSRange { + fn invalid() -> Self { + Self { + location: NSNotFound as NSUInteger, + length: 0, + } + } + + fn is_valid(&self) -> bool { + self.location != NSNotFound as NSUInteger + } + + fn to_range(&self) -> Option> { + if self.is_valid() { + let start = self.location as usize; + let end = start + self.length as usize; + Some(start..end) + } else { + None + } + } +} + +impl From> for NSRange { + fn from(range: Range) -> Self { + NSRange { + location: range.start as NSUInteger, + length: range.len() as NSUInteger, + } + } +} + unsafe impl objc::Encode for NSRange { fn encode() -> objc::Encoding { let encoding = format!( @@ -189,6 +228,10 @@ unsafe fn build_classes() { sel!(hasMarkedText), has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, ); + decl.add_method( + sel!(markedRange), + marked_range as extern "C" fn(&Object, Sel) -> NSRange, + ); decl.add_method( sel!(selectedRange), selected_range as extern "C" fn(&Object, Sel) -> NSRange, @@ -205,10 +248,11 @@ unsafe fn build_classes() { sel!(setMarkedText:selectedRange:replacementRange:), set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); decl.add_method( sel!(attributedSubstringForProposedRange:actualRange:), attributed_substring_for_proposed_range - as extern "C" fn(&Object, Sel, NSRange, id) -> id, + as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, ); decl.register() @@ -225,6 +269,8 @@ struct WindowState { resize_callback: Option>, should_close_callback: Option bool>>, close_callback: Option>, + input_handler: Option>, + pending_key_event: Option, synthetic_drag_counter: usize, executor: Rc, scene_to_render: Option, @@ -236,6 +282,13 @@ struct WindowState { previous_modifiers_changed_event: Option, } +#[derive(Default, Debug)] +struct PendingKeyEvent { + set_marked_text: Option<(String, Option>, Option>)>, + unmark_text: bool, + insert_text: Option, +} + impl Window { pub fn open( id: usize, @@ -311,6 +364,8 @@ impl Window { should_close_callback: None, close_callback: None, activate_callback: None, + input_handler: None, + pending_key_event: None, synthetic_drag_counter: 0, executor, scene_to_render: Default::default(), @@ -419,6 +474,10 @@ impl platform::Window for Window { self.0.as_ref().borrow_mut().activate_callback = Some(callback); } + fn set_input_handler(&mut self, input_handler: Box) { + self.0.as_ref().borrow_mut().input_handler = Some(input_handler); + } + fn prompt( &self, level: platform::PromptLevel, @@ -636,38 +695,103 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) { } extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { + let had_marked_text = with_input_handler(this, |input_handler| input_handler.marked_range()) + .flatten() + .is_some(); let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; if let Some(event) = event { - match &event { - Event::KeyDown(KeyDownEvent { - keystroke, - input, - is_held, - }) => { - let keydown = (keystroke.clone(), input.clone()); + let mut event = match event { + Event::KeyDown(event) => { + let keydown = (event.keystroke.clone(), event.input.clone()); // Ignore events from held-down keys after some of the initially-pressed keys // were released. - if *is_held { + if event.is_held { if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) { return YES; } } else { window_state_borrow.last_fresh_keydown = Some(keydown); } + + event } _ => return NO, + }; + + // TODO: handle "live conversion" + window_state_borrow.pending_key_event = Some(Default::default()); + drop(window_state_borrow); + + // TODO: + // Since Mac Eisu Kana keys cannot be handled by interpretKeyEvents to enable/ + // disable an IME, we need to pass the event to processInputKeyBindings. + // processInputKeyBindings is available at least on 10.11-11.0. + // if (keyCode == kVK_JIS_Eisu || keyCode == kVK_JIS_Kana) { + // if ([NSTextInputContext + // respondsToSelector:@selector(processInputKeyBindings:)]) { + // [NSTextInputContext performSelector:@selector(processInputKeyBindings:) + // withObject:theEvent]; + // } + // } else { + unsafe { + let input_context: id = msg_send![this, inputContext]; + let _: BOOL = msg_send![input_context, handleEvent: native_event]; + } + // } + + let pending_event = window_state.borrow_mut().pending_key_event.take().unwrap(); + let mut inserted_text = false; + let has_marked_text = pending_event.set_marked_text.is_some(); + if let Some(text) = pending_event.insert_text.as_ref() { + if !text.is_empty() && (had_marked_text || has_marked_text || text.len() > 1) { + with_input_handler(this, |input_handler| input_handler.commit(&text, None)); + inserted_text = true; + } } - if let Some(mut callback) = window_state_borrow.event_callback.take() { - drop(window_state_borrow); - let handled = callback(event); - window_state.borrow_mut().event_callback = Some(callback); - handled as BOOL + with_input_handler(this, |input_handler| { + if let Some((text, new_selected_range, replacement_range)) = + pending_event.set_marked_text + { + input_handler.set_composition(&text, new_selected_range, replacement_range) + } else if had_marked_text && !inserted_text { + if pending_event.unmark_text { + input_handler.finish_composition(); + } else { + input_handler.cancel_composition(); + } + } + }); + + if has_marked_text { + YES } else { - NO + let mut handled = false; + let mut window_state_borrow = window_state.borrow_mut(); + if let Some(mut callback) = window_state_borrow.event_callback.take() { + drop(window_state_borrow); + + if inserted_text { + handled = true; + } else if let Some(text) = pending_event.insert_text { + if text.len() == 1 { + event.keystroke.key = text; + handled = callback(Event::KeyDown(event)); + } else if event.keystroke.cmd || event.keystroke.ctrl { + handled = callback(Event::KeyDown(event)); + } + } else { + handled = callback(Event::KeyDown(event)); + } + + window_state.borrow_mut().event_callback = Some(callback); + } + + handled as BOOL } } else { NO @@ -926,27 +1050,138 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { unsafe { msg_send![class!(NSArray), array] } } -extern "C" fn has_marked_text(_: &Object, _: Sel) -> BOOL { - false as BOOL +extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL { + with_input_handler(this, |input_handler| input_handler.marked_range()) + .flatten() + .is_some() as BOOL } -extern "C" fn selected_range(_: &Object, _: Sel) -> NSRange { - NSRange { - location: 0, - length: 0, - } +extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.marked_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.selected_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) } extern "C" fn first_rect_for_character_range(_: &Object, _: Sel, _: NSRange, _: id) -> NSRect { NSRect::new(NSPoint::new(0., 0.), NSSize::new(20., 20.)) } -extern "C" fn insert_text(_: &Object, _: Sel, _: id, _: NSRange) {} +extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); -extern "C" fn set_marked_text(_: &Object, _: Sel, _: id, _: NSRange, _: NSRange) {} + let window_state = get_window_state(this); + let mut window_state = window_state.borrow_mut(); + if window_state.pending_key_event.is_some() && !replacement_range.is_valid() { + window_state.pending_key_event.as_mut().unwrap().insert_text = Some(text.to_string()); + drop(window_state); + } else { + drop(window_state); + with_input_handler(this, |input_handler| { + input_handler.commit(text, replacement_range.to_range()); + }); + } -extern "C" fn attributed_substring_for_proposed_range(_: &Object, _: Sel, _: NSRange, _: id) -> id { - unsafe { msg_send![class!(NSAttributedString), alloc] } + with_input_handler(this, |input_handler| input_handler.unmark()); + } +} + +extern "C" fn set_marked_text( + this: &Object, + _: Sel, + text: id, + selected_range: NSRange, + replacement_range: NSRange, +) { + println!("set_marked_text"); + unsafe { + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let selected_range = selected_range.to_range(); + let replacement_range = replacement_range.to_range(); + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + + let window_state = get_window_state(this); + let mut window_state = window_state.borrow_mut(); + if let Some(pending) = window_state.pending_key_event.as_mut() { + pending.set_marked_text = Some((text.to_string(), selected_range, replacement_range)); + } else { + drop(window_state); + with_input_handler(this, |input_handler| { + input_handler.set_composition(text, selected_range, replacement_range); + }); + } + } +} + +extern "C" fn unmark_text(this: &Object, _: Sel) { + println!("unmark_text"); + let window_state = unsafe { get_window_state(this) }; + let mut window_state = window_state.borrow_mut(); + if let Some(pending) = window_state.pending_key_event.as_mut() { + pending.unmark_text = true; + pending.set_marked_text.take(); + } else { + drop(window_state); + with_input_handler(this, |input_handler| input_handler.finish_composition()); + } +} + +extern "C" fn attributed_substring_for_proposed_range( + this: &Object, + _: Sel, + range: NSRange, + actual_range: *mut c_void, +) -> id { + with_input_handler(this, |input_handler| { + let actual_range = actual_range as *mut NSRange; + if !actual_range.is_null() { + unsafe { *actual_range = NSRange::invalid() }; + } + + let requested_range = range.to_range()?; + if requested_range.is_empty() { + return None; + } + + let selected_range = input_handler.selected_range()?; + let intersection = cmp::max(requested_range.start, selected_range.start) + ..cmp::min(requested_range.end, selected_range.end); + if intersection.start >= intersection.end { + return None; + } + + unsafe { + let selected_text = ns_string(&input_handler.text_for_range(intersection)?); + let string: id = msg_send![class!(NSAttributedString), alloc]; + let string: id = msg_send![string, initWithString: selected_text]; + Some(string) + } + }) + .flatten() + .unwrap_or(nil) } async fn synthetic_drag( @@ -974,3 +1209,19 @@ async fn synthetic_drag( unsafe fn ns_string(string: &str) -> id { NSString::alloc(nil).init_str(string).autorelease() } + +fn with_input_handler(window: &Object, f: F) -> Option +where + F: FnOnce(&mut dyn InputHandler) -> R, +{ + let window_state = unsafe { get_window_state(window) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut input_handler) = window_state_borrow.input_handler.take() { + drop(window_state_borrow); + let result = f(input_handler.as_mut()); + window_state.borrow_mut().input_handler = Some(input_handler); + Some(result) + } else { + None + } +} diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index e1f39d046dd7725c7a0f2e29947bf2094fe2a640..dfb92b77f0516f18102f40b5b539c8cf6f0484e9 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -255,6 +255,8 @@ impl super::Window for Window { self.close_handlers.push(callback); } + fn set_input_handler(&mut self, _: Box) {} + fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver { let (done_tx, done_rx) = oneshot::channel(); self.pending_prompts.borrow_mut().push_back(done_tx); From 42ac4bf9fcb2373e9f954366c6d5c5957ace55e4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 20 Jul 2022 18:06:29 +0200 Subject: [PATCH 04/47] WIP --- crates/editor/src/element.rs | 1 - crates/gpui/src/platform.rs | 4 +-- crates/gpui/src/platform/mac/window.rs | 41 ++++++++++++++++---------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 99d60ed9a279a581e9973a1c888486992a46af21..ee61c6b926229e0b46ee64e6abf21561d8350ed9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1521,7 +1521,6 @@ impl Element for EditorElement { delta, precise, }) => self.scroll(*position, *delta, *precise, layout, paint, cx), - Event::KeyDown(KeyDownEvent { input, .. }) => self.key_down(input.as_deref(), cx), Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => { self.modifiers_changed(*cmd, cx) } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3797843144c03dce3e247d67be0489cf3b3948ed..d8c845aff68414933c4f61b084431192c731aac4 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -92,13 +92,13 @@ pub trait Dispatcher: Send + Sync { pub trait InputHandler { fn select(&mut self, range: Range); fn selected_range(&self) -> Option>; - fn set_composition( + fn edit(&mut self, replacement_range: Option>, text: &str) -> bool; + fn compose( &mut self, marked_text: &str, new_selected_range: Option>, replacement_range: Option>, ); - fn commit(&mut self, text: &str, replacement_range: Option>); fn cancel_composition(&mut self); fn finish_composition(&mut self); fn unmark(&mut self); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 6b827a771194e042910d0501a52561cce78bd80d..98d2cc503946a5de85e19d774dc079270d2a834d 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -695,9 +695,6 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) { } extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { - let had_marked_text = with_input_handler(this, |input_handler| input_handler.marked_range()) - .flatten() - .is_some(); let window_state = unsafe { get_window_state(this) }; let mut window_state_borrow = window_state.as_ref().borrow_mut(); @@ -725,6 +722,10 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> // TODO: handle "live conversion" window_state_borrow.pending_key_event = Some(Default::default()); drop(window_state_borrow); + let had_marked_text = + with_input_handler(this, |input_handler| input_handler.marked_range()) + .flatten() + .is_some(); // TODO: // Since Mac Eisu Kana keys cannot be handled by interpretKeyEvents to enable/ @@ -747,8 +748,9 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> let mut inserted_text = false; let has_marked_text = pending_event.set_marked_text.is_some(); if let Some(text) = pending_event.insert_text.as_ref() { - if !text.is_empty() && (had_marked_text || has_marked_text || text.len() > 1) { - with_input_handler(this, |input_handler| input_handler.commit(&text, None)); + if !text.is_empty() && (had_marked_text || has_marked_text || text.chars().count() > 1) + { + with_input_handler(this, |input_handler| input_handler.edit(None, &text)); inserted_text = true; } } @@ -757,7 +759,7 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> if let Some((text, new_selected_range, replacement_range)) = pending_event.set_marked_text { - input_handler.set_composition(&text, new_selected_range, replacement_range) + input_handler.compose(&text, new_selected_range, replacement_range) } else if had_marked_text && !inserted_text { if pending_event.unmark_text { input_handler.finish_composition(); @@ -777,15 +779,22 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> if inserted_text { handled = true; - } else if let Some(text) = pending_event.insert_text { - if text.len() == 1 { - event.keystroke.key = text; - handled = callback(Event::KeyDown(event)); - } else if event.keystroke.cmd || event.keystroke.ctrl { - handled = callback(Event::KeyDown(event)); - } } else { - handled = callback(Event::KeyDown(event)); + if let Some(text) = pending_event.insert_text { + event.input = Some(text); + if event.input.as_ref().unwrap().chars().count() == 1 { + event.keystroke.key = event.input.clone().unwrap(); + } + } + + handled = callback(Event::KeyDown(event.clone())); + if !handled { + if let Some(input) = event.input { + with_input_handler(this, |input_handler| { + handled = input_handler.edit(None, &input); + }); + } + } } window_state.borrow_mut().event_callback = Some(callback); @@ -1093,7 +1102,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS } else { drop(window_state); with_input_handler(this, |input_handler| { - input_handler.commit(text, replacement_range.to_range()); + input_handler.edit(replacement_range.to_range(), text); }); } @@ -1130,7 +1139,7 @@ extern "C" fn set_marked_text( } else { drop(window_state); with_input_handler(this, |input_handler| { - input_handler.set_composition(text, selected_range, replacement_range); + input_handler.compose(text, selected_range, replacement_range); }); } } From 1b0e93b1536a01c7871e4225d2b660780b180ce0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 20 Jul 2022 09:53:49 -0700 Subject: [PATCH 05/47] Change interpretation of marked_text and edit when handling input Co-authored-by: Antonio Scandurra ); fn selected_range(&self) -> Option>; - fn edit(&mut self, replacement_range: Option>, text: &str) -> bool; + fn edit(&mut self, replacement_range: Option>, text: &str); fn compose( &mut self, marked_text: &str, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 98d2cc503946a5de85e19d774dc079270d2a834d..9e749bc67da9af78ab46d122cc565ed6d43ef882 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -746,7 +746,10 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> let pending_event = window_state.borrow_mut().pending_key_event.take().unwrap(); let mut inserted_text = false; - let has_marked_text = pending_event.set_marked_text.is_some(); + let has_marked_text = pending_event.set_marked_text.is_some() + || with_input_handler(this, |input_handler| input_handler.marked_range()) + .flatten() + .is_some(); if let Some(text) = pending_event.insert_text.as_ref() { if !text.is_empty() && (had_marked_text || has_marked_text || text.chars().count() > 1) { @@ -781,7 +784,9 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> handled = true; } else { if let Some(text) = pending_event.insert_text { + // TODO: we may not need this anymore. event.input = Some(text); + if event.input.as_ref().unwrap().chars().count() == 1 { event.keystroke.key = event.input.clone().unwrap(); } @@ -791,8 +796,9 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> if !handled { if let Some(input) = event.input { with_input_handler(this, |input_handler| { - handled = input_handler.edit(None, &input); + input_handler.edit(None, &input); }); + handled = true; } } } From 0b81a4dfaedda38aa53febc6deffdc015247a411 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 20 Jul 2022 16:44:26 -0700 Subject: [PATCH 06/47] Call methods on the focused view during input events --- crates/gpui/src/app.rs | 240 ++++++++++++++++++++++++- crates/gpui/src/platform.rs | 18 +- crates/gpui/src/platform/mac/window.rs | 61 ++++--- 3 files changed, 281 insertions(+), 38 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c85dda16ca2b017b3190db9d60475c86b7d894b4..3bd4bd28a0a3d4ebc79e1ae72b7a93f44d810f99 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -7,8 +7,8 @@ use crate::{ platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, - AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions, - TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseRegionId, + PathPromptOptions, TextLayoutCache, }; pub use action::*; use anyhow::{anyhow, Context, Result}; @@ -28,7 +28,7 @@ use std::{ hash::{Hash, Hasher}, marker::PhantomData, mem, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, pin::Pin, rc::{self, Rc}, @@ -64,6 +64,33 @@ pub trait View: Entity + Sized { fn debug_json(&self, _: &AppContext) -> serde_json::Value { serde_json::Value::Null } + + fn text_for_range(&self, _: Range, _: &AppContext) -> Option { + None + } + fn selected_text_range(&self, _: &AppContext) -> Option> { + None + } + fn set_selected_text_range(&mut self, _: Range, _: &mut ViewContext) {} + fn marked_text_range(&self, _: &AppContext) -> Option> { + None + } + fn unmark_text(&mut self, _: &mut ViewContext) {} + fn replace_text_in_range( + &mut self, + _: Option>, + _: &str, + _: &mut ViewContext, + ) { + } + fn replace_and_mark_text_in_range( + &mut self, + _: Option>, + _: &str, + _: Option>, + _: &mut ViewContext, + ) { + } } pub trait ReadModel { @@ -154,6 +181,11 @@ pub struct TestAppContext { condition_duration: Option, } +pub struct WindowInputHandler { + app: Rc>, + window_id: usize, +} + impl App { pub fn new(asset_source: impl AssetSource) -> Result { let platform = platform::current::platform(); @@ -310,6 +342,121 @@ impl App { } } +impl WindowInputHandler { + fn read_focused_view(&self, f: F) -> Option + where + F: FnOnce(&dyn AnyView, &AppContext) -> T, + { + let app = self.app.borrow(); + let view_id = app.focused_view_id(self.window_id)?; + let view = app.cx.views.get(&(self.window_id, view_id))?; + let result = f(view.as_ref(), &app); + Some(result) + } + + fn update_focused_view(&mut self, f: F) -> Option + where + F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T, + { + let mut app = self.app.borrow_mut(); + app.update(|app| { + let view_id = app.focused_view_id(self.window_id)?; + let mut view = app.cx.views.remove(&(self.window_id, view_id))?; + let result = f(self.window_id, view_id, view.as_mut(), &mut *app); + app.cx.views.insert((self.window_id, view_id), view); + Some(result) + }) + } +} + +impl InputHandler for WindowInputHandler { + fn text_for_range(&self, range: Range) -> Option { + let result = self + .read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) + .flatten(); + + eprintln!("text_for_range({range:?}) -> {result:?}"); + + result + } + + fn selected_text_range(&self) -> Option> { + let result = self + .read_focused_view(|view, cx| view.selected_text_range(cx)) + .flatten(); + + eprintln!("selected_text_range() -> {result:?}"); + + result + } + + fn set_selected_text_range(&mut self, range: Range) { + eprintln!("set_selected_text_range({range:?})"); + + self.update_focused_view(|window_id, view_id, view, cx| { + view.set_selected_text_range(range, cx, window_id, view_id); + }); + } + + fn replace_text_in_range(&mut self, range: Option>, text: &str) { + eprintln!("replace_text_in_range({range:?}, {text:?})"); + + self.update_focused_view(|window_id, view_id, view, cx| { + view.replace_text_in_range(range, text, cx, window_id, view_id); + }); + } + + fn marked_text_range(&self) -> Option> { + let result = self + .read_focused_view(|view, cx| view.marked_text_range(cx)) + .flatten(); + + eprintln!("marked_text_range() -> {result:?}"); + + result + } + + fn unmark_text(&mut self) { + eprintln!("unmark_text()"); + + self.update_focused_view(|window_id, view_id, view, cx| { + view.unmark_text(cx, window_id, view_id); + }); + } + + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + eprintln!( + "replace_and_mark_text_in_range({range:?}, {new_text:?}, {new_selected_range:?})" + ); + + self.update_focused_view(|window_id, view_id, view, cx| { + view.replace_and_mark_text_in_range( + range, + new_text, + new_selected_range, + cx, + window_id, + view_id, + ); + }); + } + + // TODO - do these need to be handled separately? + + fn cancel_composition(&mut self) { + self.unmark_text(); + } + + fn finish_composition(&mut self) { + self.unmark_text(); + } +} + #[cfg(any(test, feature = "test-support"))] impl TestAppContext { pub fn new( @@ -1888,6 +2035,11 @@ impl MutableAppContext { })); } + window.set_input_handler(Box::new(WindowInputHandler { + app: self.upgrade().0, + window_id, + })); + let scene = presenter .borrow_mut() @@ -3179,6 +3331,35 @@ pub trait AnyView { fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; fn debug_json(&self, cx: &AppContext) -> serde_json::Value; + + fn text_for_range(&self, range: Range, cx: &AppContext) -> Option; + fn selected_text_range(&self, cx: &AppContext) -> Option>; + fn set_selected_text_range( + &mut self, + range: Range, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ); + fn marked_text_range(&self, cx: &AppContext) -> Option>; + fn unmark_text(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ); + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ); } impl AnyView for T @@ -3229,6 +3410,59 @@ where fn debug_json(&self, cx: &AppContext) -> serde_json::Value { View::debug_json(self, cx) } + + fn text_for_range(&self, range: Range, cx: &AppContext) -> Option { + View::text_for_range(self, range, cx) + } + + fn selected_text_range(&self, cx: &AppContext) -> Option> { + View::selected_text_range(self, cx) + } + + fn set_selected_text_range( + &mut self, + range: Range, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ) { + let mut cx = ViewContext::new(cx, window_id, view_id); + View::set_selected_text_range(self, range, &mut cx) + } + + fn marked_text_range(&self, cx: &AppContext) -> Option> { + View::marked_text_range(self, cx) + } + + fn unmark_text(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) { + let mut cx = ViewContext::new(cx, window_id, view_id); + View::unmark_text(self, &mut cx) + } + + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ) { + let mut cx = ViewContext::new(cx, window_id, view_id); + View::replace_text_in_range(self, range, text, &mut cx) + } + + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ) { + let mut cx = ViewContext::new(cx, window_id, view_id); + View::replace_and_mark_text_in_range(self, range, new_text, new_selected_range, &mut cx) + } } pub struct ModelContext<'a, T: ?Sized> { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 255995cdf364806f4d4b2b7ef0c35b88d0df6022..22eaa00540166f8a0217e2212b123087cf776d4b 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -90,20 +90,20 @@ pub trait Dispatcher: Send + Sync { } pub trait InputHandler { - fn select(&mut self, range: Range); - fn selected_range(&self) -> Option>; - fn edit(&mut self, replacement_range: Option>, text: &str); - fn compose( + fn selected_text_range(&self) -> Option>; + fn set_selected_text_range(&mut self, range: Range); + fn text_for_range(&self, range: Range) -> Option; + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str); + fn replace_and_mark_text_in_range( &mut self, - marked_text: &str, + range: Option>, + new_text: &str, new_selected_range: Option>, - replacement_range: Option>, ); + fn marked_text_range(&self) -> Option>; + fn unmark_text(&mut self); fn cancel_composition(&mut self); fn finish_composition(&mut self); - fn unmark(&mut self); - fn marked_range(&self) -> Option>; - fn text_for_range(&self, range: Range) -> Option; } pub trait Window: WindowContext { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 9e749bc67da9af78ab46d122cc565ed6d43ef882..e7bffa6ecd413047cb1550841b28e7c33648b89e 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -254,6 +254,10 @@ unsafe fn build_classes() { attributed_substring_for_proposed_range as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, ); + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(&Object, Sel, Sel), + ); decl.register() }; @@ -723,7 +727,7 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> window_state_borrow.pending_key_event = Some(Default::default()); drop(window_state_borrow); let had_marked_text = - with_input_handler(this, |input_handler| input_handler.marked_range()) + with_input_handler(this, |input_handler| input_handler.marked_text_range()) .flatten() .is_some(); @@ -745,15 +749,19 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> // } let pending_event = window_state.borrow_mut().pending_key_event.take().unwrap(); + + dbg!(&pending_event); + let mut inserted_text = false; let has_marked_text = pending_event.set_marked_text.is_some() - || with_input_handler(this, |input_handler| input_handler.marked_range()) + || with_input_handler(this, |input_handler| input_handler.marked_text_range()) .flatten() .is_some(); if let Some(text) = pending_event.insert_text.as_ref() { - if !text.is_empty() && (had_marked_text || has_marked_text || text.chars().count() > 1) - { - with_input_handler(this, |input_handler| input_handler.edit(None, &text)); + if !text.is_empty() { + with_input_handler(this, |input_handler| { + input_handler.replace_text_in_range(None, &text) + }); inserted_text = true; } } @@ -762,7 +770,11 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> if let Some((text, new_selected_range, replacement_range)) = pending_event.set_marked_text { - input_handler.compose(&text, new_selected_range, replacement_range) + input_handler.replace_and_mark_text_in_range( + replacement_range, + &text, + new_selected_range, + ) } else if had_marked_text && !inserted_text { if pending_event.unmark_text { input_handler.finish_composition(); @@ -784,23 +796,12 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> handled = true; } else { if let Some(text) = pending_event.insert_text { - // TODO: we may not need this anymore. - event.input = Some(text); - - if event.input.as_ref().unwrap().chars().count() == 1 { - event.keystroke.key = event.input.clone().unwrap(); + if text.chars().count() == 1 { + event.keystroke.key = text; } } handled = callback(Event::KeyDown(event.clone())); - if !handled { - if let Some(input) = event.input { - with_input_handler(this, |input_handler| { - input_handler.edit(None, &input); - }); - handled = true; - } - } } window_state.borrow_mut().event_callback = Some(callback); @@ -1066,19 +1067,19 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { } extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL { - with_input_handler(this, |input_handler| input_handler.marked_range()) + with_input_handler(this, |input_handler| input_handler.marked_text_range()) .flatten() .is_some() as BOOL } extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange { - with_input_handler(this, |input_handler| input_handler.marked_range()) + with_input_handler(this, |input_handler| input_handler.marked_text_range()) .flatten() .map_or(NSRange::invalid(), |range| range.into()) } extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { - with_input_handler(this, |input_handler| input_handler.selected_range()) + with_input_handler(this, |input_handler| input_handler.selected_text_range()) .flatten() .map_or(NSRange::invalid(), |range| range.into()) } @@ -1108,11 +1109,11 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS } else { drop(window_state); with_input_handler(this, |input_handler| { - input_handler.edit(replacement_range.to_range(), text); + input_handler.replace_text_in_range(replacement_range.to_range(), text); }); } - with_input_handler(this, |input_handler| input_handler.unmark()); + with_input_handler(this, |input_handler| input_handler.unmark_text()); } } @@ -1145,7 +1146,11 @@ extern "C" fn set_marked_text( } else { drop(window_state); with_input_handler(this, |input_handler| { - input_handler.compose(text, selected_range, replacement_range); + input_handler.replace_and_mark_text_in_range( + replacement_range, + text, + selected_range, + ); }); } } @@ -1181,7 +1186,7 @@ extern "C" fn attributed_substring_for_proposed_range( return None; } - let selected_range = input_handler.selected_range()?; + let selected_range = input_handler.selected_text_range()?; let intersection = cmp::max(requested_range.start, selected_range.start) ..cmp::min(requested_range.end, selected_range.end); if intersection.start >= intersection.end { @@ -1199,6 +1204,10 @@ extern "C" fn attributed_substring_for_proposed_range( .unwrap_or(nil) } +extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) { + // +} + async fn synthetic_drag( window_state: Weak>, drag_id: usize, From f985515141cbfd53bc7a3a7022a12d2dd6bd5f92 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 20 Jul 2022 16:45:27 -0700 Subject: [PATCH 07/47] Start work on new text input handling in Editor --- crates/collab/src/integration_tests.rs | 10 +- crates/editor/src/display_map.rs | 5 + crates/editor/src/editor.rs | 133 ++++++++++++++++++++----- crates/theme/src/theme.rs | 1 + crates/zed/src/zed.rs | 12 +-- styles/src/styleTree/editor.ts | 11 +- 6 files changed, 129 insertions(+), 43 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 358f016366b56db44c88d0219616d8df4aa2d4b5..a3dcd5fbce3942355da8cfacacb875b6bca0fd41 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -11,8 +11,8 @@ use client::{ }; use collections::{BTreeMap, HashMap, HashSet}; use editor::{ - self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, - ToOffset, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset, + ToggleCodeActions, Undo, }; use futures::{channel::mpsc, Future, StreamExt as _}; use gpui::{ @@ -154,9 +154,7 @@ async fn test_share_project( // .await; // Edit the buffer as client B and see that edit as client A. - editor_b.update(cx_b, |editor, cx| { - editor.handle_input(&Input("ok, ".into()), cx) - }); + editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); buffer_a .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") .await; @@ -1751,7 +1749,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu // Type a completion trigger character as the guest. editor_b.update(cx_b, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(&Input(".".into()), cx); + editor.handle_input(".", cx); cx.focus(&editor_b); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index e4c888af4d51ea99fa4e78d048ac9191987baff3..0a0ac4a0e0970513b0cef1d0fcadde41967eed56 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -195,6 +195,11 @@ impl DisplayMap { .insert(Some(type_id), Arc::new((style, ranges))); } + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + let highlights = self.text_highlights.get(&Some(type_id))?; + Some((highlights.0, &highlights.1)) + } + pub fn clear_text_highlights( &mut self, type_id: TypeId, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ac7b8a18668e7d02a18cb5f54d86b0bf2e456492..b3698b6ad93be38c472307bda19ca3d0190b44b2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -228,6 +228,7 @@ impl_internal_actions!(editor, [Scroll, Select, Jump]); enum DocumentHighlightRead {} enum DocumentHighlightWrite {} +enum InputComposition {} #[derive(Copy, Clone, PartialEq, Eq)] pub enum Direction { @@ -240,7 +241,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); cx.add_action(Editor::select); cx.add_action(Editor::cancel); - cx.add_action(Editor::handle_input); cx.add_action(Editor::newline); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); @@ -1813,13 +1813,11 @@ impl Editor { cx.propagate_action(); } - pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { + pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { if !self.input_enabled { - cx.propagate_action(); return; } - let text = action.0.as_ref(); if !self.skip_autoclose_end(text, cx) { self.transact(cx, |this, cx| { if !this.surround_with_bracket_pair(text, cx) { @@ -5522,6 +5520,13 @@ impl Editor { cx.notify(); } + pub fn text_highlights<'a, T: 'static>( + &'a self, + cx: &'a AppContext, + ) -> Option<(HighlightStyle, &'a [Range])> { + self.display_map.read(cx).text_highlights(TypeId::of::()) + } + pub fn clear_text_highlights( &mut self, cx: &mut ViewContext, @@ -5871,6 +5876,80 @@ impl View for Editor { context } + + fn text_for_range(&self, range: Range, cx: &AppContext) -> Option { + Some( + self.buffer + .read(cx) + .read(cx) + .text_for_range(range) + .collect(), + ) + } + + fn selected_text_range(&self, cx: &AppContext) -> Option> { + Some(self.selections.newest(cx).range()) + } + + fn set_selected_text_range(&mut self, range: Range, cx: &mut ViewContext) { + self.change_selections(None, cx, |selections| selections.select_ranges([range])); + } + + fn marked_text_range(&self, cx: &AppContext) -> Option> { + let range = self.text_highlights::(cx)?.1.get(0)?; + Some(range.to_offset(&*self.buffer.read(cx).read(cx))) + } + + fn unmark_text(&mut self, cx: &mut ViewContext) { + self.clear_text_highlights::(cx); + } + + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + if let Some(range) = range { + this.set_selected_text_range(range, cx); + } + this.handle_input(text, cx); + }); + } + + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + text: &str, + _new_selected_range: Option>, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + let range = range.or_else(|| { + let ranges = this.text_highlights::(cx)?.1; + let range = ranges.first()?; + let snapshot = this.buffer.read(cx).read(cx); + Some(range.to_offset(&*snapshot)) + }); + if let Some(range) = range { + this.set_selected_text_range(range, cx); + } + + let selection = this.selections.newest_anchor(); + let marked_range = { + let snapshot = this.buffer.read(cx).read(cx); + selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot) + }; + this.highlight_text::( + vec![marked_range], + this.style(cx).composition_mark, + cx, + ); + + this.handle_input(text, cx); + }); + } } fn build_style( @@ -8241,9 +8320,9 @@ mod tests { // is pasted at each cursor. cx.set_state("|two one✅ four three six five |"); cx.update_editor(|e, cx| { - e.handle_input(&Input("( ".into()), cx); + e.handle_input("( ", cx); e.paste(&Paste, cx); - e.handle_input(&Input(") ".into()), cx); + e.handle_input(") ", cx); }); cx.assert_editor_state(indoc! {" ( one✅ @@ -8918,9 +8997,9 @@ mod tests { ]) }); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -8933,9 +9012,9 @@ mod tests { ); view.move_right(&MoveRight, cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + view.handle_input("}", cx); assert_eq!( view.text(cx), " @@ -8948,8 +9027,8 @@ mod tests { ); view.undo(&Undo, cx); - view.handle_input(&Input("/".to_string()), cx); - view.handle_input(&Input("*".to_string()), cx); + view.handle_input("/", cx); + view.handle_input("*", cx); assert_eq!( view.text(cx), " @@ -8968,7 +9047,7 @@ mod tests { DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), ]) }); - view.handle_input(&Input("*".to_string()), cx); + view.handle_input("*", cx); assert_eq!( view.text(cx), " @@ -8986,7 +9065,7 @@ mod tests { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) }); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -9002,7 +9081,7 @@ mod tests { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1)]) }); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -9019,7 +9098,7 @@ mod tests { ); view.undo(&Undo, cx); - view.handle_input(&Input("[".to_string()), cx); + view.handle_input("[", cx); assert_eq!( view.text(cx), " @@ -9039,7 +9118,7 @@ mod tests { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]) }); - view.handle_input(&Input("[".to_string()), cx); + view.handle_input("[", cx); assert_eq!( view.text(cx), " @@ -9095,9 +9174,9 @@ mod tests { ]) }); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -9177,9 +9256,9 @@ mod tests { ]) }); - editor.handle_input(&Input("{".to_string()), cx); - editor.handle_input(&Input("{".to_string()), cx); - editor.handle_input(&Input("_".to_string()), cx); + editor.handle_input("{", cx); + editor.handle_input("{", cx); + editor.handle_input("_", cx); assert_eq!( editor.text(cx), " @@ -9905,7 +9984,7 @@ mod tests { ]) }); - view.handle_input(&Input("X".to_string()), cx); + view.handle_input("X", cx); assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); assert_eq!( view.selections.ranges(cx), @@ -9945,7 +10024,7 @@ mod tests { assert_eq!(view.text(cx), expected_text); view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); - view.handle_input(&Input("X".to_string()), cx); + view.handle_input("X", cx); let (expected_text, expected_selections) = marked_text_ranges(indoc! {" aaaa diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 2299bc3477fa55fef6ab1d956c3646de408a16c8..ad013bc8602f478a88e33f524edbc42fbc6d3462 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -450,6 +450,7 @@ pub struct Editor { pub unnecessary_code_fade: f32, pub hover_popover: HoverPopover, pub link_definition: HighlightStyle, + pub composition_mark: HighlightStyle, pub jump_icon: Interactive, } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a7420c59f0f012dad9019532a6e23da9d2738e23..3a5725905d83c8da5a814b6d0f16dd4928b05de3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -920,11 +920,7 @@ mod tests { item.downcast::().unwrap() }); - cx.update(|cx| { - editor.update(cx, |editor, cx| { - editor.handle_input(&editor::Input("x".into()), cx) - }) - }); + cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input("x", cx))); app_state .fs .as_fake() @@ -971,7 +967,7 @@ mod tests { editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); - editor.handle_input(&editor::Input("hi".into()), cx); + editor.handle_input("hi", cx); assert!(editor.is_dirty(cx)); }); @@ -997,7 +993,7 @@ mod tests { // Edit the file and save it again. This time, there is no filename prompt. editor.update(cx, |editor, cx| { - editor.handle_input(&editor::Input(" there".into()), cx); + editor.handle_input(" there", cx); assert_eq!(editor.is_dirty(cx.as_ref()), true); }); let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); @@ -1057,7 +1053,7 @@ mod tests { editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); - editor.handle_input(&editor::Input("hi".into()), cx); + editor.handle_input("hi", cx); assert!(editor.is_dirty(cx.as_ref())); }); diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 8031a229c4a81b4f5de26aa35b614a03cb9eb20d..222cf52d60fb46656aa6b517223a9fc35a44f829 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -2,6 +2,7 @@ import Theme from "../themes/common/theme"; import { backgroundColor, border, + borderColor, iconColor, player, popoverShadow, @@ -138,8 +139,8 @@ export default function editor(theme: Theme) { invalidHintDiagnostic: diagnostic(theme, "muted"), invalidInformationDiagnostic: diagnostic(theme, "muted"), invalidWarningDiagnostic: diagnostic(theme, "muted"), - hover_popover: hoverPopover(theme), - link_definition: { + hoverPopover: hoverPopover(theme), + linkDefinition: { color: theme.syntax.linkUri.color, underline: theme.syntax.linkUri.underline, }, @@ -159,6 +160,12 @@ export default function editor(theme: Theme) { background: backgroundColor(theme, "on500", "base"), } }, + compositionMark: { + underline: { + thickness: 1.0, + color: borderColor(theme, "active") + }, + }, syntax, }; } From f712dec4c04433dd9e6c3a1f910927199d7f7377 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 20 Jul 2022 17:33:37 -0700 Subject: [PATCH 08/47] Use new API for input handling in Terminal --- crates/terminal/src/terminal.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 26880258daff3a2bc3e94d35d297a817038b7a9d..a41b8f5ad3994e587ed6eab216b93b59f080bf14 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -10,22 +10,19 @@ use alacritty_terminal::{ use connection::{Event, TerminalConnection}; use dirs::home_dir; -use editor::Input; use futures::channel::mpsc::UnboundedSender; use gpui::{ actions, elements::*, keymap::Keystroke, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, }; use modal::deploy_modal; - use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; +use terminal_element::TerminalEl; use workspace::{Item, Workspace}; -use crate::terminal_element::TerminalEl; - const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space. const DEBUG_TERMINAL_HEIGHT: f32 = 200.; const DEBUG_CELL_WIDTH: f32 = 5.; @@ -67,7 +64,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(deploy_modal); cx.add_action(Terminal::copy); cx.add_action(Terminal::paste); - cx.add_action(Terminal::input); cx.add_action(Terminal::clear); } @@ -154,10 +150,10 @@ impl Terminal { } } - fn input(&mut self, Input(text): &Input, cx: &mut ViewContext) { + fn input(&mut self, text: &str, cx: &mut ViewContext) { self.connection.update(cx, |connection, _| { //TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837) - connection.write_to_pty(text.clone()); + connection.write_to_pty(text.to_string()); }); if self.has_bell { @@ -265,6 +261,15 @@ impl View for Terminal { } context } + + fn replace_text_in_range( + &mut self, + _: Option>, + text: &str, + cx: &mut ViewContext, + ) { + self.input(text, cx); + } } impl Item for Terminal { From eda60effed30c9aa1efada06f7ab7278487e9667 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 08:57:20 +0200 Subject: [PATCH 09/47] Honor setting the selected range in addition to marking text --- crates/editor/src/editor.rs | 6 +++++- crates/gpui/src/app.rs | 2 ++ crates/gpui/src/platform/mac/window.rs | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b3698b6ad93be38c472307bda19ca3d0190b44b2..ba5ae4eefea16f54970c577aa740130db827aa53 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5922,7 +5922,7 @@ impl View for Editor { &mut self, range: Option>, text: &str, - _new_selected_range: Option>, + new_selected_range: Option>, cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { @@ -5948,6 +5948,10 @@ impl View for Editor { ); this.handle_input(text, cx); + + if let Some(new_selected_range) = new_selected_range { + this.set_selected_text_range(new_selected_range, cx); + } }); } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 3bd4bd28a0a3d4ebc79e1ae72b7a93f44d810f99..a049f5753fba9140145ce662c2b6966f74f3861b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -449,10 +449,12 @@ impl InputHandler for WindowInputHandler { // TODO - do these need to be handled separately? fn cancel_composition(&mut self) { + println!("cancel_composition()"); self.unmark_text(); } fn finish_composition(&mut self) { + println!("finish_composition()"); self.unmark_text(); } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index e7bffa6ecd413047cb1550841b28e7c33648b89e..097a53b260ab1045a3fae5b93955ab6f7c7c5ab6 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -775,7 +775,7 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> &text, new_selected_range, ) - } else if had_marked_text && !inserted_text { + } else if had_marked_text && !has_marked_text && !inserted_text { if pending_event.unmark_text { input_handler.finish_composition(); } else { @@ -1194,9 +1194,9 @@ extern "C" fn attributed_substring_for_proposed_range( } unsafe { - let selected_text = ns_string(&input_handler.text_for_range(intersection)?); + let selected_text = input_handler.text_for_range(intersection)?; let string: id = msg_send![class!(NSAttributedString), alloc]; - let string: id = msg_send![string, initWithString: selected_text]; + let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; Some(string) } }) From 32662b6b92784319505dd9471d6149b5c2292d63 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 09:40:48 +0200 Subject: [PATCH 10/47] Start indexing UTF-16 offsets This is needed because cocoa will report ranges as UTF-16 indices. --- crates/editor/src/display_map/fold_map.rs | 28 ++--- crates/editor/src/multi_buffer.rs | 120 +++++++++++++++++----- crates/project/src/fs.rs | 2 +- crates/text/src/rope.rs | 113 +++++++++++++++++--- crates/text/src/tests.rs | 15 ++- crates/text/src/text.rs | 38 +++++++ 6 files changed, 261 insertions(+), 55 deletions(-) diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 95ca958ad651ed7f3f7fc8441a9dcc2eed36f4d5..95c3abb25238adbf76dccf496ad2e719f815b881 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -63,14 +63,14 @@ impl FoldPoint { .cursor::<(FoldPoint, TransformSummary)>(); cursor.seek(self, Bias::Right, &()); let overshoot = self.0 - cursor.start().1.output.lines; - let mut offset = cursor.start().1.output.bytes; + let mut offset = cursor.start().1.output.len; if !overshoot.is_zero() { let transform = cursor.item().expect("display point out of range"); assert!(transform.output_text.is_none()); let end_buffer_offset = snapshot .buffer_snapshot .point_to_offset(cursor.start().1.input.lines + overshoot); - offset += end_buffer_offset - cursor.start().1.input.bytes; + offset += end_buffer_offset - cursor.start().1.input.len; } FoldOffset(offset) } @@ -249,7 +249,7 @@ impl FoldMap { fn check_invariants(&self) { if cfg!(test) { assert_eq!( - self.transforms.lock().summary().input.bytes, + self.transforms.lock().summary().input.len, self.buffer.lock().len(), "transform tree does not match buffer's length" ); @@ -341,7 +341,7 @@ impl FoldMap { let mut fold = folds.next().unwrap(); let sum = new_transforms.summary(); - assert!(fold.start >= sum.input.bytes); + assert!(fold.start >= sum.input.len); while folds .peek() @@ -353,9 +353,9 @@ impl FoldMap { } } - if fold.start > sum.input.bytes { + if fold.start > sum.input.len { let text_summary = new_buffer - .text_summary_for_range::(sum.input.bytes..fold.start); + .text_summary_for_range::(sum.input.len..fold.start); new_transforms.push( Transform { summary: TransformSummary { @@ -384,9 +384,9 @@ impl FoldMap { } let sum = new_transforms.summary(); - if sum.input.bytes < edit.new.end { + if sum.input.len < edit.new.end { let text_summary = new_buffer - .text_summary_for_range::(sum.input.bytes..edit.new.end); + .text_summary_for_range::(sum.input.len..edit.new.end); new_transforms.push( Transform { summary: TransformSummary { @@ -558,7 +558,7 @@ impl FoldSnapshot { } pub fn len(&self) -> FoldOffset { - FoldOffset(self.transforms.summary().output.bytes) + FoldOffset(self.transforms.summary().output.len) } pub fn line_len(&self, row: u32) -> u32 { @@ -766,7 +766,7 @@ impl FoldSnapshot { ) } } else { - FoldOffset(self.transforms.summary().output.bytes) + FoldOffset(self.transforms.summary().output.len) } } @@ -1050,7 +1050,7 @@ impl<'a> Iterator for FoldChunks<'a> { // advance the transform and buffer cursors to the end of the fold. if let Some(output_text) = transform.output_text { self.buffer_chunk.take(); - self.buffer_offset += transform.summary.input.bytes; + self.buffer_offset += transform.summary.input.len; self.buffer_chunks.seek(self.buffer_offset); while self.buffer_offset >= self.transform_cursor.end(&()).1 @@ -1158,7 +1158,7 @@ impl FoldOffset { let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0 .0) as u32) } else { - let buffer_offset = cursor.start().1.input.bytes + self.0 - cursor.start().0 .0; + let buffer_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0; let buffer_point = snapshot.buffer_snapshot.offset_to_point(buffer_offset); buffer_point - cursor.start().1.input.lines }; @@ -1176,7 +1176,7 @@ impl Sub for FoldOffset { impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset { fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - self.0 += &summary.output.bytes; + self.0 += &summary.output.len; } } @@ -1188,7 +1188,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize { fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - *self += &summary.input.bytes; + *self += &summary.input.len; } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d5b85b0aee86dca3f995e047ca998a865cfeac5e..d52f693f1a1dc726bd9250e93cd2b83dd42b14a4 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -9,7 +9,7 @@ pub use language::Completion; use language::{ char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, - ToPoint as _, ToPointUtf16 as _, TransactionId, + ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; use settings::Settings; use smallvec::SmallVec; @@ -29,7 +29,7 @@ use text::{ locator::Locator, rope::TextDimension, subscription::{Subscription, Topic}, - Edit, Point, PointUtf16, TextSummary, + Edit, OffsetUtf16, Point, PointUtf16, TextSummary, }; use theme::SyntaxTheme; use util::post_inc; @@ -72,6 +72,10 @@ pub trait ToOffset: 'static + fmt::Debug { fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize; } +pub trait ToOffsetUtf16: 'static + fmt::Debug { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16; +} + pub trait ToPoint: 'static + fmt::Debug { fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point; } @@ -809,7 +813,7 @@ impl MultiBuffer { let mut cursor = snapshot.excerpts.cursor::>(); let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &()); - let edit_start = new_excerpts.summary().text.bytes; + let edit_start = new_excerpts.summary().text.len; new_excerpts.update_last( |excerpt| { excerpt.has_trailing_newline = true; @@ -862,7 +866,7 @@ impl MultiBuffer { &(), ); - let edit_end = new_excerpts.summary().text.bytes; + let edit_end = new_excerpts.summary().text.len; let suffix = cursor.suffix(&()); let changed_trailing_excerpt = suffix.is_empty(); @@ -1068,7 +1072,7 @@ impl MultiBuffer { // Push an edit for the removal of this run of excerpts. let old_end = cursor.start().1; - let new_start = new_excerpts.summary().text.bytes; + let new_start = new_excerpts.summary().text.len; edits.push(Edit { old: old_start..old_end, new: new_start..new_start, @@ -1297,7 +1301,7 @@ impl MultiBuffer { ) .map(|mut edit| { let excerpt_old_start = cursor.start().1; - let excerpt_new_start = new_excerpts.summary().text.bytes; + let excerpt_new_start = new_excerpts.summary().text.len; edit.old.start += excerpt_old_start; edit.old.end += excerpt_old_start; edit.new.start += excerpt_new_start; @@ -1527,7 +1531,7 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::(); cursor.seek(&offset, Bias::Left, &()); let mut excerpt_chunks = cursor.item().map(|excerpt| { - let end_before_footer = cursor.start() + excerpt.text_summary.bytes; + let end_before_footer = cursor.start() + excerpt.text_summary.len; let start = excerpt.range.context.start.to_offset(&excerpt.buffer); let end = start + (cmp::min(offset, end_before_footer) - cursor.start()); excerpt.buffer.reversed_chunks_in_range(start..end) @@ -1629,7 +1633,7 @@ impl MultiBufferSnapshot { } pub fn len(&self) -> usize { - self.excerpts.summary().text.bytes + self.excerpts.summary().text.len } pub fn max_buffer_row(&self) -> u32 { @@ -1824,7 +1828,53 @@ impl MultiBufferSnapshot { .point_to_offset(excerpt_start_point + overshoot); *start_offset + buffer_offset - excerpt_start_offset } else { - self.excerpts.summary().text.bytes + self.excerpts.summary().text.len + } + } + + pub fn offset_utf16_to_offset(&self, offset_utf16: OffsetUtf16) -> usize { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_utf16_to_offset(offset_utf16); + } + + let mut cursor = self.excerpts.cursor::<(OffsetUtf16, usize)>(); + cursor.seek(&offset_utf16, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset_utf16, start_offset) = cursor.start(); + let overshoot = offset_utf16 - start_offset_utf16; + let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_start_offset_utf16 = + excerpt.buffer.offset_to_offset_utf16(excerpt_start_offset); + let buffer_offset = excerpt + .buffer + .offset_utf16_to_offset(excerpt_start_offset_utf16 + overshoot); + *start_offset + (buffer_offset - excerpt_start_offset) + } else { + self.excerpts.summary().text.len + } + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_offset_utf16(offset); + } + + let mut cursor = self.excerpts.cursor::<(usize, OffsetUtf16)>(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset, start_offset_utf16) = cursor.start(); + let overshoot = offset - start_offset; + let excerpt_start_offset_utf16 = + excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); + let excerpt_start_offset = excerpt + .buffer + .offset_utf16_to_offset(excerpt_start_offset_utf16); + let buffer_offset_utf16 = excerpt + .buffer + .offset_to_offset_utf16(excerpt_start_offset + overshoot); + *start_offset_utf16 + (buffer_offset_utf16 - excerpt_start_offset_utf16) + } else { + OffsetUtf16(self.excerpts.summary().text.len_utf16) } } @@ -1847,7 +1897,7 @@ impl MultiBufferSnapshot { .point_utf16_to_offset(excerpt_start_point + overshoot); *start_offset + (buffer_offset - excerpt_start_offset) } else { - self.excerpts.summary().text.bytes + self.excerpts.summary().text.len } } @@ -2311,7 +2361,7 @@ impl MultiBufferSnapshot { .context .start .to_offset(&start_excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes; + let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len; let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(*cursor.start()); @@ -2415,7 +2465,7 @@ impl MultiBufferSnapshot { .context .start .to_offset(&start_excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes; + let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len; let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(*cursor.start()); @@ -2717,11 +2767,11 @@ impl Excerpt { ) -> ExcerptChunks<'a> { let content_start = self.range.context.start.to_offset(&self.buffer); let chunks_start = content_start + range.start; - let chunks_end = content_start + cmp::min(range.end, self.text_summary.bytes); + let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.bytes - && range.end > self.text_summary.bytes + && range.start <= self.text_summary.len + && range.end > self.text_summary.len { 1 } else { @@ -2739,10 +2789,10 @@ impl Excerpt { fn bytes_in_range(&self, range: Range) -> ExcerptBytes { let content_start = self.range.context.start.to_offset(&self.buffer); let bytes_start = content_start + range.start; - let bytes_end = content_start + cmp::min(range.end, self.text_summary.bytes); + let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.bytes - && range.end > self.text_summary.bytes + && range.start <= self.text_summary.len + && range.end > self.text_summary.len { 1 } else { @@ -2836,13 +2886,13 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for TextSummary { impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for usize { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.bytes; + *self += summary.text.len; } } impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize { fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.text.bytes) + Ord::cmp(self, &cursor_location.text.len) } } @@ -2852,6 +2902,12 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a } } +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for OffsetUtf16 { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + self.0 += summary.text.len_utf16; + } +} + impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { *self += summary.text.lines; @@ -3060,6 +3116,24 @@ impl ToOffset for usize { } } +impl ToOffset for OffsetUtf16 { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + snapshot.offset_utf16_to_offset(*self) + } +} + +impl ToOffsetUtf16 for OffsetUtf16 { + fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + *self + } +} + +impl ToOffsetUtf16 for usize { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + snapshot.offset_to_offset_utf16(*self) + } +} + impl ToPoint for usize { fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { snapshot.offset_to_point(*self) @@ -3823,7 +3897,7 @@ mod tests { buffer.text_summary_for_range::(0..buffer_range.start); let excerpt_start = excerpt_starts.next().unwrap(); - let mut offset = excerpt_start.bytes; + let mut offset = excerpt_start.len; let mut buffer_offset = buffer_range.start; let mut point = excerpt_start.lines; let mut buffer_point = buffer_start_point; @@ -3841,7 +3915,7 @@ mod tests { let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right); assert_eq!( left_offset, - excerpt_start.bytes + (buffer_left_offset - buffer_range.start), + excerpt_start.len + (buffer_left_offset - buffer_range.start), "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}", offset, buffer_id, @@ -3849,7 +3923,7 @@ mod tests { ); assert_eq!( right_offset, - excerpt_start.bytes + (buffer_right_offset - buffer_range.start), + excerpt_start.len + (buffer_right_offset - buffer_range.start), "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}", offset, buffer_id, diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 460b788afdb476158d47a5ca89ce068b9cf787de..0084402b85d92661df32cf73ab95e3c5b5c0ccad 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -162,7 +162,7 @@ impl Fs for RealFs { } async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { - let buffer_size = text.summary().bytes.min(10 * 1024); + let buffer_size = text.summary().len.min(10 * 1024); let file = smol::fs::File::create(path).await?; let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); for chunk in chunks(text, line_ending) { diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index e8aff3f52fa6648c26c6f7398a57f89a803cf8e0..8d278bb8f78ebd7d3c9fc953c86b4621088fe670 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -1,6 +1,5 @@ -use crate::PointUtf16; - use super::Point; +use crate::{OffsetUtf16, PointUtf16}; use arrayvec::ArrayString; use bromberg_sl2::{DigestString, HashMatrix}; use smallvec::SmallVec; @@ -165,8 +164,34 @@ impl Rope { Chunks::new(self, range, true) } + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + if offset >= self.summary().len { + return OffsetUtf16(self.summary().len_utf16); + } + let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_to_offset_utf16(overshoot) + }) + } + + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + if offset.0 >= self.summary().len_utf16 { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_utf16_to_offset(overshoot) + }) + } + pub fn offset_to_point(&self, offset: usize) -> Point { - if offset >= self.summary().bytes { + if offset >= self.summary().len { return self.summary().lines; } let mut cursor = self.chunks.cursor::<(usize, Point)>(); @@ -179,7 +204,7 @@ impl Rope { } pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if offset >= self.summary().bytes { + if offset >= self.summary().len { return self.summary().lines_utf16; } let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(); @@ -206,7 +231,7 @@ impl Rope { pub fn point_to_offset(&self, point: Point) -> usize { if point >= self.summary().lines { - return self.summary().bytes; + return self.summary().len; } let mut cursor = self.chunks.cursor::<(Point, usize)>(); cursor.seek(&point, Bias::Left, &()); @@ -219,7 +244,7 @@ impl Rope { pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { if point >= self.summary().lines_utf16 { - return self.summary().bytes; + return self.summary().len; } let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(); cursor.seek(&point, Bias::Left, &()); @@ -262,7 +287,7 @@ impl Rope { } offset } else { - self.summary().bytes + self.summary().len } } @@ -543,6 +568,34 @@ impl<'a> io::Read for Bytes<'a> { struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>); impl Chunk { + fn offset_to_offset_utf16(&self, target: usize) -> OffsetUtf16 { + let mut offset = 0; + let mut offset_utf16 = OffsetUtf16(0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset_utf16 + } + + fn offset_utf16_to_offset(&self, target: OffsetUtf16) -> usize { + let mut offset_utf16 = OffsetUtf16(0); + let mut offset = 0; + for ch in self.0.chars() { + if offset_utf16 >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset + } + fn offset_to_point(&self, target: usize) -> Point { let mut offset = 0; let mut point = Point::new(0, 0); @@ -748,7 +801,8 @@ impl sum_tree::Summary for ChunkSummary { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TextSummary { - pub bytes: usize, + pub len: usize, + pub len_utf16: usize, pub lines: Point, pub lines_utf16: PointUtf16, pub first_line_chars: u32, @@ -759,6 +813,7 @@ pub struct TextSummary { impl<'a> From<&'a str> for TextSummary { fn from(text: &'a str) -> Self { + let mut len_utf16 = 0; let mut lines = Point::new(0, 0); let mut lines_utf16 = PointUtf16::new(0, 0); let mut first_line_chars = 0; @@ -766,6 +821,8 @@ impl<'a> From<&'a str> for TextSummary { let mut longest_row = 0; let mut longest_row_chars = 0; for c in text.chars() { + len_utf16 += c.len_utf16(); + if c == '\n' { lines += Point::new(1, 0); lines_utf16 += PointUtf16::new(1, 0); @@ -787,7 +844,8 @@ impl<'a> From<&'a str> for TextSummary { } TextSummary { - bytes: text.len(), + len: text.len(), + len_utf16, lines, lines_utf16, first_line_chars, @@ -837,7 +895,8 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { self.last_line_chars = other.last_line_chars; } - self.bytes += other.bytes; + self.len += other.len; + self.len_utf16 += other.len_utf16; self.lines += other.lines; self.lines_utf16 += other.lines_utf16; } @@ -886,13 +945,29 @@ impl TextDimension for TextSummary { impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize { fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { - *self += summary.text.bytes; + *self += summary.text.len; } } impl TextDimension for usize { fn from_text_summary(summary: &TextSummary) -> Self { - summary.bytes + summary.len + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + self.0 += summary.text.len_utf16; + } +} + +impl TextDimension for OffsetUtf16 { + fn from_text_summary(summary: &TextSummary) -> Self { + Self(summary.len_utf16) } fn add_assign(&mut self, other: &Self) { @@ -1054,6 +1129,7 @@ mod tests { ); } + let mut offset_utf16 = OffsetUtf16(0); let mut point = Point::new(0, 0); let mut point_utf16 = PointUtf16::new(0, 0); for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) { @@ -1076,6 +1152,18 @@ mod tests { "point_utf16_to_offset({:?})", point_utf16 ); + assert_eq!( + actual.offset_to_offset_utf16(ix), + offset_utf16, + "offset_to_offset_utf16({:?})", + ix + ); + assert_eq!( + actual.offset_utf16_to_offset(offset_utf16), + ix, + "offset_utf16_to_offset({:?})", + offset_utf16 + ); if ch == '\n' { point += Point::new(1, 0); point_utf16 += PointUtf16::new(1, 0); @@ -1083,6 +1171,7 @@ mod tests { point.column += ch.len_utf8() as u32; point_utf16.column += ch.len_utf16() as u32; } + offset_utf16.0 += ch.len_utf16(); } let mut point_utf16 = PointUtf16::zero(); diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 4da9edd7351d3c67bf128d5d4b94a656217c04a6..3594f72984d9854179c5db5cdf1fa5eb43ad135b 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -247,7 +247,8 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(1..3), TextSummary { - bytes: 2, + len: 2, + len_utf16: 2, lines: Point::new(1, 0), lines_utf16: PointUtf16::new(1, 0), first_line_chars: 1, @@ -259,7 +260,8 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(1..12), TextSummary { - bytes: 11, + len: 11, + len_utf16: 11, lines: Point::new(3, 0), lines_utf16: PointUtf16::new(3, 0), first_line_chars: 1, @@ -271,7 +273,8 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(0..20), TextSummary { - bytes: 20, + len: 20, + len_utf16: 20, lines: Point::new(4, 1), lines_utf16: PointUtf16::new(4, 1), first_line_chars: 2, @@ -283,7 +286,8 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(0..22), TextSummary { - bytes: 22, + len: 22, + len_utf16: 22, lines: Point::new(4, 3), lines_utf16: PointUtf16::new(4, 3), first_line_chars: 2, @@ -295,7 +299,8 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(7..22), TextSummary { - bytes: 15, + len: 15, + len_utf16: 15, lines: Point::new(2, 3), lines_utf16: PointUtf16::new(2, 3), first_line_chars: 4, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 536b8329420628693f84dad0b9398de2577164f0..da4dcfe490a3e419f308153374105aefbcbc98e4 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2,6 +2,7 @@ mod anchor; pub mod locator; #[cfg(any(test, feature = "test-support"))] pub mod network; +mod offset_utf16; pub mod operation_queue; mod patch; mod point; @@ -20,6 +21,7 @@ use clock::ReplicaId; use collections::{HashMap, HashSet}; use lazy_static::lazy_static; use locator::Locator; +pub use offset_utf16::*; use operation_queue::OperationQueue; pub use patch::Patch; pub use point::*; @@ -1621,6 +1623,14 @@ impl BufferSnapshot { self.visible_text.point_utf16_to_point(point) } + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + self.visible_text.offset_utf16_to_offset(offset) + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + self.visible_text.offset_to_offset_utf16(offset) + } + pub fn offset_to_point(&self, offset: usize) -> Point { self.visible_text.offset_to_point(offset) } @@ -2423,6 +2433,12 @@ impl ToOffset for usize { } } +impl ToOffset for OffsetUtf16 { + fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.offset_utf16_to_offset(*self) + } +} + impl ToOffset for Anchor { fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { snapshot.summary_for_anchor(self) @@ -2491,6 +2507,28 @@ impl ToPointUtf16 for Point { } } +pub trait ToOffsetUtf16 { + fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16; +} + +impl ToOffsetUtf16 for Anchor { + fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + snapshot.summary_for_anchor(self) + } +} + +impl ToOffsetUtf16 for usize { + fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + snapshot.offset_to_offset_utf16(*self) + } +} + +impl ToOffsetUtf16 for OffsetUtf16 { + fn to_offset_utf16<'a>(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 { + *self + } +} + pub trait Clip { fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self; } From b02681ee8ab04ce5f4ad72d435d2059a8baca601 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 13:41:35 +0200 Subject: [PATCH 11/47] Treat NSTextInputClient ranges as UTF-16 --- crates/editor/src/editor.rs | 76 +++++++++++++++--------- crates/editor/src/multi_buffer/anchor.rs | 10 +++- crates/gpui/src/platform/mac/window.rs | 49 +++------------ crates/text/src/offset_utf16.rs | 50 ++++++++++++++++ 4 files changed, 115 insertions(+), 70 deletions(-) create mode 100644 crates/text/src/offset_utf16.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba5ae4eefea16f54970c577aa740130db827aa53..babc6f3ca4b6697e8375b29fb59509cc968870f8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -39,15 +39,15 @@ pub use items::MAX_TAB_TITLE_LEN; pub use language::{char_kind, CharKind}; use language::{ BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, - IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, + IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::LinkGoToDefinitionState; -use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; +use multi_buffer::{MultiBufferChunks, ToOffsetUtf16}; use ordered_float::OrderedFloat; use project::{LocationLink, Project, ProjectPath, ProjectTransaction}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; @@ -5877,27 +5877,33 @@ impl View for Editor { context } - fn text_for_range(&self, range: Range, cx: &AppContext) -> Option { + fn text_for_range(&self, range_utf16: Range, cx: &AppContext) -> Option { Some( self.buffer .read(cx) .read(cx) - .text_for_range(range) + .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)) .collect(), ) } fn selected_text_range(&self, cx: &AppContext) -> Option> { - Some(self.selections.newest(cx).range()) + let range = self.selections.newest::(cx).range(); + Some(range.start.0..range.end.0) } - fn set_selected_text_range(&mut self, range: Range, cx: &mut ViewContext) { - self.change_selections(None, cx, |selections| selections.select_ranges([range])); + fn set_selected_text_range(&mut self, range_utf16: Range, cx: &mut ViewContext) { + self.change_selections(None, cx, |selections| { + selections.select_ranges([OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)]) + }); } fn marked_text_range(&self, cx: &AppContext) -> Option> { let range = self.text_highlights::(cx)?.1.get(0)?; - Some(range.to_offset(&*self.buffer.read(cx).read(cx))) + let snapshot = self.buffer.read(cx).read(cx); + let range_utf16 = + range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot); + Some(range_utf16.start.0..range_utf16.end.0) } fn unmark_text(&mut self, cx: &mut ViewContext) { @@ -5906,34 +5912,36 @@ impl View for Editor { fn replace_text_in_range( &mut self, - range: Option>, + range_utf16: Option>, text: &str, cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - if let Some(range) = range { - this.set_selected_text_range(range, cx); + if let Some(range_utf16) = range_utf16.or_else(|| this.marked_text_range(cx)) { + this.set_selected_text_range(range_utf16, cx); } this.handle_input(text, cx); + this.unmark_text(cx); }); } fn replace_and_mark_text_in_range( &mut self, - range: Option>, + range_utf16: Option>, text: &str, - new_selected_range: Option>, + new_selected_range_utf16: Option>, cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - let range = range.or_else(|| { - let ranges = this.text_highlights::(cx)?.1; - let range = ranges.first()?; - let snapshot = this.buffer.read(cx).read(cx); - Some(range.to_offset(&*snapshot)) - }); - if let Some(range) = range { - this.set_selected_text_range(range, cx); + if let Some(mut marked_range) = this.marked_text_range(cx) { + if let Some(relative_range_utf16) = range_utf16.as_ref() { + marked_range.end = marked_range.start + relative_range_utf16.end; + marked_range.start += relative_range_utf16.start; + } + + this.set_selected_text_range(marked_range, cx); + } else if let Some(range_utf16) = range_utf16 { + this.set_selected_text_range(range_utf16, cx); } let selection = this.selections.newest_anchor(); @@ -5941,16 +5949,28 @@ impl View for Editor { let snapshot = this.buffer.read(cx).read(cx); selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot) }; - this.highlight_text::( - vec![marked_range], - this.style(cx).composition_mark, - cx, - ); + + if text.is_empty() { + this.unmark_text(cx); + } else { + this.highlight_text::( + vec![marked_range.clone()], + this.style(cx).composition_mark, + cx, + ); + } this.handle_input(text, cx); - if let Some(new_selected_range) = new_selected_range { - this.set_selected_text_range(new_selected_range, cx); + if let Some(new_selected_range) = new_selected_range_utf16 { + let snapshot = this.buffer.read(cx).read(cx); + let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; + drop(snapshot); + this.set_selected_text_range( + insertion_start + new_selected_range.start + ..insertion_start + new_selected_range.end, + cx, + ); } }); } diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index df080f074cdd5d1295b6a0e4729939819ec71bb0..1340ea814dfe746b3251415ee463d59e6486760b 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -1,10 +1,10 @@ -use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; +use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint}; use std::{ cmp::Ordering, ops::{Range, Sub}, }; use sum_tree::Bias; -use text::{rope::TextDimension, Point}; +use text::{rope::TextDimension, OffsetUtf16, Point}; #[derive(Clone, Eq, PartialEq, Debug, Hash)] pub struct Anchor { @@ -89,6 +89,12 @@ impl ToOffset for Anchor { } } +impl ToOffsetUtf16 for Anchor { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + self.summary(snapshot) + } +} + impl ToPoint for Anchor { fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { self.summary(snapshot) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 097a53b260ab1045a3fae5b93955ab6f7c7c5ab6..3c4c2804878464a057a69d1a5f951f61309e4c13 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1139,62 +1139,31 @@ extern "C" fn set_marked_text( .to_str() .unwrap(); - let window_state = get_window_state(this); - let mut window_state = window_state.borrow_mut(); - if let Some(pending) = window_state.pending_key_event.as_mut() { - pending.set_marked_text = Some((text.to_string(), selected_range, replacement_range)); - } else { - drop(window_state); - with_input_handler(this, |input_handler| { - input_handler.replace_and_mark_text_in_range( - replacement_range, - text, - selected_range, - ); - }); - } + with_input_handler(this, |input_handler| { + input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range); + }); } } extern "C" fn unmark_text(this: &Object, _: Sel) { - println!("unmark_text"); - let window_state = unsafe { get_window_state(this) }; - let mut window_state = window_state.borrow_mut(); - if let Some(pending) = window_state.pending_key_event.as_mut() { - pending.unmark_text = true; - pending.set_marked_text.take(); - } else { - drop(window_state); - with_input_handler(this, |input_handler| input_handler.finish_composition()); - } + with_input_handler(this, |input_handler| input_handler.finish_composition()); } extern "C" fn attributed_substring_for_proposed_range( this: &Object, _: Sel, range: NSRange, - actual_range: *mut c_void, + _actual_range: *mut c_void, ) -> id { + println!("attributed_substring_for_proposed_range({:?})", range); with_input_handler(this, |input_handler| { - let actual_range = actual_range as *mut NSRange; - if !actual_range.is_null() { - unsafe { *actual_range = NSRange::invalid() }; - } - - let requested_range = range.to_range()?; - if requested_range.is_empty() { - return None; - } - - let selected_range = input_handler.selected_text_range()?; - let intersection = cmp::max(requested_range.start, selected_range.start) - ..cmp::min(requested_range.end, selected_range.end); - if intersection.start >= intersection.end { + let range = range.to_range()?; + if range.is_empty() { return None; } + let selected_text = input_handler.text_for_range(range)?; unsafe { - let selected_text = input_handler.text_for_range(intersection)?; let string: id = msg_send![class!(NSAttributedString), alloc]; let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; Some(string) diff --git a/crates/text/src/offset_utf16.rs b/crates/text/src/offset_utf16.rs new file mode 100644 index 0000000000000000000000000000000000000000..9a52b3c3f900788262696fe136dcc7d5995b3a5a --- /dev/null +++ b/crates/text/src/offset_utf16.rs @@ -0,0 +1,50 @@ +use std::ops::{Add, AddAssign, Sub}; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub struct OffsetUtf16(pub usize); + +impl<'a> Add<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn add(self, other: &'a Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl Add for OffsetUtf16 { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl<'a> Sub<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn sub(self, other: &'a Self) -> Self::Output { + debug_assert!(*other <= self); + Self(self.0 - other.0) + } +} + +impl Sub for OffsetUtf16 { + type Output = OffsetUtf16; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + Self(self.0 - other.0) + } +} + +impl<'a> AddAssign<&'a Self> for OffsetUtf16 { + fn add_assign(&mut self, other: &'a Self) { + self.0 += other.0; + } +} + +impl AddAssign for OffsetUtf16 { + fn add_assign(&mut self, other: Self) { + self.0 += other.0; + } +} From 481078ae2282782ededa9d33f5d535da8dbfbc2d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 13:41:55 +0200 Subject: [PATCH 12/47] Restructure IME composition to not follow Chromium so closely --- crates/gpui/src/platform/mac/window.rs | 145 ++++++++----------------- 1 file changed, 48 insertions(+), 97 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 3c4c2804878464a057a69d1a5f951f61309e4c13..897bcc25ee44935ae56d272a7320f6ec2c253e5d 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -37,7 +37,6 @@ use smol::Timer; use std::{ any::Any, cell::{Cell, RefCell}, - cmp, convert::TryInto, ffi::{c_void, CStr}, mem, @@ -274,7 +273,7 @@ struct WindowState { should_close_callback: Option bool>>, close_callback: Option>, input_handler: Option>, - pending_key_event: Option, + pending_keydown_event: Option, synthetic_drag_counter: usize, executor: Rc, scene_to_render: Option, @@ -286,13 +285,6 @@ struct WindowState { previous_modifiers_changed_event: Option, } -#[derive(Default, Debug)] -struct PendingKeyEvent { - set_marked_text: Option<(String, Option>, Option>)>, - unmark_text: bool, - insert_text: Option, -} - impl Window { pub fn open( id: usize, @@ -369,7 +361,7 @@ impl Window { close_callback: None, activate_callback: None, input_handler: None, - pending_key_event: None, + pending_keydown_event: None, synthetic_drag_counter: 0, executor, scene_to_render: Default::default(), @@ -705,7 +697,7 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; if let Some(event) = event { - let mut event = match event { + window_state_borrow.pending_keydown_event = match event { Event::KeyDown(event) => { let keydown = (event.keystroke.clone(), event.input.clone()); // Ignore events from held-down keys after some of the initially-pressed keys @@ -718,97 +710,30 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> window_state_borrow.last_fresh_keydown = Some(keydown); } - event + Some(event) } _ => return NO, }; - - // TODO: handle "live conversion" - window_state_borrow.pending_key_event = Some(Default::default()); drop(window_state_borrow); - let had_marked_text = - with_input_handler(this, |input_handler| input_handler.marked_text_range()) - .flatten() - .is_some(); - // TODO: - // Since Mac Eisu Kana keys cannot be handled by interpretKeyEvents to enable/ - // disable an IME, we need to pass the event to processInputKeyBindings. - // processInputKeyBindings is available at least on 10.11-11.0. - // if (keyCode == kVK_JIS_Eisu || keyCode == kVK_JIS_Kana) { - // if ([NSTextInputContext - // respondsToSelector:@selector(processInputKeyBindings:)]) { - // [NSTextInputContext performSelector:@selector(processInputKeyBindings:) - // withObject:theEvent]; - // } - // } else { unsafe { let input_context: id = msg_send![this, inputContext]; let _: BOOL = msg_send![input_context, handleEvent: native_event]; } - // } - - let pending_event = window_state.borrow_mut().pending_key_event.take().unwrap(); - - dbg!(&pending_event); - - let mut inserted_text = false; - let has_marked_text = pending_event.set_marked_text.is_some() - || with_input_handler(this, |input_handler| input_handler.marked_text_range()) - .flatten() - .is_some(); - if let Some(text) = pending_event.insert_text.as_ref() { - if !text.is_empty() { - with_input_handler(this, |input_handler| { - input_handler.replace_text_in_range(None, &text) - }); - inserted_text = true; - } - } - - with_input_handler(this, |input_handler| { - if let Some((text, new_selected_range, replacement_range)) = - pending_event.set_marked_text - { - input_handler.replace_and_mark_text_in_range( - replacement_range, - &text, - new_selected_range, - ) - } else if had_marked_text && !has_marked_text && !inserted_text { - if pending_event.unmark_text { - input_handler.finish_composition(); - } else { - input_handler.cancel_composition(); - } - } - }); - if has_marked_text { - YES - } else { - let mut handled = false; - let mut window_state_borrow = window_state.borrow_mut(); + let mut handled = false; + let mut window_state_borrow = window_state.borrow_mut(); + if let Some(event) = window_state_borrow.pending_keydown_event.take() { if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); - - if inserted_text { - handled = true; - } else { - if let Some(text) = pending_event.insert_text { - if text.chars().count() == 1 { - event.keystroke.key = text; - } - } - - handled = callback(Event::KeyDown(event.clone())); - } - + handled = callback(Event::KeyDown(event.clone())); window_state.borrow_mut().event_callback = Some(callback); } - - handled as BOOL + } else { + handled = true; } + + handled as BOOL } else { NO } @@ -1090,6 +1015,11 @@ extern "C" fn first_rect_for_character_range(_: &Object, _: Sel, _: NSRange, _: extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { unsafe { + let window_state = get_window_state(this); + let mut window_state_borrow = window_state.borrow_mut(); + let pending_keydown_event = window_state_borrow.pending_keydown_event.take(); + drop(window_state_borrow); + let is_attributed_string: BOOL = msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; let text: id = if is_attributed_string == YES { @@ -1100,20 +1030,36 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS let text = CStr::from_ptr(text.UTF8String() as *mut c_char) .to_str() .unwrap(); + let replacement_range = replacement_range.to_range(); - let window_state = get_window_state(this); - let mut window_state = window_state.borrow_mut(); - if window_state.pending_key_event.is_some() && !replacement_range.is_valid() { - window_state.pending_key_event.as_mut().unwrap().insert_text = Some(text.to_string()); - drop(window_state); - } else { - drop(window_state); + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + + if is_composing || text.chars().count() > 1 || pending_keydown_event.is_none() { with_input_handler(this, |input_handler| { - input_handler.replace_text_in_range(replacement_range.to_range(), text); + input_handler.replace_text_in_range(replacement_range, text) }); - } + } else { + let mut pending_keydown_event = pending_keydown_event.unwrap(); + pending_keydown_event.keystroke.key = text.into(); + let mut window_state_borrow = window_state.borrow_mut(); + let event_callback = window_state_borrow.event_callback.take(); + drop(window_state_borrow); - with_input_handler(this, |input_handler| input_handler.unmark_text()); + let mut handled = false; + if let Some(mut event_callback) = event_callback { + handled = event_callback(Event::KeyDown(pending_keydown_event)); + window_state.borrow_mut().event_callback = Some(event_callback); + } + + if !handled { + with_input_handler(this, |input_handler| { + input_handler.replace_text_in_range(replacement_range, text) + }); + } + } } } @@ -1126,6 +1072,11 @@ extern "C" fn set_marked_text( ) { println!("set_marked_text"); unsafe { + get_window_state(this) + .borrow_mut() + .pending_keydown_event + .take(); + let is_attributed_string: BOOL = msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; let text: id = if is_attributed_string == YES { From a4ceae3cf286ece23ab2fd31a5006088c1db5bf8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 13:48:56 +0200 Subject: [PATCH 13/47] Remove unused `{cancel,finish}_composition` methods --- crates/gpui/src/app.rs | 12 ------------ crates/gpui/src/platform.rs | 2 -- crates/gpui/src/platform/mac/window.rs | 2 +- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a049f5753fba9140145ce662c2b6966f74f3861b..15f5bad570b4ae8d6bc5312b2e8ff9c9a7077d4e 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -445,18 +445,6 @@ impl InputHandler for WindowInputHandler { ); }); } - - // TODO - do these need to be handled separately? - - fn cancel_composition(&mut self) { - println!("cancel_composition()"); - self.unmark_text(); - } - - fn finish_composition(&mut self) { - println!("finish_composition()"); - self.unmark_text(); - } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 22eaa00540166f8a0217e2212b123087cf776d4b..be121ff5b7a307f07c6165a60fb742c51370a6f2 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -102,8 +102,6 @@ pub trait InputHandler { ); fn marked_text_range(&self) -> Option>; fn unmark_text(&mut self); - fn cancel_composition(&mut self); - fn finish_composition(&mut self); } pub trait Window: WindowContext { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 897bcc25ee44935ae56d272a7320f6ec2c253e5d..f7fdc700d26c883472e8e1dd724b11f93a809932 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1097,7 +1097,7 @@ extern "C" fn set_marked_text( } extern "C" fn unmark_text(this: &Object, _: Sel) { - with_input_handler(this, |input_handler| input_handler.finish_composition()); + with_input_handler(this, |input_handler| input_handler.unmark_text()); } extern "C" fn attributed_substring_for_proposed_range( From f170582c26c06e8ec8e263e5db168f42b2db650d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 13:51:31 +0200 Subject: [PATCH 14/47] Remove `println` statements from NSTextInputClient protocol functions --- crates/gpui/src/platform/mac/window.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index f7fdc700d26c883472e8e1dd724b11f93a809932..cc23193830172d7c6d065a65b88890b111a6013f 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1070,7 +1070,6 @@ extern "C" fn set_marked_text( selected_range: NSRange, replacement_range: NSRange, ) { - println!("set_marked_text"); unsafe { get_window_state(this) .borrow_mut() @@ -1106,7 +1105,6 @@ extern "C" fn attributed_substring_for_proposed_range( range: NSRange, _actual_range: *mut c_void, ) -> id { - println!("attributed_substring_for_proposed_range({:?})", range); with_input_handler(this, |input_handler| { let range = range.to_range()?; if range.is_empty() { From 101a0663d3b50a0e1c590929c4154d0e7453aa0c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 14:29:27 +0200 Subject: [PATCH 15/47] Remove `input` from `KeyDownEvent` --- crates/editor/src/element.rs | 17 +---------- crates/editor/src/test.rs | 8 +----- crates/gpui/src/app.rs | 19 +++---------- crates/gpui/src/platform/event.rs | 2 -- crates/gpui/src/platform/mac/event.rs | 39 ++++---------------------- crates/gpui/src/platform/mac/window.rs | 10 +++---- 6 files changed, 16 insertions(+), 79 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ee61c6b926229e0b46ee64e6abf21561d8350ed9..589f7b4c5222430d8b623314a775dcdd669c3c9b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -24,7 +24,7 @@ use gpui::{ json::{self, ToJson}, platform::CursorStyle, text_layout::{self, Line, RunStyle, TextLayoutCache}, - AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent, + AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, LayoutContext, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext, WeakViewHandle, @@ -278,21 +278,6 @@ impl EditorElement { true } - fn key_down(&self, input: Option<&str>, cx: &mut EventContext) -> bool { - let view = self.view.upgrade(cx.app).unwrap(); - - if view.is_focused(cx.app) { - if let Some(input) = input { - cx.dispatch_action(Input(input.to_string())); - true - } else { - false - } - } else { - false - } - } - fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool { cx.dispatch_action(CmdChanged { cmd_down: cmd }); false diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index dd05a14bd67bbb4e82d1f33a9d1b95be663cc44b..df9b48e6481e6a5c1dea8846ee1aa10a41ba50f8 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -141,13 +141,7 @@ impl<'a> EditorTestContext<'a> { pub fn simulate_keystroke(&mut self, keystroke_text: &str) { let keystroke = Keystroke::parse(keystroke_text).unwrap(); - let input = if keystroke.modified() { - None - } else { - Some(keystroke.key.clone()) - }; - self.cx - .dispatch_keystroke(self.window_id, keystroke, input, false); + self.cx.dispatch_keystroke(self.window_id, keystroke, false); } pub fn simulate_keystrokes(&mut self, keystroke_texts: [&str; COUNT]) { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 15f5bad570b4ae8d6bc5312b2e8ff9c9a7077d4e..d7b091886bd29a8db27a7997440bc2fe0b2268f6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -498,13 +498,7 @@ impl TestAppContext { self.cx.borrow_mut().dispatch_global_action(action); } - pub fn dispatch_keystroke( - &mut self, - window_id: usize, - keystroke: Keystroke, - input: Option, - is_held: bool, - ) { + pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { self.cx.borrow_mut().update(|cx| { let presenter = cx .presenters_and_platform_windows @@ -515,14 +509,9 @@ impl TestAppContext { let dispatch_path = presenter.borrow().dispatch_path(cx.as_ref()); if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) { - presenter.borrow_mut().dispatch_event( - Event::KeyDown(KeyDownEvent { - keystroke, - input, - is_held, - }), - cx, - ); + presenter + .borrow_mut() + .dispatch_event(Event::KeyDown(KeyDownEvent { keystroke, is_held }), cx); } }); } diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 90b5d21fc2f7e1f8ec9092c598ca4a4c58cde658..6815619d1cc277f0d584c937b6f78164fc55723e 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -3,14 +3,12 @@ use crate::{geometry::vector::Vector2F, keymap::Keystroke}; #[derive(Clone, Debug)] pub struct KeyDownEvent { pub keystroke: Keystroke, - pub input: Option, pub is_held: bool, } #[derive(Clone, Debug)] pub struct KeyUpEvent { pub keystroke: Keystroke, - pub input: Option, } #[derive(Clone, Debug)] diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index b6cc066b2e255a84cc08ca7d79f6bffc16ebc23e..3840ef2eeb35b5f55005fa17d2d953d36108fba9 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -83,10 +83,8 @@ impl Event { let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); - - let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?; + let unmodified_chars = get_key_text(native_event)?; Some(Self::KeyDown(KeyDownEvent { keystroke: Keystroke { ctrl, @@ -95,7 +93,6 @@ impl Event { cmd, key: unmodified_chars.into(), }, - input, is_held: native_event.isARepeat() == YES, })) } @@ -105,10 +102,7 @@ impl Event { let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); - - let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?; - + let unmodified_chars = get_key_text(native_event)?; Some(Self::KeyUp(KeyUpEvent { keystroke: Keystroke { ctrl, @@ -117,7 +111,6 @@ impl Event { cmd, key: unmodified_chars.into(), }, - input, })) } NSEventType::NSLeftMouseDown @@ -238,27 +231,18 @@ impl Event { } } -unsafe fn get_key_text( - native_event: id, - cmd: bool, - ctrl: bool, - function: bool, -) -> Option<(&'static str, Option)> { +unsafe fn get_key_text(native_event: id) -> Option<&'static str> { let unmodified_chars = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() .unwrap(); - let mut input = None; let first_char = unmodified_chars.chars().next()?; use cocoa::appkit::*; #[allow(non_upper_case_globals)] let unmodified_chars = match first_char as u16 { - SPACE_KEY => { - input = Some(" ".to_string()); - "space" - } + SPACE_KEY => "space", BACKSPACE_KEY => "backspace", ENTER_KEY | NUMPAD_ENTER_KEY => "enter", ESCAPE_KEY => "escape", @@ -284,19 +268,8 @@ unsafe fn get_key_text( NSF10FunctionKey => "f10", NSF11FunctionKey => "f11", NSF12FunctionKey => "f12", - - _ => { - if !cmd && !ctrl && !function { - input = Some( - CStr::from_ptr(native_event.characters().UTF8String() as *mut c_char) - .to_str() - .unwrap() - .into(), - ); - } - unmodified_chars - } + _ => unmodified_chars, }; - Some((unmodified_chars, input)) + Some(unmodified_chars) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index cc23193830172d7c6d065a65b88890b111a6013f..ec05d1c8c563c419b40ef36d9a6c09b81921f0fc 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -279,7 +279,7 @@ struct WindowState { scene_to_render: Option, renderer: Renderer, command_queue: metal::CommandQueue, - last_fresh_keydown: Option<(Keystroke, Option)>, + last_fresh_keydown: Option, layer: id, traffic_light_position: Option, previous_modifiers_changed_event: Option, @@ -699,7 +699,7 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> if let Some(event) = event { window_state_borrow.pending_keydown_event = match event { Event::KeyDown(event) => { - let keydown = (event.keystroke.clone(), event.input.clone()); + let keydown = event.keystroke.clone(); // Ignore events from held-down keys after some of the initially-pressed keys // were released. if event.is_held { @@ -812,21 +812,19 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { let window_state = unsafe { get_window_state(this) }; let mut window_state_borrow = window_state.as_ref().borrow_mut(); - let chars = ".".to_string(); let keystroke = Keystroke { cmd: true, ctrl: false, alt: false, shift: false, - key: chars.clone(), + key: ".".into(), }; let event = Event::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), - input: Some(chars.clone()), is_held: false, }); - window_state_borrow.last_fresh_keydown = Some((keystroke, Some(chars))); + window_state_borrow.last_fresh_keydown = Some(keystroke); if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); callback(event); From b2f2c5b055f526949dc0609d6ed731369c6a6e58 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 14:35:55 +0200 Subject: [PATCH 16/47] Handle keydown event even when `charactersIgnoringModifiers` is empty This allows the input system to work in the presence of dead keys. --- crates/gpui/src/platform/mac/event.rs | 55 +++++++++++++------------- crates/gpui/src/platform/mac/window.rs | 22 +++++------ 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 3840ef2eeb35b5f55005fa17d2d953d36108fba9..de018fc679f0755a8fe77027f7935ffdac646083 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -237,37 +237,36 @@ unsafe fn get_key_text(native_event: id) -> Option<&'static str> { .to_str() .unwrap(); - let first_char = unmodified_chars.chars().next()?; + let first_char = unmodified_chars.chars().next().map(|ch| ch as u16); use cocoa::appkit::*; #[allow(non_upper_case_globals)] - let unmodified_chars = match first_char as u16 { - SPACE_KEY => "space", - BACKSPACE_KEY => "backspace", - ENTER_KEY | NUMPAD_ENTER_KEY => "enter", - ESCAPE_KEY => "escape", - TAB_KEY => "tab", - SHIFT_TAB_KEY => "tab", - - NSUpArrowFunctionKey => "up", - NSDownArrowFunctionKey => "down", - NSLeftArrowFunctionKey => "left", - NSRightArrowFunctionKey => "right", - NSPageUpFunctionKey => "pageup", - NSPageDownFunctionKey => "pagedown", - NSDeleteFunctionKey => "delete", - NSF1FunctionKey => "f1", - NSF2FunctionKey => "f2", - NSF3FunctionKey => "f3", - NSF4FunctionKey => "f4", - NSF5FunctionKey => "f5", - NSF6FunctionKey => "f6", - NSF7FunctionKey => "f7", - NSF8FunctionKey => "f8", - NSF9FunctionKey => "f9", - NSF10FunctionKey => "f10", - NSF11FunctionKey => "f11", - NSF12FunctionKey => "f12", + let unmodified_chars = match first_char { + Some(SPACE_KEY) => "space", + Some(BACKSPACE_KEY) => "backspace", + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", + Some(ESCAPE_KEY) => "escape", + Some(TAB_KEY) => "tab", + Some(SHIFT_TAB_KEY) => "tab", + Some(NSUpArrowFunctionKey) => "up", + Some(NSDownArrowFunctionKey) => "down", + Some(NSLeftArrowFunctionKey) => "left", + Some(NSRightArrowFunctionKey) => "right", + Some(NSPageUpFunctionKey) => "pageup", + Some(NSPageDownFunctionKey) => "pagedown", + Some(NSDeleteFunctionKey) => "delete", + Some(NSF1FunctionKey) => "f1", + Some(NSF2FunctionKey) => "f2", + Some(NSF3FunctionKey) => "f3", + Some(NSF4FunctionKey) => "f4", + Some(NSF5FunctionKey) => "f5", + Some(NSF6FunctionKey) => "f6", + Some(NSF7FunctionKey) => "f7", + Some(NSF8FunctionKey) => "f8", + Some(NSF9FunctionKey) => "f9", + Some(NSF10FunctionKey) => "f10", + Some(NSF11FunctionKey) => "f11", + Some(NSF12FunctionKey) => "f12", _ => unmodified_chars, }; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index ec05d1c8c563c419b40ef36d9a6c09b81921f0fc..c8d8fe30589f50dfcc61171c92a669243337c5dc 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -273,7 +273,7 @@ struct WindowState { should_close_callback: Option bool>>, close_callback: Option>, input_handler: Option>, - pending_keydown_event: Option, + pending_key_down_event: Option, synthetic_drag_counter: usize, executor: Rc, scene_to_render: Option, @@ -361,7 +361,7 @@ impl Window { close_callback: None, activate_callback: None, input_handler: None, - pending_keydown_event: None, + pending_key_down_event: None, synthetic_drag_counter: 0, executor, scene_to_render: Default::default(), @@ -697,7 +697,7 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; if let Some(event) = event { - window_state_borrow.pending_keydown_event = match event { + window_state_borrow.pending_key_down_event = match event { Event::KeyDown(event) => { let keydown = event.keystroke.clone(); // Ignore events from held-down keys after some of the initially-pressed keys @@ -723,10 +723,10 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> let mut handled = false; let mut window_state_borrow = window_state.borrow_mut(); - if let Some(event) = window_state_borrow.pending_keydown_event.take() { + if let Some(event) = window_state_borrow.pending_key_down_event.take() { if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); - handled = callback(Event::KeyDown(event.clone())); + handled = callback(Event::KeyDown(event)); window_state.borrow_mut().event_callback = Some(callback); } } else { @@ -745,7 +745,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let mut window_state_borrow = window_state.as_ref().borrow_mut(); let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; - if let Some(event) = event { match &event { Event::MouseMoved( @@ -1015,7 +1014,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS unsafe { let window_state = get_window_state(this); let mut window_state_borrow = window_state.borrow_mut(); - let pending_keydown_event = window_state_borrow.pending_keydown_event.take(); + let pending_key_down_event = window_state_borrow.pending_key_down_event.take(); drop(window_state_borrow); let is_attributed_string: BOOL = @@ -1035,20 +1034,19 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS .flatten() .is_some(); - if is_composing || text.chars().count() > 1 || pending_keydown_event.is_none() { + if is_composing || text.chars().count() > 1 || pending_key_down_event.is_none() { with_input_handler(this, |input_handler| { input_handler.replace_text_in_range(replacement_range, text) }); } else { - let mut pending_keydown_event = pending_keydown_event.unwrap(); - pending_keydown_event.keystroke.key = text.into(); + let pending_key_down_event = pending_key_down_event.unwrap(); let mut window_state_borrow = window_state.borrow_mut(); let event_callback = window_state_borrow.event_callback.take(); drop(window_state_borrow); let mut handled = false; if let Some(mut event_callback) = event_callback { - handled = event_callback(Event::KeyDown(pending_keydown_event)); + handled = event_callback(Event::KeyDown(pending_key_down_event)); window_state.borrow_mut().event_callback = Some(event_callback); } @@ -1071,7 +1069,7 @@ extern "C" fn set_marked_text( unsafe { get_window_state(this) .borrow_mut() - .pending_keydown_event + .pending_key_down_event .take(); let is_attributed_string: BOOL = From 3d6c257551f974a6687626960d4e59932c874249 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 14:39:22 +0200 Subject: [PATCH 17/47] :art: --- crates/gpui/src/platform/mac/window.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index c8d8fe30589f50dfcc61171c92a669243337c5dc..d50f03b18d7d813ecdb890ca4401f917f6db7eab 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1039,14 +1039,11 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS input_handler.replace_text_in_range(replacement_range, text) }); } else { - let pending_key_down_event = pending_key_down_event.unwrap(); - let mut window_state_borrow = window_state.borrow_mut(); - let event_callback = window_state_borrow.event_callback.take(); - drop(window_state_borrow); - let mut handled = false; + + let event_callback = window_state.borrow_mut().event_callback.take(); if let Some(mut event_callback) = event_callback { - handled = event_callback(Event::KeyDown(pending_key_down_event)); + handled = event_callback(Event::KeyDown(pending_key_down_event.unwrap())); window_state.borrow_mut().event_callback = Some(event_callback); } From 3c5d7e001ee41c1651506d6ac59a9daa81466049 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 15:53:21 +0200 Subject: [PATCH 18/47] Always mark keydown events as handled to suppress beep --- crates/gpui/src/platform/mac/window.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index d50f03b18d7d813ecdb890ca4401f917f6db7eab..2d03e89c2c43bb674fe0fab58b9e125349a8c4e3 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -721,19 +721,16 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> let _: BOOL = msg_send![input_context, handleEvent: native_event]; } - let mut handled = false; let mut window_state_borrow = window_state.borrow_mut(); if let Some(event) = window_state_borrow.pending_key_down_event.take() { if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); - handled = callback(Event::KeyDown(event)); + callback(Event::KeyDown(event)); window_state.borrow_mut().event_callback = Some(callback); } - } else { - handled = true; } - handled as BOOL + YES } else { NO } From 97ce3998ec2dded535b64f627819e4e568e18be2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 17:35:40 +0200 Subject: [PATCH 19/47] Position IME input according to where the selection is rendered --- crates/editor/src/display_map.rs | 8 ++- crates/editor/src/element.rs | 39 ++++++++++++++- crates/gpui/examples/text.rs | 15 +++++- crates/gpui/src/app.rs | 8 +++ crates/gpui/src/elements.rs | 49 ++++++++++++++++++- crates/gpui/src/elements/align.rs | 16 +++++- crates/gpui/src/elements/canvas.rs | 13 +++++ crates/gpui/src/elements/constrained_box.rs | 18 ++++++- crates/gpui/src/elements/container.rs | 15 ++++++ crates/gpui/src/elements/empty.rs | 15 ++++++ crates/gpui/src/elements/event_handler.rs | 20 ++++++-- crates/gpui/src/elements/expanded.rs | 18 ++++++- crates/gpui/src/elements/flex.rs | 29 ++++++++++- crates/gpui/src/elements/hook.rs | 15 ++++++ crates/gpui/src/elements/image.rs | 15 +++++- crates/gpui/src/elements/keystroke_label.rs | 12 +++++ crates/gpui/src/elements/label.rs | 15 ++++++ crates/gpui/src/elements/list.rs | 46 +++++++++++++++++ .../gpui/src/elements/mouse_event_handler.rs | 18 +++++-- crates/gpui/src/elements/overlay.rs | 15 ++++++ crates/gpui/src/elements/stack.rs | 18 +++++++ crates/gpui/src/elements/svg.rs | 15 +++++- crates/gpui/src/elements/text.rs | 18 ++++++- crates/gpui/src/elements/tooltip.rs | 14 ++++++ crates/gpui/src/elements/uniform_list.rs | 16 ++++++ crates/gpui/src/gpui.rs | 3 +- crates/gpui/src/platform.rs | 7 +-- crates/gpui/src/platform/mac/window.rs | 29 ++++++++++- crates/gpui/src/presenter.rs | 46 ++++++++++++++++- crates/terminal/src/terminal_element.rs | 12 +++++ crates/workspace/src/workspace.rs | 13 +++++ 31 files changed, 563 insertions(+), 27 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 0a0ac4a0e0970513b0cef1d0fcadde41967eed56..6f50da6a7d28a51b67c0711b6dfe35ea1ab0adf7 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -11,7 +11,7 @@ use gpui::{ fonts::{FontId, HighlightStyle}, Entity, ModelContext, ModelHandle, }; -use language::{Point, Subscription as BufferSubscription}; +use language::{OffsetUtf16, Point, Subscription as BufferSubscription}; use settings::Settings; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; @@ -549,6 +549,12 @@ impl ToDisplayPoint for usize { } } +impl ToDisplayPoint for OffsetUtf16 { + fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { + self.to_offset(&map.buffer_snapshot).to_display_point(map) + } +} + impl ToDisplayPoint for Point { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { map.point_to_display_point(*self, Bias::Left) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 589f7b4c5222430d8b623314a775dcdd669c3c9b..951796b6bfbdf829c7ee55ebb8e7085f31494318 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -30,7 +30,7 @@ use gpui::{ WeakViewHandle, }; use json::json; -use language::{Bias, DiagnosticSeverity, Selection}; +use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection}; use project::ProjectPath; use settings::Settings; use smallvec::SmallVec; @@ -1517,6 +1517,43 @@ impl Element for EditorElement { } } + fn rect_for_text_range( + &self, + range_utf16: Range, + bounds: RectF, + _: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + let text_bounds = RectF::new( + bounds.origin() + vec2f(layout.gutter_size.x(), 0.0), + layout.text_size, + ); + let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.); + let scroll_position = layout.snapshot.scroll_position(); + let start_row = scroll_position.y() as u32; + let scroll_top = scroll_position.y() * layout.line_height; + let scroll_left = scroll_position.x() * layout.em_width; + + let range_start = + OffsetUtf16(range_utf16.start).to_display_point(&layout.snapshot.display_snapshot); + if range_start.row() < start_row { + return None; + } + + let line = layout + .line_layouts + .get((range_start.row() - start_row) as usize)?; + let range_start_x = line.x_for_index(range_start.column() as usize); + let range_start_y = range_start.row() as f32 * layout.line_height; + Some(RectF::new( + content_origin + vec2f(range_start_x, range_start_y + layout.line_height) + - vec2f(scroll_left, scroll_top), + vec2f(layout.em_width, layout.line_height), + )) + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 74e2c2c2eaa94bbd49508dae5e0de38f478599c0..8e0153fe38792e034f028a8582f07ac8815362dd 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -2,11 +2,12 @@ use gpui::{ color::Color, fonts::{Properties, Weight}, text_layout::RunStyle, - DebugContext, Element as _, Quad, + DebugContext, Element as _, MeasurementContext, Quad, }; use log::LevelFilter; use pathfinder_geometry::rect::RectF; use simplelog::SimpleLogger; +use std::ops::Range; fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); @@ -112,6 +113,18 @@ impl gpui::Element for TextElement { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d7b091886bd29a8db27a7997440bc2fe0b2268f6..c098927f4a96caec82bdea9aa393c47389624fe4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3,6 +3,7 @@ pub mod action; use crate::{ elements::ElementBox, executor::{self, Task}, + geometry::rect::RectF, keymap::{self, Binding, Keystroke}, platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, @@ -445,6 +446,13 @@ impl InputHandler for WindowInputHandler { ); }); } + + fn rect_for_range(&self, range_utf16: Range) -> Option { + let app = self.app.borrow(); + let (presenter, _) = app.presenters_and_platform_windows.get(&self.window_id)?; + let presenter = presenter.borrow(); + presenter.rect_for_text_range(range_utf16, &app) + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 35703b8a1fb945deabef035f8f3cc2b196dd61e8..7a0da8dad96208b77916a2372a9baab68f4d771d 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -31,7 +31,9 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, + json, + presenter::MeasurementContext, + Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, SizeConstraint, View, }; use core::panic; @@ -41,7 +43,7 @@ use std::{ borrow::Cow, cell::RefCell, mem, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, rc::Rc, }; @@ -49,6 +51,11 @@ trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F; fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext); fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool; + fn rect_for_text_range( + &self, + range_utf16: Range, + cx: &MeasurementContext, + ) -> Option; fn debug(&self, cx: &DebugContext) -> serde_json::Value; fn size(&self) -> Vector2F; @@ -83,6 +90,16 @@ pub trait Element { cx: &mut EventContext, ) -> bool; + fn rect_for_text_range( + &self, + range_utf16: Range, + bounds: RectF, + visible_bounds: RectF, + layout: &Self::LayoutState, + paint: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option; + fn metadata(&self) -> Option<&dyn Any> { None } @@ -287,6 +304,26 @@ impl AnyElement for Lifecycle { } } + fn rect_for_text_range( + &self, + range_utf16: Range, + cx: &MeasurementContext, + ) -> Option { + if let Lifecycle::PostPaint { + element, + bounds, + visible_bounds, + layout, + paint, + .. + } = self + { + element.rect_for_text_range(range_utf16, *bounds, *visible_bounds, layout, paint, cx) + } else { + None + } + } + fn size(&self) -> Vector2F { match self { Lifecycle::Empty | Lifecycle::Init { .. } => panic!("invalid element lifecycle state"), @@ -385,6 +422,14 @@ impl ElementRc { self.element.borrow_mut().dispatch_event(event, cx) } + pub fn rect_for_text_range( + &self, + range_utf16: Range, + cx: &MeasurementContext, + ) -> Option { + self.element.borrow().rect_for_text_range(range_utf16, cx) + } + pub fn size(&self) -> Vector2F { self.element.borrow().size() } diff --git a/crates/gpui/src/elements/align.rs b/crates/gpui/src/elements/align.rs index 5388f7647e503d65233d551c7cf1b9fd76ff40ae..5158b7229eda70897290fff516095ac21c7f0b8c 100644 --- a/crates/gpui/src/elements/align.rs +++ b/crates/gpui/src/elements/align.rs @@ -1,6 +1,8 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + json, + presenter::MeasurementContext, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use json::ToJson; @@ -94,6 +96,18 @@ impl Element for Align { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, bounds: pathfinder_geometry::rect::RectF, diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index 2e10c59049f6c945829c2311a1f9765d0492c1ef..6fec2dd1dc1cfafa84c971487883a60abbc52681 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -1,6 +1,7 @@ use super::Element; use crate::{ json::{self, json}, + presenter::MeasurementContext, DebugContext, PaintContext, }; use json::ToJson; @@ -67,6 +68,18 @@ where false } + fn rect_for_text_range( + &self, + _: std::ops::Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/constrained_box.rs b/crates/gpui/src/elements/constrained_box.rs index 5ab01df1e1d7547ba1d565f001c177c0edcfb863..012bbe23dd05f5b542a541310b844aafe5ca0975 100644 --- a/crates/gpui/src/elements/constrained_box.rs +++ b/crates/gpui/src/elements/constrained_box.rs @@ -1,9 +1,13 @@ +use std::ops::Range; + use json::ToJson; use serde_json::json; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + json, + presenter::MeasurementContext, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -165,6 +169,18 @@ impl Element for ConstrainedBox { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index b8eebad88c63a306c93ac09bf580556958519c1b..a581e60b74e933f4d0720e42f2c9b21fb499a985 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use crate::{ color::Color, geometry::{ @@ -7,6 +9,7 @@ use crate::{ }, json::ToJson, platform::CursorStyle, + presenter::MeasurementContext, scene::{self, Border, CursorRegion, Quad}, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -271,6 +274,18 @@ impl Element for Container { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/empty.rs b/crates/gpui/src/elements/empty.rs index afe24127b58ef6eadc4acf20801d95011ca1036b..9a67115c2382e0cf12796073718db725ae6aa148 100644 --- a/crates/gpui/src/elements/empty.rs +++ b/crates/gpui/src/elements/empty.rs @@ -1,9 +1,12 @@ +use std::ops::Range; + use crate::{ geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{json, ToJson}, + presenter::MeasurementContext, DebugContext, }; use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; @@ -67,6 +70,18 @@ impl Element for Empty { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index 1fec838788e28217d94671877cae26439e03b467..5fe6900c409f0f6701b6715795ec4a7b39752703 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -1,11 +1,11 @@ use crate::{ - geometry::vector::Vector2F, CursorRegion, DebugContext, Element, ElementBox, Event, - EventContext, LayoutContext, MouseButton, MouseEvent, MouseRegion, NavigationDirection, - PaintContext, SizeConstraint, + geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element, + ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseEvent, MouseRegion, + NavigationDirection, PaintContext, SizeConstraint, }; use pathfinder_geometry::rect::RectF; use serde_json::json; -use std::{any::TypeId, rc::Rc}; +use std::{any::TypeId, ops::Range, rc::Rc}; pub struct EventHandler { child: ElementBox, @@ -158,6 +158,18 @@ impl Element for EventHandler { } } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/expanded.rs b/crates/gpui/src/elements/expanded.rs index 6f69d8a92a8bfa2aa647a1cb162de5b74353c72d..e8803affab3556cb8de38e6d9785aa3bb5fb708c 100644 --- a/crates/gpui/src/elements/expanded.rs +++ b/crates/gpui/src/elements/expanded.rs @@ -1,6 +1,10 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + json, + presenter::MeasurementContext, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use serde_json::json; @@ -74,6 +78,18 @@ impl Element for Expanded { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index cb43c1db68e87112b7f991171c00e72119c757bd..1d577344c6e387e100687ab229c047486dc7e472 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -1,7 +1,8 @@ -use std::{any::Any, f32::INFINITY}; +use std::{any::Any, f32::INFINITY, ops::Range}; use crate::{ json::{self, ToJson, Value}, + presenter::MeasurementContext, Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, Vector2FExt, View, @@ -334,6 +335,20 @@ impl Element for Flex { handled } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.children + .iter() + .find_map(|child| child.rect_for_text_range(range_utf16.clone(), cx)) + } + fn debug( &self, bounds: RectF, @@ -417,6 +432,18 @@ impl Element for FlexItem { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn metadata(&self) -> Option<&dyn Any> { Some(&self.metadata) } diff --git a/crates/gpui/src/elements/hook.rs b/crates/gpui/src/elements/hook.rs index e947c3bac7da2a9138298048c3f314f1af0b4ed7..c620847372bed4963d49412809b4733e9515108e 100644 --- a/crates/gpui/src/elements/hook.rs +++ b/crates/gpui/src/elements/hook.rs @@ -1,6 +1,9 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::json, + presenter::MeasurementContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -65,6 +68,18 @@ impl Element for Hook { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index 6b55b567b4b4395170c9033d398e0fd91010a960..dff46804e7e4765c4935c779f06ff61844490d51 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -5,11 +5,12 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{json, ToJson}, + presenter::MeasurementContext, scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, SizeConstraint, }; use serde::Deserialize; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; pub struct Image { data: Arc, @@ -89,6 +90,18 @@ impl Element for Image { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/keystroke_label.rs b/crates/gpui/src/elements/keystroke_label.rs index 0112b548463b450c0b49aa5e1ad4c93f633a5d10..9b21a1b8a86e942da7d31dccf52b24d2a71d2bd2 100644 --- a/crates/gpui/src/elements/keystroke_label.rs +++ b/crates/gpui/src/elements/keystroke_label.rs @@ -76,6 +76,18 @@ impl Element for KeystrokeLabel { element.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/label.rs b/crates/gpui/src/elements/label.rs index e6ae9cbd51330fbd6c411ffebdb9c5deea9b7082..18654808682bb181bd6c4557114782a649d9dda1 100644 --- a/crates/gpui/src/elements/label.rs +++ b/crates/gpui/src/elements/label.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use crate::{ fonts::TextStyle, geometry::{ @@ -5,6 +7,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, + presenter::MeasurementContext, text_layout::{Line, RunStyle}, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -174,6 +177,18 @@ impl Element for Label { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index e368b45288ce5490bb16ad121943b3d3ea5c4ab2..2b44918e43696ae4470beb192b4936d1c1148106 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -4,6 +4,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::json, + presenter::MeasurementContext, DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext, }; @@ -328,6 +329,39 @@ impl Element for List { handled } + fn rect_for_text_range( + &self, + range_utf16: Range, + bounds: RectF, + _: RectF, + scroll_top: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + let state = self.state.0.borrow(); + let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item); + let mut cursor = state.items.cursor::(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + while let Some(item) = cursor.item() { + if item_origin.y() > bounds.max_y() { + break; + } + + if let ListItem::Rendered(element) = item { + if let Some(rect) = element.rect_for_text_range(range_utf16.clone(), cx) { + return Some(rect); + } + + item_origin.set_y(item_origin.y() + element.size().y()); + cursor.next(&()); + } else { + unreachable!(); + } + } + + None + } + fn debug( &self, bounds: RectF, @@ -939,6 +973,18 @@ mod tests { todo!() } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + todo!() + } + fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value { self.id.into() } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 832aafaa9e8b890cd613027c982cd2ef841c266e..e170ee3163177da648f7cb7a038c0088e5f2395c 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, rc::Rc}; +use std::{any::TypeId, ops::Range, rc::Rc}; use super::Padding; use crate::{ @@ -8,8 +8,8 @@ use crate::{ }, platform::CursorStyle, scene::CursorRegion, - DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, MouseState, - PaintContext, RenderContext, SizeConstraint, View, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, + MouseState, PaintContext, RenderContext, SizeConstraint, View, presenter::MeasurementContext, }; use serde_json::json; @@ -192,6 +192,18 @@ impl Element for MouseEventHandler { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 4ed0ca7d228db1524d5519336e7da579e8100d25..bf2ef595799ecc6c0d666bd695465f77a018ee62 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -1,6 +1,9 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, + presenter::MeasurementContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, PaintContext, SizeConstraint, }; @@ -126,6 +129,18 @@ impl Element for Overlay { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/stack.rs b/crates/gpui/src/elements/stack.rs index 4531734085487dcc1d0e9e6d5cc28154133ba2a3..992a8922a741c1120c42c63e0107941e020e8e05 100644 --- a/crates/gpui/src/elements/stack.rs +++ b/crates/gpui/src/elements/stack.rs @@ -1,6 +1,9 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{self, json, ToJson}, + presenter::MeasurementContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -64,6 +67,21 @@ impl Element for Stack { false } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.children + .iter() + .rev() + .find_map(|child| child.rect_for_text_range(range_utf16.clone(), cx)) + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index d473e1f0fb01f90dd565e9768ac6da05c4af929d..544abb3314a526b72f674e01a213f5f4b75af334 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, ops::Range}; use serde_json::json; @@ -8,6 +8,7 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, + presenter::MeasurementContext, scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -84,6 +85,18 @@ impl Element for Svg { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index fb1e2ea33188c2b0041b8af74b562008d58dd3dc..580240e3fde31649a62bf401ad1d6d60d883ae3d 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -6,6 +6,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, + presenter::MeasurementContext, text_layout::{Line, RunStyle, ShapedBoundary}, DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, SizeConstraint, TextLayoutCache, @@ -63,7 +64,7 @@ impl Element for Text { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { // Convert the string and highlight ranges into an iterator of highlighted chunks. - + let mut offset = 0; let mut highlight_ranges = self.highlights.iter().peekable(); let chunks = std::iter::from_fn(|| { @@ -81,7 +82,8 @@ impl Element for Text { "Highlight out of text range. Text len: {}, Highlight range: {}..{}", self.text.len(), range.start, - range.end); + range.end + ); result = None; } } else if offset < self.text.len() { @@ -188,6 +190,18 @@ impl Element for Text { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 9a65b2661d71c216a50432605138336a35bb67db..03e5548239c25ab28ff7a4c6a5df06fedc294d46 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -6,12 +6,14 @@ use crate::{ fonts::TextStyle, geometry::{rect::RectF, vector::Vector2F}, json::json, + presenter::MeasurementContext, Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, Task, View, }; use serde::Deserialize; use std::{ cell::{Cell, RefCell}, + ops::Range, rc::Rc, time::Duration, }; @@ -196,6 +198,18 @@ impl Element for Tooltip { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 9b2d966a7d6cc44b39fee390747fe7d28f4265b8..2ab4e91538c0c8d2b51fe889d4d086605aa3f776 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,6 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{self, json}, + presenter::MeasurementContext, ElementBox, RenderContext, ScrollWheelEvent, View, }; use json::ToJson; @@ -327,6 +328,21 @@ impl Element for UniformList { handled } + fn rect_for_text_range( + &self, + range: Range, + _: RectF, + _: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + layout + .items + .iter() + .find_map(|child| child.rect_for_text_range(range.clone(), cx)) + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 723e25c55d62a55951a013255992167bbe6676bf..4cca93edc8a5dabfac011e137ea4ccd48698a3b0 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -30,7 +30,8 @@ pub mod platform; pub use gpui_macros::test; pub use platform::*; pub use presenter::{ - Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, + Axis, DebugContext, EventContext, LayoutContext, MeasurementContext, PaintContext, + SizeConstraint, Vector2FExt, }; pub use anyhow; diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index be121ff5b7a307f07c6165a60fb742c51370a6f2..aab184f87d5fb8d0830fe1fc93a3d20e8b6d4e11 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -91,17 +91,18 @@ pub trait Dispatcher: Send + Sync { pub trait InputHandler { fn selected_text_range(&self) -> Option>; - fn set_selected_text_range(&mut self, range: Range); - fn text_for_range(&self, range: Range) -> Option; + fn set_selected_text_range(&mut self, range_utf16: Range); + fn text_for_range(&self, range_utf16: Range) -> Option; fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str); fn replace_and_mark_text_in_range( &mut self, - range: Option>, + range_utf16: Option>, new_text: &str, new_selected_range: Option>, ); fn marked_text_range(&self) -> Option>; fn unmark_text(&mut self); + fn rect_for_range(&self, range_utf16: Range) -> Option; } pub trait Window: WindowContext { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 2d03e89c2c43bb674fe0fab58b9e125349a8c4e3..ce06ddac37a9885acb093476c4245fb7936a254f 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1003,8 +1003,33 @@ extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { .map_or(NSRange::invalid(), |range| range.into()) } -extern "C" fn first_rect_for_character_range(_: &Object, _: Sel, _: NSRange, _: id) -> NSRect { - NSRect::new(NSPoint::new(0., 0.), NSSize::new(20., 20.)) +extern "C" fn first_rect_for_character_range( + this: &Object, + _: Sel, + range: NSRange, + _: id, +) -> NSRect { + let frame = unsafe { + let window = get_window_state(this).borrow().native_window; + NSView::frame(window) + }; + + with_input_handler(this, |input_handler| { + input_handler.rect_for_range(range.to_range()?) + }) + .flatten() + .map_or( + NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)), + |rect| { + NSRect::new( + NSPoint::new( + frame.origin.x + rect.origin_x() as f64, + frame.origin.y + frame.size.height - rect.origin_y() as f64, + ), + NSSize::new(rect.width() as f64, rect.height() as f64), + ) + }, + ) } extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 86a8c4cf3049e924cfcea798ae2ff703ea6132d8..2b0ad394ddc62cfd36f020a8ab9da32ecfea8a2d 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -19,7 +19,7 @@ use smallvec::SmallVec; use std::{ collections::{HashMap, HashSet}, marker::PhantomData, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -224,6 +224,17 @@ impl Presenter { } } + pub fn rect_for_text_range(&self, range_utf16: Range, cx: &AppContext) -> Option { + cx.focused_view_id(self.window_id).and_then(|view_id| { + let cx = MeasurementContext { + app: cx, + rendered_views: &self.rendered_views, + window_id: self.window_id, + }; + cx.rect_for_text_range(view_id, range_utf16) + }) + } + pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) -> bool { if let Some(root_view_id) = cx.root_view_id(self.window_id) { let mut invalidated_views = Vec::new(); @@ -777,6 +788,27 @@ impl<'a> DerefMut for EventContext<'a> { } } +pub struct MeasurementContext<'a> { + app: &'a AppContext, + rendered_views: &'a HashMap, + pub window_id: usize, +} + +impl<'a> Deref for MeasurementContext<'a> { + type Target = AppContext; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a> MeasurementContext<'a> { + fn rect_for_text_range(&self, view_id: usize, range_utf16: Range) -> Option { + let element = self.rendered_views.get(&view_id)?; + element.rect_for_text_range(range_utf16, self) + } +} + pub struct DebugContext<'a> { rendered_views: &'a HashMap, pub font_cache: &'a FontCache, @@ -936,6 +968,18 @@ impl Element for ChildView { cx.dispatch_event(self.view.id(), event) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + cx.rect_for_text_range(self.view.id(), range_utf16) + } + fn debug( &self, bounds: RectF, diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 3406b4b9e1b0efa142348fb913c24d0f8a090b50..b00a706f43a36278bcf063fea8a76ad72d080324 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -395,6 +395,18 @@ impl Element for TerminalEl { } } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + todo!() + } + fn debug( &self, _bounds: gpui::geometry::rect::RectF, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f6c185a19f2971a503d99731d2278b73a93bb31e..a13da1bc7fae702a9de04a5288a8af605c59adc7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,6 +43,7 @@ use std::{ fmt, future::Future, mem, + ops::Range, path::{Path, PathBuf}, rc::Rc, sync::{ @@ -2538,6 +2539,18 @@ impl Element for AvatarRibbon { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: gpui::geometry::rect::RectF, From 509f54bf2024d1beeec265a2440637cfd57bdd38 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Jul 2022 17:37:32 +0200 Subject: [PATCH 20/47] Don't dispatch keydown event if editor is still composing --- crates/gpui/src/platform/mac/window.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index ce06ddac37a9885acb093476c4245fb7936a254f..9ccbaf33a3d17c477007ac9f7b89961afef879d2 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -725,7 +725,15 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> if let Some(event) = window_state_borrow.pending_key_down_event.take() { if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); - callback(Event::KeyDown(event)); + + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + if !is_composing { + callback(Event::KeyDown(event)); + } + window_state.borrow_mut().event_callback = Some(callback); } } From 7c575990bef7191a020e9b31ef9350dce97ac7ac Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Jul 2022 12:22:12 -0700 Subject: [PATCH 21/47] Remove the set_selected_text_range method from the InputHandler trait --- crates/editor/src/editor.rs | 41 ++++++++++++++++++++---------------- crates/editor/src/element.rs | 2 +- crates/gpui/src/app.rs | 27 ------------------------ crates/gpui/src/platform.rs | 3 +-- 4 files changed, 25 insertions(+), 48 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index babc6f3ca4b6697e8375b29fb59509cc968870f8..fdcbb875226e3f01f2b13941fa38b58409fe3bb8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5892,12 +5892,6 @@ impl View for Editor { Some(range.start.0..range.end.0) } - fn set_selected_text_range(&mut self, range_utf16: Range, cx: &mut ViewContext) { - self.change_selections(None, cx, |selections| { - selections.select_ranges([OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)]) - }); - } - fn marked_text_range(&self, cx: &AppContext) -> Option> { let range = self.text_highlights::(cx)?.1.get(0)?; let snapshot = self.buffer.read(cx).read(cx); @@ -5917,8 +5911,10 @@ impl View for Editor { cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - if let Some(range_utf16) = range_utf16.or_else(|| this.marked_text_range(cx)) { - this.set_selected_text_range(range_utf16, cx); + if let Some(range) = range_utf16.or_else(|| this.marked_text_range(cx)) { + this.change_selections(None, cx, |selections| { + selections.select_ranges([OffsetUtf16(range.start)..OffsetUtf16(range.end)]) + }); } this.handle_input(text, cx); this.unmark_text(cx); @@ -5933,15 +5929,22 @@ impl View for Editor { cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - if let Some(mut marked_range) = this.marked_text_range(cx) { + let range_to_replace = if let Some(mut marked_range) = this.marked_text_range(cx) { if let Some(relative_range_utf16) = range_utf16.as_ref() { marked_range.end = marked_range.start + relative_range_utf16.end; marked_range.start += relative_range_utf16.start; } - - this.set_selected_text_range(marked_range, cx); + Some(marked_range) } else if let Some(range_utf16) = range_utf16 { - this.set_selected_text_range(range_utf16, cx); + Some(range_utf16) + } else { + None + }; + + if let Some(range) = range_to_replace { + this.change_selections(None, cx, |s| { + s.select_ranges([OffsetUtf16(range.start)..OffsetUtf16(range.end)]) + }); } let selection = this.selections.newest_anchor(); @@ -5962,15 +5965,17 @@ impl View for Editor { this.handle_input(text, cx); - if let Some(new_selected_range) = new_selected_range_utf16 { + if let Some(mut new_selected_range) = new_selected_range_utf16 { let snapshot = this.buffer.read(cx).read(cx); let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; + new_selected_range.start += insertion_start; + new_selected_range.end += insertion_start; drop(snapshot); - this.set_selected_text_range( - insertion_start + new_selected_range.start - ..insertion_start + new_selected_range.end, - cx, - ); + this.change_selections(None, cx, |selections| { + selections + .select_ranges([OffsetUtf16(new_selected_range.start) + ..OffsetUtf16(new_selected_range.end)]) + }); } }); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 951796b6bfbdf829c7ee55ebb8e7085f31494318..9f5da43916c9a5a1dce518e58ed1d5a6762d2ed8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,6 +1,6 @@ use super::{ display_map::{BlockContext, ToDisplayPoint}, - Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase, + Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN, }; use crate::{ diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c098927f4a96caec82bdea9aa393c47389624fe4..90626c46655707a90e84e21a1c32862a91c52bdb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -72,7 +72,6 @@ pub trait View: Entity + Sized { fn selected_text_range(&self, _: &AppContext) -> Option> { None } - fn set_selected_text_range(&mut self, _: Range, _: &mut ViewContext) {} fn marked_text_range(&self, _: &AppContext) -> Option> { None } @@ -391,14 +390,6 @@ impl InputHandler for WindowInputHandler { result } - fn set_selected_text_range(&mut self, range: Range) { - eprintln!("set_selected_text_range({range:?})"); - - self.update_focused_view(|window_id, view_id, view, cx| { - view.set_selected_text_range(range, cx, window_id, view_id); - }); - } - fn replace_text_in_range(&mut self, range: Option>, text: &str) { eprintln!("replace_text_in_range({range:?}, {text:?})"); @@ -3321,13 +3312,6 @@ pub trait AnyView { fn text_for_range(&self, range: Range, cx: &AppContext) -> Option; fn selected_text_range(&self, cx: &AppContext) -> Option>; - fn set_selected_text_range( - &mut self, - range: Range, - cx: &mut MutableAppContext, - window_id: usize, - view_id: usize, - ); fn marked_text_range(&self, cx: &AppContext) -> Option>; fn unmark_text(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn replace_text_in_range( @@ -3406,17 +3390,6 @@ where View::selected_text_range(self, cx) } - fn set_selected_text_range( - &mut self, - range: Range, - cx: &mut MutableAppContext, - window_id: usize, - view_id: usize, - ) { - let mut cx = ViewContext::new(cx, window_id, view_id); - View::set_selected_text_range(self, range, &mut cx) - } - fn marked_text_range(&self, cx: &AppContext) -> Option> { View::marked_text_range(self, cx) } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index aab184f87d5fb8d0830fe1fc93a3d20e8b6d4e11..f85b06438e6de5e6d535ca355b80488941128ff9 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -91,7 +91,7 @@ pub trait Dispatcher: Send + Sync { pub trait InputHandler { fn selected_text_range(&self) -> Option>; - fn set_selected_text_range(&mut self, range_utf16: Range); + fn marked_text_range(&self) -> Option>; fn text_for_range(&self, range_utf16: Range) -> Option; fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str); fn replace_and_mark_text_in_range( @@ -100,7 +100,6 @@ pub trait InputHandler { new_text: &str, new_selected_range: Option>, ); - fn marked_text_range(&self) -> Option>; fn unmark_text(&mut self); fn rect_for_range(&self, range_utf16: Range) -> Option; } From beeaec8647787ee128f498b1c4ce8470b65bef34 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Jul 2022 12:48:36 -0700 Subject: [PATCH 22/47] Prevent IME window from appearing while editor's input is disabled (vim) --- crates/editor/src/editor.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fdcbb875226e3f01f2b13941fa38b58409fe3bb8..7118f3c0ff6859de9572ab85279561a441b8eeff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5888,6 +5888,12 @@ impl View for Editor { } fn selected_text_range(&self, cx: &AppContext) -> Option> { + // Prevent the IME menu from appearing when holding down an alphabetic key + // while input is disabled. + if !self.input_enabled { + return None; + } + let range = self.selections.newest::(cx).range(); Some(range.start.0..range.end.0) } From 2142fca6735de7d3492ff2dd5da045e5e8a63922 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Jul 2022 13:40:48 -0700 Subject: [PATCH 23/47] Remove Input action, detect ignored input in vim via an event --- assets/keymaps/default.json | 5 +---- crates/editor/src/editor.rs | 15 +++++++++++---- crates/file_finder/src/file_finder.rs | 12 +++++++----- crates/vim/src/editor_events.rs | 17 ++++++++++++++--- crates/vim/src/vim.rs | 12 +----------- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a8b907663398aae270655b18d3341a404f5f616a..a1dbf98fbebd1ed7272fb767bf85a768b792f992 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -138,10 +138,7 @@ { "context": "Editor && mode == auto_height", "bindings": { - "alt-enter": [ - "editor::Input", - "\n" - ] + "alt-enter": "editor::Newline" } }, { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7118f3c0ff6859de9572ab85279561a441b8eeff..1bda73e74e696a46bf269ce70f0c503ce97d38eb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -98,9 +98,6 @@ pub struct Jump { anchor: language::Anchor, } -#[derive(Clone, Deserialize, PartialEq)] -pub struct Input(pub String); - #[derive(Clone, Deserialize, PartialEq)] pub struct SelectToBeginningOfLine { #[serde(default)] @@ -214,7 +211,6 @@ actions!( impl_actions!( editor, [ - Input, SelectNext, SelectToBeginningOfLine, SelectToEndOfLine, @@ -5772,6 +5768,7 @@ pub enum Event { SelectionsChanged { local: bool }, ScrollPositionChanged { local: bool }, Closed, + IgnoredInput, } pub struct EditorFocused(pub ViewHandle); @@ -5916,6 +5913,11 @@ impl View for Editor { text: &str, cx: &mut ViewContext, ) { + if !self.input_enabled { + cx.emit(Event::IgnoredInput); + return; + } + self.transact(cx, |this, cx| { if let Some(range) = range_utf16.or_else(|| this.marked_text_range(cx)) { this.change_selections(None, cx, |selections| { @@ -5934,6 +5936,11 @@ impl View for Editor { new_selected_range_utf16: Option>, cx: &mut ViewContext, ) { + if !self.input_enabled { + cx.emit(Event::IgnoredInput); + return; + } + self.transact(cx, |this, cx| { let range_to_replace = if let Some(mut marked_range) = this.marked_text_range(cx) { if let Some(relative_range_utf16) = range_utf16.as_ref() { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 720e0142beb35684fc4afad10fd7e7f5e0545e00..5603fa22efe6ffec534911c4a43c822e81e0377d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -279,7 +279,7 @@ impl PickerDelegate for FileFinder { #[cfg(test)] mod tests { use super::*; - use editor::{Editor, Input}; + use editor::Editor; use menu::{Confirm, SelectNext}; use serde_json::json; use workspace::{AppState, Workspace}; @@ -326,12 +326,14 @@ mod tests { .downcast::() .unwrap() }); - cx.dispatch_action(window_id, Input("b".into())); - cx.dispatch_action(window_id, Input("n".into())); - cx.dispatch_action(window_id, Input("a".into())); finder - .condition(&cx, |finder, _| finder.matches.len() == 2) + .update(cx, |finder, cx| { + finder.update_matches("bna".to_string(), cx) + }) .await; + finder.read_with(cx, |finder, _| { + assert_eq!(finder.matches.len(), 2); + }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); cx.dispatch_action(window_id, SelectNext); diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index c68e5182b0ea0f4cebe754cac2450553c9219638..055b0bdb08bac73aff7a36d2f8a4dbdd96162865 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -22,9 +22,20 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont vim.active_editor = Some(editor.downgrade()); vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| { if editor.read(cx).leader_replica_id().is_none() { - if let editor::Event::SelectionsChanged { local: true } = event { - let newest_empty = editor.read(cx).selections.newest::(cx).is_empty(); - editor_local_selections_changed(newest_empty, cx); + match event { + editor::Event::SelectionsChanged { local: true } => { + let newest_empty = + editor.read(cx).selections.newest::(cx).is_empty(); + editor_local_selections_changed(newest_empty, cx); + } + editor::Event::IgnoredInput => { + Vim::update(cx, |vim, cx| { + if vim.active_operator().is_some() { + vim.clear_operator(cx); + } + }); + } + _ => (), } } })); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 5655e51e29cfea44a7934668663859a33507236e..445d25a91c706690ebd8c998c82a142abab58b0b 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -11,7 +11,7 @@ mod visual; use collections::HashMap; use command_palette::CommandPaletteFilter; -use editor::{Bias, Cancel, CursorShape, Editor, Input}; +use editor::{Bias, Cancel, CursorShape, Editor}; use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle}; use serde::Deserialize; @@ -45,16 +45,6 @@ pub fn init(cx: &mut MutableAppContext) { ); // Editor Actions - cx.add_action(|_: &mut Editor, _: &Input, cx| { - // If we have an unbound input with an active operator, cancel that operator. Otherwise forward - // the input to the editor - if Vim::read(cx).active_operator().is_some() { - // Defer without updating editor - MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx))) - } else { - cx.propagate_action() - } - }); cx.add_action(|_: &mut Editor, _: &Cancel, cx| { // If we are in a non normal mode or have an active operator, swap to normal mode // Otherwise forward cancel on to the editor From 6d264502b2a60bdc888f5f2c581ed53122b996d2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Jul 2022 13:44:58 -0700 Subject: [PATCH 24/47] Remove print statements --- crates/editor/src/link_go_to_definition.rs | 1 - crates/gpui/src/app.rs | 35 ++++------------------ 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index f034df64b6eea4528048bb365d3f74fee14cc463..805d1621265964185446a3534b2c2e14b31c9e3b 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -410,7 +410,6 @@ mod tests { requests.next().await; cx.foreground().run_until_parked(); - println!("tag"); cx.assert_editor_text_highlights::(indoc! {" fn test() [do_work](); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 90626c46655707a90e84e21a1c32862a91c52bdb..4733864b97df46f99e1b90c7909155115c72f5e0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -371,46 +371,27 @@ impl WindowInputHandler { impl InputHandler for WindowInputHandler { fn text_for_range(&self, range: Range) -> Option { - let result = self - .read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) - .flatten(); - - eprintln!("text_for_range({range:?}) -> {result:?}"); - - result + self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) + .flatten() } fn selected_text_range(&self) -> Option> { - let result = self - .read_focused_view(|view, cx| view.selected_text_range(cx)) - .flatten(); - - eprintln!("selected_text_range() -> {result:?}"); - - result + self.read_focused_view(|view, cx| view.selected_text_range(cx)) + .flatten() } fn replace_text_in_range(&mut self, range: Option>, text: &str) { - eprintln!("replace_text_in_range({range:?}, {text:?})"); - self.update_focused_view(|window_id, view_id, view, cx| { view.replace_text_in_range(range, text, cx, window_id, view_id); }); } fn marked_text_range(&self) -> Option> { - let result = self - .read_focused_view(|view, cx| view.marked_text_range(cx)) - .flatten(); - - eprintln!("marked_text_range() -> {result:?}"); - - result + self.read_focused_view(|view, cx| view.marked_text_range(cx)) + .flatten() } fn unmark_text(&mut self) { - eprintln!("unmark_text()"); - self.update_focused_view(|window_id, view_id, view, cx| { view.unmark_text(cx, window_id, view_id); }); @@ -422,10 +403,6 @@ impl InputHandler for WindowInputHandler { new_text: &str, new_selected_range: Option>, ) { - eprintln!( - "replace_and_mark_text_in_range({range:?}, {new_text:?}, {new_selected_range:?})" - ); - self.update_focused_view(|window_id, view_id, view, cx| { view.replace_and_mark_text_in_range( range, From 0185b4fef43726d7a4eb316d88d89f22ac4d195e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Jul 2022 18:04:56 -0700 Subject: [PATCH 25/47] Add simple IME handling to the terminal --- crates/editor/src/element.rs | 7 +++ crates/terminal/src/connection/keymappings.rs | 48 +------------------ crates/terminal/src/terminal.rs | 4 ++ crates/terminal/src/terminal_element.rs | 15 ++++-- styles/package-lock.json | 1 + 5 files changed, 25 insertions(+), 50 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9f5da43916c9a5a1dce518e58ed1d5a6762d2ed8..88c61fe6900ca6b7e47171a6eb0aa8a61c4be9bf 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1713,6 +1713,13 @@ impl Cursor { } } + pub fn bounding_rect(&self, origin: Vector2F) -> RectF { + RectF::new( + self.origin + origin, + vec2f(self.block_width, self.line_height), + ) + } + pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) { let bounds = match self.shape { CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)), diff --git a/crates/terminal/src/connection/keymappings.rs b/crates/terminal/src/connection/keymappings.rs index a4d429843bf17aa51760b69e3b00f8317b681d15..56e431e0b1345c044a17c6d10ced5a2157f04fc8 100644 --- a/crates/terminal/src/connection/keymappings.rs +++ b/crates/terminal/src/connection/keymappings.rs @@ -240,53 +240,7 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option { } } - //Fallback to sending the keystroke input directly - //Skin colors in utf8 are implemented as a seperate, invisible character - //that modifies the associated emoji. Some languages may have similarly - //implemented modifiers, e.g. certain diacritics that can be typed as a single character. - //This means that we need to assume some user input can result in multi-byte, - //multi-char strings. This is somewhat difficult, as GPUI normalizes all - //keys into a string representation. Hence, the check here to filter out GPUI - //keys that weren't captured above. - if !matches_gpui_key_str(&keystroke.key) { - return Some(keystroke.key.clone()); - } else { - None - } -} - -///Checks if the given string matches a GPUI key string. -///Table made from reading the source at gpui/src/platform/mac/event.rs -fn matches_gpui_key_str(str: &str) -> bool { - match str { - "backspace" => true, - "up" => true, - "down" => true, - "left" => true, - "right" => true, - "pageup" => true, - "pagedown" => true, - "home" => true, - "end" => true, - "delete" => true, - "enter" => true, - "escape" => true, - "tab" => true, - "f1" => true, - "f2" => true, - "f3" => true, - "f4" => true, - "f5" => true, - "f6" => true, - "f7" => true, - "f8" => true, - "f9" => true, - "f10" => true, - "f11" => true, - "f12" => true, - "space" => true, - _ => false, - } + None } /// Code Modifiers diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a41b8f5ad3994e587ed6eab216b93b59f080bf14..1cb5b62a765a93c23ef24403fd8dab1c55bed1ea 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -262,6 +262,10 @@ impl View for Terminal { context } + fn selected_text_range(&self, _: &AppContext) -> Option> { + Some(0..0) + } + fn replace_text_in_range( &mut self, _: Option>, diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index b00a706f43a36278bcf063fea8a76ad72d080324..367c5769825f2665f9431b9211aa553ee3e45b40 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -398,13 +398,22 @@ impl Element for TerminalEl { fn rect_for_text_range( &self, _: Range, + bounds: RectF, _: RectF, - _: RectF, - _: &Self::LayoutState, + layout: &Self::LayoutState, _: &Self::PaintState, _: &gpui::MeasurementContext, ) -> Option { - todo!() + // Use the same origin that's passed to `Cursor::paint` in the paint + // method bove. + let mut origin = bounds.origin() + vec2f(layout.em_width.0, 0.); + + // TODO - Why is it necessary to move downward one line to get correct + // positioning? I would think that we'd want the same rect that is + // painted for the cursor. + origin += vec2f(0., layout.line_height.0); + + Some(layout.cursor.as_ref()?.bounding_rect(origin)) } fn debug( diff --git a/styles/package-lock.json b/styles/package-lock.json index 2eb6d3a1bfaeaa206f0cc8a0a03efa0387d144c2..49304dc2fa98dfec942bf6687be56cefc13cdf80 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { From 372c3eed52fc2645444da7a252e807ba2c9d42bc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Jul 2022 21:36:40 -0700 Subject: [PATCH 26/47] Adjust editor tests to use input APIs instead of key events --- crates/editor/src/editor.rs | 14 ++++++++------ crates/editor/src/test.rs | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1bda73e74e696a46bf269ce70f0c503ce97d38eb..a6a050d374657b84ce2e86092f171e3b0a5d0ecd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9708,7 +9708,7 @@ mod tests { one| two three"}); - cx.simulate_keystroke("."); + cx.simulate_input("."); handle_completion_request( &mut cx, indoc! {" @@ -9754,9 +9754,9 @@ mod tests { two| three| additional edit"}); - cx.simulate_keystroke(" "); + cx.simulate_input(" "); assert!(cx.editor(|e, _| e.context_menu.is_none())); - cx.simulate_keystroke("s"); + cx.simulate_input("s"); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.assert_editor_state(indoc! {" @@ -9777,7 +9777,7 @@ mod tests { cx.condition(|editor, _| editor.context_menu_visible()) .await; - cx.simulate_keystroke("i"); + cx.simulate_input("i"); handle_completion_request( &mut cx, @@ -9812,9 +9812,11 @@ mod tests { }) }); cx.set_state("editor|"); - cx.simulate_keystroke("."); + cx.simulate_input("."); assert!(cx.editor(|e, _| e.context_menu.is_none())); - cx.simulate_keystrokes(["c", "l", "o"]); + cx.simulate_input("c"); + cx.simulate_input("l"); + cx.simulate_input("o"); cx.assert_editor_state("editor.clo|"); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.update_editor(|editor, cx| { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index df9b48e6481e6a5c1dea8846ee1aa10a41ba50f8..d5ef041a19592fc56040208686c2ac34340c1d57 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -139,6 +139,12 @@ impl<'a> EditorTestContext<'a> { }) } + pub fn simulate_input(&mut self, input: &str) { + self.editor.update(self.cx, |editor, cx| { + editor.handle_input(input, cx); + }); + } + pub fn simulate_keystroke(&mut self, keystroke_text: &str) { let keystroke = Keystroke::parse(keystroke_text).unwrap(); self.cx.dispatch_keystroke(self.window_id, keystroke, false); From 9c412a88069b5c01af31f01ae649b73c1bcc3bbd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Jul 2022 21:40:37 -0700 Subject: [PATCH 27/47] Remove test for handling input via key events Now, textual input is handled by a different code path than other key events. --- crates/terminal/src/connection/keymappings.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/terminal/src/connection/keymappings.rs b/crates/terminal/src/connection/keymappings.rs index 56e431e0b1345c044a17c6d10ced5a2157f04fc8..36921a5b3e638f8dc253ab3a63de1c19b59ce3ec 100644 --- a/crates/terminal/src/connection/keymappings.rs +++ b/crates/terminal/src/connection/keymappings.rs @@ -313,20 +313,6 @@ mod test { assert_eq!(to_esc_str(&pagedown, &any), Some("\x1b[6~".to_string())); } - #[test] - fn test_multi_char_fallthrough() { - let ks = Keystroke { - ctrl: false, - alt: false, - shift: false, - cmd: false, - - key: "🖖🏻".to_string(), //2 char string - }; - - assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("🖖🏻".to_string())); - } - #[test] fn test_application_mode() { let app_cursor = TermMode::APP_CURSOR; From 136550de9f013334e6ec41befe6594b236b617ff Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Jul 2022 08:15:44 +0200 Subject: [PATCH 28/47] Discard `shift` when it causes keyboard to output a different character --- assets/keymaps/default.json | 62 +++++++++--------- crates/gpui/src/platform/mac/event.rs | 90 ++++++++++++++------------- 2 files changed, 78 insertions(+), 74 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a1dbf98fbebd1ed7272fb767bf85a768b792f992..21c45acafe8da92bb619bdf5af2047b5859e9329 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -11,22 +11,22 @@ "enter": "menu::Confirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "shift-cmd-{": "pane::ActivatePrevItem", - "shift-cmd-}": "pane::ActivateNextItem", + "cmd-{": "pane::ActivatePrevItem", + "cmd-}": "pane::ActivateNextItem", "alt-cmd-left": "pane::ActivatePrevItem", "alt-cmd-right": "pane::ActivateNextItem", "cmd-w": "pane::CloseActiveItem", - "cmd-shift-W": "workspace::CloseWindow", + "cmd-shift-w": "workspace::CloseWindow", "alt-cmd-t": "pane::CloseInactiveItems", "cmd-s": "workspace::Save", - "cmd-shift-S": "workspace::SaveAs", + "cmd-shift-s": "workspace::SaveAs", "cmd-=": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", "cmd-0": "zed::ResetBufferFontSize", "cmd-,": "zed::OpenSettings", "cmd-q": "zed::Quit", "cmd-n": "workspace::NewFile", - "cmd-shift-N": "workspace::NewWindow", + "cmd-shift-n": "workspace::NewWindow", "cmd-o": "workspace::Open" } }, @@ -53,7 +53,7 @@ "cmd-c": "editor::Copy", "cmd-v": "editor::Paste", "cmd-z": "editor::Undo", - "cmd-shift-Z": "editor::Redo", + "cmd-shift-z": "editor::Redo", "up": "editor::MoveUp", "down": "editor::MoveDown", "left": "editor::MoveLeft", @@ -73,17 +73,17 @@ "cmd-up": "editor::MoveToBeginning", "cmd-down": "editor::MoveToEnd", "shift-up": "editor::SelectUp", - "ctrl-shift-P": "editor::SelectUp", + "ctrl-shift-p": "editor::SelectUp", "shift-down": "editor::SelectDown", - "ctrl-shift-N": "editor::SelectDown", + "ctrl-shift-n": "editor::SelectDown", "shift-left": "editor::SelectLeft", - "ctrl-shift-B": "editor::SelectLeft", + "ctrl-shift-b": "editor::SelectLeft", "shift-right": "editor::SelectRight", - "ctrl-shift-F": "editor::SelectRight", + "ctrl-shift-f": "editor::SelectRight", "alt-shift-left": "editor::SelectToPreviousWordStart", - "alt-shift-B": "editor::SelectToPreviousWordStart", + "alt-shift-b": "editor::SelectToPreviousWordStart", "alt-shift-right": "editor::SelectToNextWordEnd", - "alt-shift-F": "editor::SelectToNextWordEnd", + "alt-shift-f": "editor::SelectToNextWordEnd", "cmd-shift-up": "editor::SelectToBeginning", "cmd-shift-down": "editor::SelectToEnd", "cmd-a": "editor::SelectAll", @@ -94,7 +94,7 @@ "stop_at_soft_wraps": true } ], - "ctrl-shift-A": [ + "ctrl-shift-a": [ "editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true @@ -106,7 +106,7 @@ "stop_at_soft_wraps": true } ], - "ctrl-shift-E": [ + "ctrl-shift-e": [ "editor::SelectToEndOfLine", { "stop_at_soft_wraps": true @@ -155,7 +155,7 @@ "bindings": { "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", - "cmd-shift-G": "search::SelectPrevMatch", + "cmd-shift-g": "search::SelectPrevMatch", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", "alt-cmd-r": "search::ToggleRegex" @@ -187,7 +187,7 @@ "alt-up": "editor::SelectLargerSyntaxNode", "alt-down": "editor::SelectSmallerSyntaxNode", "cmd-u": "editor::UndoSelection", - "cmd-shift-U": "editor::RedoSelection", + "cmd-shift-u": "editor::RedoSelection", "f8": "editor::GoToNextDiagnostic", "shift-f8": "editor::GoToPrevDiagnostic", "f2": "editor::Rename", @@ -203,7 +203,7 @@ { "context": "Editor && mode == full", "bindings": { - "cmd-shift-O": "outline::Toggle", + "cmd-shift-o": "outline::Toggle", "ctrl-g": "go_to_line::Toggle" } }, @@ -248,9 +248,9 @@ ], "ctrl-0": "pane::ActivateLastItem", "ctrl--": "pane::GoBack", - "shift-ctrl-_": "pane::GoForward", - "cmd-shift-T": "pane::ReopenClosedItem", - "cmd-shift-F": "project_search::ToggleFocus" + "ctrl-_": "pane::GoForward", + "cmd-shift-t": "pane::ReopenClosedItem", + "cmd-shift-f": "project_search::ToggleFocus" } }, { @@ -293,14 +293,14 @@ 8 ], "cmd-b": "workspace::ToggleLeftSidebar", - "cmd-shift-F": "project_search::Deploy", + "cmd-shift-f": "project_search::Deploy", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", "cmd-t": "project_symbols::Toggle", "cmd-p": "file_finder::Toggle", - "cmd-shift-P": "command_palette::Toggle", - "cmd-shift-M": "diagnostics::Deploy", - "cmd-shift-E": "project_panel::Toggle", + "cmd-shift-p": "command_palette::Toggle", + "cmd-shift-m": "diagnostics::Deploy", + "cmd-shift-e": "project_panel::Toggle", "cmd-alt-s": "workspace::SaveAll" } }, @@ -308,9 +308,9 @@ { "context": "Editor", "bindings": { - "ctrl-shift-K": "editor::DeleteLine", - "cmd-shift-D": "editor::DuplicateLine", - "cmd-shift-L": "editor::SplitSelectionIntoLines", + "ctrl-shift-k": "editor::DeleteLine", + "cmd-shift-d": "editor::DuplicateLine", + "cmd-shift-l": "editor::SplitSelectionIntoLines", "ctrl-cmd-up": "editor::MoveLineUp", "ctrl-cmd-down": "editor::MoveLineDown", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", @@ -322,9 +322,9 @@ "ctrl-alt-right": "editor::MoveToNextSubwordEnd", "ctrl-alt-f": "editor::MoveToNextSubwordEnd", "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-B": "editor::SelectToPreviousSubwordStart", + "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-alt-shift-F": "editor::SelectToNextSubwordEnd" + "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" } }, { @@ -385,8 +385,8 @@ { "context": "Workspace", "bindings": { - "cmd-shift-C": "contacts_panel::Toggle", - "cmd-shift-B": "workspace::ToggleRightSidebar" + "cmd-shift-c": "contacts_panel::Toggle", + "cmd-shift-b": "workspace::ToggleRightSidebar" } }, { diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index de018fc679f0755a8fe77027f7935ffdac646083..050d69bcda5d7961aabf4d1b0cf541309c425032 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -10,6 +10,7 @@ use cocoa::{ base::{id, YES}, foundation::NSString as _, }; +use objc::{msg_send, sel, sel_impl}; use std::{borrow::Cow, ffi::CStr, os::raw::c_char}; const BACKSPACE_KEY: u16 = 0x7f; @@ -77,42 +78,13 @@ impl Event { cmd, })) } - NSEventType::NSKeyDown => { - let modifiers = native_event.modifierFlags(); - let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - - let unmodified_chars = get_key_text(native_event)?; - Some(Self::KeyDown(KeyDownEvent { - keystroke: Keystroke { - ctrl, - alt, - shift, - cmd, - key: unmodified_chars.into(), - }, - is_held: native_event.isARepeat() == YES, - })) - } - NSEventType::NSKeyUp => { - let modifiers = native_event.modifierFlags(); - let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let unmodified_chars = get_key_text(native_event)?; - Some(Self::KeyUp(KeyUpEvent { - keystroke: Keystroke { - ctrl, - alt, - shift, - cmd, - key: unmodified_chars.into(), - }, - })) - } + NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent { + keystroke: parse_keystroke(native_event), + is_held: native_event.isARepeat() == YES, + })), + NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent { + keystroke: parse_keystroke(native_event), + })), NSEventType::NSLeftMouseDown | NSEventType::NSRightMouseDown | NSEventType::NSOtherMouseDown => { @@ -231,17 +203,21 @@ impl Event { } } -unsafe fn get_key_text(native_event: id) -> Option<&'static str> { +unsafe fn parse_keystroke(native_event: id) -> Keystroke { + use cocoa::appkit::*; + + let modifiers = native_event.modifierFlags(); + let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); let unmodified_chars = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() .unwrap(); - let first_char = unmodified_chars.chars().next().map(|ch| ch as u16); - use cocoa::appkit::*; - #[allow(non_upper_case_globals)] - let unmodified_chars = match first_char { + let key = match unmodified_chars.chars().next().map(|ch| ch as u16) { Some(SPACE_KEY) => "space", Some(BACKSPACE_KEY) => "backspace", Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", @@ -267,8 +243,36 @@ unsafe fn get_key_text(native_event: id) -> Option<&'static str> { Some(NSF10FunctionKey) => "f10", Some(NSF11FunctionKey) => "f11", Some(NSF12FunctionKey) => "f12", - _ => unmodified_chars, + _ => { + if shift { + let chars_without_shift: id = msg_send![ + native_event, + charactersByApplyingModifiers: NSEventModifierFlags::empty() + ]; + let chars_without_shift = + CStr::from_ptr(chars_without_shift.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + + if chars_without_shift == unmodified_chars.to_ascii_lowercase() { + chars_without_shift + } else if chars_without_shift != unmodified_chars { + shift = false; + unmodified_chars + } else { + unmodified_chars + } + } else { + unmodified_chars + } + } }; - Some(unmodified_chars) + Keystroke { + ctrl, + alt, + shift, + cmd, + key: key.into(), + } } From 622596619c805ff09d189dbdeb38f359959aa0bd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Jul 2022 14:52:24 +0200 Subject: [PATCH 29/47] =?UTF-8?q?Honor=20=E2=8C=98=20when=20Dvorak-QWERTY?= =?UTF-8?q?=20is=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/gpui/src/platform/mac/event.rs | 46 +++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 050d69bcda5d7961aabf4d1b0cf541309c425032..b01934f20c7ee9415ea0cebbb985c7bb5a70e291 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -211,13 +211,14 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let unmodified_chars = + + let chars = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() .unwrap(); #[allow(non_upper_case_globals)] - let key = match unmodified_chars.chars().next().map(|ch| ch as u16) { + let key = match chars.chars().next().map(|ch| ch as u16) { Some(SPACE_KEY) => "space", Some(BACKSPACE_KEY) => "backspace", Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", @@ -244,26 +245,37 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { Some(NSF11FunctionKey) => "f11", Some(NSF12FunctionKey) => "f12", _ => { - if shift { - let chars_without_shift: id = msg_send![ - native_event, - charactersByApplyingModifiers: NSEventModifierFlags::empty() - ]; - let chars_without_shift = - CStr::from_ptr(chars_without_shift.UTF8String() as *mut c_char) - .to_str() - .unwrap(); + let chars_without_modifiers: id = msg_send![ + native_event, + charactersByApplyingModifiers: NSEventModifierFlags::empty() + ]; + let chars_without_modifiers = + CStr::from_ptr(chars_without_modifiers.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + + let chars_with_cmd: id = msg_send![ + native_event, + charactersByApplyingModifiers: NSEventModifierFlags::NSCommandKeyMask + ]; + let chars_with_cmd = CStr::from_ptr(chars_with_cmd.UTF8String() as *mut c_char) + .to_str() + .unwrap(); - if chars_without_shift == unmodified_chars.to_ascii_lowercase() { - chars_without_shift - } else if chars_without_shift != unmodified_chars { + if cmd && chars_without_modifiers != chars_with_cmd { + // Honor ⌘ when Dvorak-QWERTY is used. + chars_with_cmd + } else if shift { + if chars_without_modifiers == chars.to_ascii_lowercase() { + chars_without_modifiers + } else if chars_without_modifiers != chars { shift = false; - unmodified_chars + chars } else { - unmodified_chars + chars } } else { - unmodified_chars + chars } } }; From 2bfa3b90068a8352452fc205547ec19b7d5ecd7e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Jul 2022 15:51:34 +0200 Subject: [PATCH 30/47] Synthesize CGEvents instead of using `charactersByApplyingModifiers` --- crates/gpui/src/platform/mac/event.rs | 67 ++++++++++++++++----------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index b01934f20c7ee9415ea0cebbb985c7bb5a70e291..81c849d242809fdb0a0501e234ec15b2e9d3bb6a 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -10,7 +10,11 @@ use cocoa::{ base::{id, YES}, foundation::NSString as _, }; -use objc::{msg_send, sel, sel_impl}; +use core_graphics::{ + event::{CGEvent, CGEventFlags, CGKeyCode}, + event_source::{CGEventSource, CGEventSourceStateID}, +}; +use objc::{class, msg_send, sel, sel_impl}; use std::{borrow::Cow, ffi::CStr, os::raw::c_char}; const BACKSPACE_KEY: u16 = 0x7f; @@ -212,13 +216,13 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let chars = + let chars_ignoring_modifiers = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() .unwrap(); #[allow(non_upper_case_globals)] - let key = match chars.chars().next().map(|ch| ch as u16) { + let key = match chars_ignoring_modifiers.chars().next().map(|ch| ch as u16) { Some(SPACE_KEY) => "space", Some(BACKSPACE_KEY) => "backspace", Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", @@ -245,37 +249,26 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { Some(NSF11FunctionKey) => "f11", Some(NSF12FunctionKey) => "f12", _ => { - let chars_without_modifiers: id = msg_send![ - native_event, - charactersByApplyingModifiers: NSEventModifierFlags::empty() - ]; - let chars_without_modifiers = - CStr::from_ptr(chars_without_modifiers.UTF8String() as *mut c_char) - .to_str() - .unwrap(); - - let chars_with_cmd: id = msg_send![ - native_event, - charactersByApplyingModifiers: NSEventModifierFlags::NSCommandKeyMask - ]; - let chars_with_cmd = CStr::from_ptr(chars_with_cmd.UTF8String() as *mut c_char) - .to_str() - .unwrap(); - - if cmd && chars_without_modifiers != chars_with_cmd { + let chars_ignoring_modifiers_and_shift = + chars_for_modified_key(native_event.keyCode(), CGEventFlags::empty()); + let chars_with_cmd = + chars_for_modified_key(native_event.keyCode(), CGEventFlags::CGEventFlagCommand); + if cmd && chars_ignoring_modifiers_and_shift != chars_with_cmd { // Honor ⌘ when Dvorak-QWERTY is used. chars_with_cmd } else if shift { - if chars_without_modifiers == chars.to_ascii_lowercase() { - chars_without_modifiers - } else if chars_without_modifiers != chars { + if chars_ignoring_modifiers_and_shift + == chars_ignoring_modifiers.to_ascii_lowercase() + { + chars_ignoring_modifiers_and_shift + } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers { shift = false; - chars + chars_ignoring_modifiers } else { - chars + chars_ignoring_modifiers } } else { - chars + chars_ignoring_modifiers } } }; @@ -288,3 +281,23 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { key: key.into(), } } + +fn chars_for_modified_key<'a>(code: CGKeyCode, flags: CGEventFlags) -> &'a str { + // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that + // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing + // an event with the given flags instead lets us access `characters`, which always + // returns a valid string. + let event = CGEvent::new_keyboard_event( + CGEventSource::new(CGEventSourceStateID::Private).unwrap(), + code, + true, + ) + .unwrap(); + event.set_flags(flags); + let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] }; + unsafe { + CStr::from_ptr(event.characters().UTF8String()) + .to_str() + .unwrap() + } +} From 65fd943509d8913d8d2fe5e7f6f88db2044a83f5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Jul 2022 11:24:14 -0700 Subject: [PATCH 31/47] Move edited_ranges_for_transaction from BufferSnapshot to Buffer Co-authored-by: Antonio Scandurra --- crates/editor/src/editor.rs | 27 +++++++------- crates/text/src/text.rs | 72 ++++++++++++++++++------------------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 59a352cc9021f1f2114baac58e4584c767fe681b..aa185ae09be96f6e68af77049989d66fb1946518 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2480,14 +2480,17 @@ impl Editor { }); if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { if excerpted_buffer == *buffer { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - let excerpt_range = excerpt_range.to_offset(&snapshot); - if snapshot - .edited_ranges_for_transaction(transaction) - .all(|range| { - excerpt_range.start <= range.start && excerpt_range.end >= range.end - }) - { + let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + }); + + if all_edits_within_excerpt { return Ok(()); } } @@ -2500,12 +2503,12 @@ impl Editor { let mut ranges_to_highlight = Vec::new(); let excerpt_buffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - for (buffer, transaction) in &entries { - let snapshot = buffer.read(cx).snapshot(); + for (buffer_handle, transaction) in &entries { + let buffer = buffer_handle.read(cx); ranges_to_highlight.extend( multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - snapshot + buffer_handle.clone(), + buffer .edited_ranges_for_transaction::(transaction) .collect(), 1, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index da4dcfe490a3e419f308153374105aefbcbc98e4..792c8e3f1804b742057849aee9ecd7357d0fe710 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1322,6 +1322,42 @@ impl Buffer { self.history.finalize_last_transaction(); } + pub fn edited_ranges_for_transaction<'a, D>( + &'a self, + transaction: &'a Transaction, + ) -> impl 'a + Iterator> + where + D: TextDimension, + { + let mut cursor = self.fragments.cursor::<(VersionedFullOffset, usize)>(); + let mut rope_cursor = self.visible_text.cursor(0); + let cx = Some(transaction.end.clone()); + let mut position = D::default(); + transaction.ranges.iter().map(move |range| { + cursor.seek_forward(&VersionedFullOffset::Offset(range.start), Bias::Right, &cx); + let mut start_offset = cursor.start().1; + if cursor + .item() + .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) + { + start_offset += range.start - cursor.start().0.full_offset() + } + position.add_assign(&rope_cursor.summary(start_offset)); + let start = position.clone(); + + cursor.seek_forward(&VersionedFullOffset::Offset(range.end), Bias::Left, &cx); + let mut end_offset = cursor.start().1; + if cursor + .item() + .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) + { + end_offset += range.end - cursor.start().0.full_offset(); + } + position.add_assign(&rope_cursor.summary(end_offset)); + start..position.clone() + }) + } + pub fn subscribe(&mut self) -> Subscription { self.subscriptions.subscribe() } @@ -1878,42 +1914,6 @@ impl BufferSnapshot { self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX) } - pub fn edited_ranges_for_transaction<'a, D>( - &'a self, - transaction: &'a Transaction, - ) -> impl 'a + Iterator> - where - D: TextDimension, - { - let mut cursor = self.fragments.cursor::<(VersionedFullOffset, usize)>(); - let mut rope_cursor = self.visible_text.cursor(0); - let cx = Some(transaction.end.clone()); - let mut position = D::default(); - transaction.ranges.iter().map(move |range| { - cursor.seek_forward(&VersionedFullOffset::Offset(range.start), Bias::Right, &cx); - let mut start_offset = cursor.start().1; - if cursor - .item() - .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) - { - start_offset += range.start - cursor.start().0.full_offset() - } - position.add_assign(&rope_cursor.summary(start_offset)); - let start = position.clone(); - - cursor.seek_forward(&VersionedFullOffset::Offset(range.end), Bias::Left, &cx); - let mut end_offset = cursor.start().1; - if cursor - .item() - .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) - { - end_offset += range.end - cursor.start().0.full_offset(); - } - position.add_assign(&rope_cursor.summary(end_offset)); - start..position.clone() - }) - } - pub fn edits_since_in_range<'a, D>( &'a self, since: &'a clock::Global, From 7c3421e041195f26b00f9342616a7bdb97baffd5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Jul 2022 16:05:30 -0700 Subject: [PATCH 32/47] Remove versioned offset ranges from transactions and undo operations Now, instead of using these versioned offset ranges, we locate the fragments associated with a transaction using the transaction's edit ids. To make this possible, buffers now store a new map called `insertion_slices`, which lets you look up the ranges of insertions that were affected by a given edit. Co-authored-by: Antonio Scandurra --- crates/language/src/proto.rs | 16 -- crates/rpc/proto/zed.proto | 2 - crates/text/src/text.rs | 285 ++++++++++++++++------------------- 3 files changed, 131 insertions(+), 172 deletions(-) diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 5cb3feb2145bd8eba0e1308171bafaa14bbb13eb..c7c22c55c9a6ad731ee45644747b293b24d5ab06 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -39,11 +39,6 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation { local_timestamp: undo.id.value, lamport_timestamp: lamport_timestamp.value, version: serialize_version(&undo.version), - transaction_ranges: undo - .transaction_ranges - .iter() - .map(serialize_range) - .collect(), transaction_version: serialize_version(&undo.transaction_version), counts: undo .counts @@ -204,11 +199,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { ) }) .collect(), - transaction_ranges: undo - .transaction_ranges - .into_iter() - .map(deserialize_range) - .collect(), transaction_version: deserialize_version(undo.transaction_version), }, }), @@ -461,7 +451,6 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { .collect(), start: serialize_version(&transaction.start), end: serialize_version(&transaction.end), - ranges: transaction.ranges.iter().map(serialize_range).collect(), } } @@ -479,11 +468,6 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result, pub start: clock::Global, pub end: clock::Global, - pub ranges: Vec>, } #[derive(Clone, Copy, Debug, PartialEq)] @@ -114,71 +113,32 @@ impl HistoryEntry { self.transaction .end .observe(edit_operation.timestamp.local()); - - let mut edits = edit_operation - .ranges - .iter() - .zip(edit_operation.new_text.iter()) - .peekable(); - let mut new_ranges = Vec::new(); - let mut delta = 0; - - for mut self_range in self.transaction.ranges.iter().cloned() { - self_range.start += delta; - self_range.end += delta; - - while let Some((other_range, new_text)) = edits.peek() { - let insertion_len = new_text.len(); - let mut other_range = (*other_range).clone(); - other_range.start += delta; - other_range.end += delta; - - if other_range.start <= self_range.end { - edits.next().unwrap(); - delta += insertion_len; - - if other_range.end < self_range.start { - new_ranges.push(other_range.start..other_range.end + insertion_len); - self_range.start += insertion_len; - self_range.end += insertion_len; - } else { - self_range.start = cmp::min(self_range.start, other_range.start); - self_range.end = cmp::max(self_range.end, other_range.end) + insertion_len; - } - } else { - break; - } - } - - new_ranges.push(self_range); - } - - for (other_range, new_text) in edits { - let insertion_len = new_text.len(); - new_ranges.push(other_range.start + delta..other_range.end + delta + insertion_len); - delta += insertion_len; - } - - self.transaction.ranges = new_ranges; } } -#[derive(Clone)] struct History { // TODO: Turn this into a String or Rope, maybe. base_text: Arc, operations: HashMap, + insertion_slices: HashMap>, undo_stack: Vec, redo_stack: Vec, transaction_depth: usize, group_interval: Duration, } +#[derive(Clone, Debug)] +struct InsertionSlice { + insertion_id: clock::Local, + range: Range, +} + impl History { pub fn new(base_text: Arc) -> Self { Self { base_text, operations: Default::default(), + insertion_slices: Default::default(), undo_stack: Vec::new(), redo_stack: Vec::new(), transaction_depth: 0, @@ -205,7 +165,6 @@ impl History { start: start.clone(), end: start, edit_ids: Default::default(), - ranges: Default::default(), }, first_edit_at: now, last_edit_at: now, @@ -226,7 +185,7 @@ impl History { .last() .unwrap() .transaction - .ranges + .edit_ids .is_empty() { self.undo_stack.pop(); @@ -549,7 +508,6 @@ pub struct EditOperation { pub struct UndoOperation { pub id: clock::Local, pub counts: HashMap, - pub transaction_ranges: Vec>, pub transaction_version: clock::Global, pub version: clock::Global, } @@ -679,6 +637,7 @@ impl Buffer { }; let mut new_insertions = Vec::new(); let mut insertion_offset = 0; + let mut insertion_slices = Vec::new(); let mut edits = edits .map(|(range, new_text)| (range.to_offset(&*self), new_text)) @@ -737,10 +696,6 @@ impl Buffer { if !new_text.is_empty() { let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: fragment_start..fragment_start, - new: new_start..new_start + new_text.len(), - }); let fragment = Fragment { id: Locator::between( &new_fragments.summary().max_id, @@ -755,6 +710,11 @@ impl Buffer { max_undos: Default::default(), visible: true, }; + edits_patch.push(Edit { + old: fragment_start..fragment_start, + new: new_start..new_start + new_text.len(), + }); + insertion_slices.push(fragment.insertion_slice()); new_insertions.push(InsertionFragment::insert_new(&fragment)); new_ropes.push_str(new_text.as_ref()); new_fragments.push(fragment, &None); @@ -783,6 +743,7 @@ impl Buffer { old: fragment_start..intersection_end, new: new_start..new_start, }); + insertion_slices.push(intersection.insertion_slice()); } new_insertions.push(InsertionFragment::insert_new(&intersection)); new_ropes.push_fragment(&intersection, fragment.visible); @@ -825,6 +786,9 @@ impl Buffer { self.snapshot.visible_text = visible_text; self.snapshot.deleted_text = deleted_text; self.subscriptions.publish_mut(&edits_patch); + self.history + .insertion_slices + .insert(timestamp.local(), insertion_slices); edit_op } @@ -894,6 +858,7 @@ impl Buffer { let edits = ranges.into_iter().zip(new_text.into_iter()); let mut edits_patch = Patch::default(); + let mut insertion_slices = Vec::new(); let cx = Some(version.clone()); let mut new_insertions = Vec::new(); let mut insertion_offset = 0; @@ -984,10 +949,6 @@ impl Buffer { old_start += fragment_start.0 - old_fragments.start().0.full_offset().0; } let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: old_start..old_start, - new: new_start..new_start + new_text.len(), - }); let fragment = Fragment { id: Locator::between( &new_fragments.summary().max_id, @@ -1002,6 +963,11 @@ impl Buffer { max_undos: Default::default(), visible: true, }; + edits_patch.push(Edit { + old: old_start..old_start, + new: new_start..new_start + new_text.len(), + }); + insertion_slices.push(fragment.insertion_slice()); new_insertions.push(InsertionFragment::insert_new(&fragment)); new_ropes.push_str(new_text); new_fragments.push(fragment, &None); @@ -1023,6 +989,7 @@ impl Buffer { Locator::between(&new_fragments.summary().max_id, &intersection.id); intersection.deletions.insert(timestamp.local()); intersection.visible = false; + insertion_slices.push(intersection.insertion_slice()); } if intersection.len > 0 { if fragment.visible && !intersection.visible { @@ -1070,90 +1037,105 @@ impl Buffer { self.snapshot.visible_text = visible_text; self.snapshot.deleted_text = deleted_text; self.snapshot.insertions.edit(new_insertions, &()); + self.history + .insertion_slices + .insert(timestamp.local(), insertion_slices); self.subscriptions.publish_mut(&edits_patch) } - fn apply_undo(&mut self, undo: &UndoOperation) -> Result<()> { - let mut edits = Patch::default(); - self.snapshot.undo_map.insert(undo); + fn fragment_ids_for_edits<'a>( + &'a self, + edit_ids: impl Iterator, + ) -> Vec<&'a Locator> { + // Get all of the insertion slices changed by the given edits. + let mut insertion_slices = Vec::new(); + for edit_id in edit_ids { + if let Some(slices) = self.history.insertion_slices.get(edit_id) { + insertion_slices.extend_from_slice(slices) + } + } + insertion_slices + .sort_unstable_by_key(|s| (s.insertion_id, s.range.start, Reverse(s.range.end))); - let mut cx = undo.transaction_version.clone(); - for edit_id in undo.counts.keys().copied() { - cx.observe(edit_id); + // Get all of the fragments corresponding to these insertion slices. + let mut fragment_ids = Vec::new(); + let mut insertions_cursor = self.insertions.cursor::(); + for insertion_slice in &insertion_slices { + if insertion_slice.insertion_id != insertions_cursor.start().timestamp + || insertion_slice.range.start > insertions_cursor.start().split_offset + { + insertions_cursor.seek_forward( + &InsertionFragmentKey { + timestamp: insertion_slice.insertion_id, + split_offset: insertion_slice.range.start, + }, + Bias::Left, + &(), + ); + } + while let Some(item) = insertions_cursor.item() { + if item.timestamp != insertion_slice.insertion_id + || item.split_offset >= insertion_slice.range.end + { + break; + } + fragment_ids.push(&item.fragment_id); + insertions_cursor.next(&()); + } } - let cx = Some(cx); + fragment_ids.sort_unstable(); + fragment_ids + } - let mut old_fragments = self.fragments.cursor::<(VersionedFullOffset, usize)>(); - let mut new_fragments = old_fragments.slice( - &VersionedFullOffset::Offset(undo.transaction_ranges[0].start), - Bias::Right, - &cx, - ); + fn apply_undo(&mut self, undo: &UndoOperation) -> Result<()> { + self.snapshot.undo_map.insert(undo); + + let mut edits = Patch::default(); + let mut old_fragments = self.fragments.cursor::<(Option<&Locator>, usize)>(); + let mut new_fragments = SumTree::new(); let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - new_ropes.push_tree(new_fragments.summary().text); - for range in &undo.transaction_ranges { - let mut end_offset = old_fragments.end(&cx).0.full_offset(); + let fragment_ids = self.fragment_ids_for_edits(undo.counts.keys()); + for fragment_id in fragment_ids { + let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None); + new_ropes.push_tree(preceding_fragments.summary().text); + new_fragments.push_tree(preceding_fragments, &None); - if end_offset < range.start { - let preceding_fragments = old_fragments.slice( - &VersionedFullOffset::Offset(range.start), - Bias::Right, - &cx, - ); - new_ropes.push_tree(preceding_fragments.summary().text); - new_fragments.push_tree(preceding_fragments, &None); - } + if let Some(fragment) = old_fragments.item() { + let mut fragment = fragment.clone(); + let fragment_was_visible = fragment.visible; - while end_offset <= range.end { - if let Some(fragment) = old_fragments.item() { - let mut fragment = fragment.clone(); - let fragment_was_visible = fragment.visible; - - if fragment.was_visible(&undo.transaction_version, &self.undo_map) - || undo - .counts - .contains_key(&fragment.insertion_timestamp.local()) - { - fragment.visible = fragment.is_visible(&self.undo_map); - fragment.max_undos.observe(undo.id); - } - - let old_start = old_fragments.start().1; - let new_start = new_fragments.summary().text.visible; - if fragment_was_visible && !fragment.visible { - edits.push(Edit { - old: old_start..old_start + fragment.len, - new: new_start..new_start, - }); - } else if !fragment_was_visible && fragment.visible { - edits.push(Edit { - old: old_start..old_start, - new: new_start..new_start + fragment.len, - }); - } - new_ropes.push_fragment(&fragment, fragment_was_visible); - new_fragments.push(fragment, &None); + if fragment.was_visible(&undo.transaction_version, &self.undo_map) + || undo + .counts + .contains_key(&fragment.insertion_timestamp.local()) + { + fragment.visible = fragment.is_visible(&self.undo_map); + fragment.max_undos.observe(undo.id); + } - old_fragments.next(&cx); - if end_offset == old_fragments.end(&cx).0.full_offset() { - let unseen_fragments = old_fragments.slice( - &VersionedFullOffset::Offset(end_offset), - Bias::Right, - &cx, - ); - new_ropes.push_tree(unseen_fragments.summary().text); - new_fragments.push_tree(unseen_fragments, &None); - } - end_offset = old_fragments.end(&cx).0.full_offset(); - } else { - break; + let old_start = old_fragments.start().1; + let new_start = new_fragments.summary().text.visible; + if fragment_was_visible && !fragment.visible { + edits.push(Edit { + old: old_start..old_start + fragment.len, + new: new_start..new_start, + }); + } else if !fragment_was_visible && fragment.visible { + edits.push(Edit { + old: old_start..old_start, + new: new_start..new_start + fragment.len, + }); } + new_ropes.push_fragment(&fragment, fragment_was_visible); + new_fragments.push(fragment, &None); + + old_fragments.next(&None); } } - let suffix = old_fragments.suffix(&cx); + let suffix = old_fragments.suffix(&None); new_ropes.push_tree(suffix.summary().text); new_fragments.push_tree(suffix, &None); @@ -1304,7 +1286,6 @@ impl Buffer { id: self.local_clock.tick(), version: self.version(), counts, - transaction_ranges: transaction.ranges, transaction_version: transaction.start.clone(), }; self.apply_undo(&undo)?; @@ -1329,33 +1310,22 @@ impl Buffer { where D: TextDimension, { - let mut cursor = self.fragments.cursor::<(VersionedFullOffset, usize)>(); + let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(); let mut rope_cursor = self.visible_text.cursor(0); - let cx = Some(transaction.end.clone()); let mut position = D::default(); - transaction.ranges.iter().map(move |range| { - cursor.seek_forward(&VersionedFullOffset::Offset(range.start), Bias::Right, &cx); - let mut start_offset = cursor.start().1; - if cursor - .item() - .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) - { - start_offset += range.start - cursor.start().0.full_offset() - } - position.add_assign(&rope_cursor.summary(start_offset)); - let start = position.clone(); - - cursor.seek_forward(&VersionedFullOffset::Offset(range.end), Bias::Left, &cx); - let mut end_offset = cursor.start().1; - if cursor - .item() - .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) - { - end_offset += range.end - cursor.start().0.full_offset(); - } - position.add_assign(&rope_cursor.summary(end_offset)); - start..position.clone() - }) + self.fragment_ids_for_edits(transaction.edit_ids.iter()) + .into_iter() + .filter_map(move |fragment_id| { + cursor.seek_forward(&Some(fragment_id), Bias::Left, &None); + let fragment = cursor.item()?; + let start_offset = cursor.start().1; + let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 }; + position.add_assign(&rope_cursor.summary(start_offset)); + let start = position.clone(); + position.add_assign(&rope_cursor.summary(end_offset)); + let end = position.clone(); + Some(start..end) + }) } pub fn subscribe(&mut self) -> Subscription { @@ -2100,6 +2070,13 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo } impl Fragment { + fn insertion_slice(&self) -> InsertionSlice { + InsertionSlice { + insertion_id: self.insertion_timestamp.local(), + range: self.insertion_offset..self.insertion_offset + self.len, + } + } + fn is_visible(&self, undos: &UndoMap) -> bool { !undos.is_undone(self.insertion_timestamp.local()) && self.deletions.iter().all(|d| undos.is_undone(*d)) From 79f960b69e1521774039530f10f4c5d7231af775 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Jul 2022 17:41:31 -0700 Subject: [PATCH 33/47] Combine adjacent ranges in 'edited_ranges_for_transaction' --- crates/text/src/text.rs | 42 ++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 2b10c59bb96564c3c5635cd0fd19afb96c054a97..266cb402a0e938cf1da58b3068533212ea220a55 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1310,22 +1310,46 @@ impl Buffer { where D: TextDimension, { + // get fragment ranges let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(); - let mut rope_cursor = self.visible_text.cursor(0); - let mut position = D::default(); - self.fragment_ids_for_edits(transaction.edit_ids.iter()) + let offset_ranges = self + .fragment_ids_for_edits(transaction.edit_ids.iter()) .into_iter() .filter_map(move |fragment_id| { cursor.seek_forward(&Some(fragment_id), Bias::Left, &None); let fragment = cursor.item()?; let start_offset = cursor.start().1; let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 }; - position.add_assign(&rope_cursor.summary(start_offset)); - let start = position.clone(); - position.add_assign(&rope_cursor.summary(end_offset)); - let end = position.clone(); - Some(start..end) - }) + Some(start_offset..end_offset) + }); + + // combine adjacent ranges + let mut prev_range: Option> = None; + let disjoint_ranges = offset_ranges + .map(Some) + .chain([None]) + .filter_map(move |range| { + if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) { + if prev_range.end == range.start { + prev_range.end = range.end; + return None; + } + } + let result = prev_range.clone(); + prev_range = range; + result + }); + + // convert to the desired text dimension. + let mut position = D::default(); + let mut rope_cursor = self.visible_text.cursor(0); + disjoint_ranges.map(move |range| { + position.add_assign(&rope_cursor.summary(range.start)); + let start = position.clone(); + position.add_assign(&rope_cursor.summary(range.end)); + let end = position.clone(); + start..end + }) } pub fn subscribe(&mut self) -> Subscription { From 0fc73089320a4e7698bd44ed2a83093881dd7673 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 23 Jul 2022 09:27:21 +0200 Subject: [PATCH 34/47] Allow grouping local transactions even if remote peer edits in between --- crates/language/src/proto.rs | 2 -- crates/rpc/proto/zed.proto | 1 - crates/text/src/tests.rs | 2 ++ crates/text/src/text.rs | 21 ++++----------------- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c7c22c55c9a6ad731ee45644747b293b24d5ab06..4dca2f855dce645788d82901417eb1ca143859a0 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -450,7 +450,6 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { .map(serialize_local_timestamp) .collect(), start: serialize_version(&transaction.start), - end: serialize_version(&transaction.end), } } @@ -467,7 +466,6 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result, pub start: clock::Global, - pub end: clock::Global, } #[derive(Clone, Copy, Debug, PartialEq)] @@ -105,15 +104,6 @@ impl HistoryEntry { pub fn transaction_id(&self) -> TransactionId { self.transaction.id } - - fn push_edit(&mut self, edit_operation: &EditOperation) { - self.transaction - .edit_ids - .push(edit_operation.timestamp.local()); - self.transaction - .end - .observe(edit_operation.timestamp.local()); - } } struct History { @@ -162,8 +152,7 @@ impl History { self.undo_stack.push(HistoryEntry { transaction: Transaction { id, - start: start.clone(), - end: start, + start, edit_ids: Default::default(), }, first_edit_at: now, @@ -209,7 +198,6 @@ impl History { while let Some(prev_entry) = entries.next_back() { if !prev_entry.suppress_grouping && entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval - && entry.transaction.start == prev_entry.transaction.end { entry = prev_entry; new_len -= 1; @@ -223,13 +211,12 @@ impl History { if let Some(last_entry) = entries_to_keep.last_mut() { for entry in &*entries_to_merge { for edit_id in &entry.transaction.edit_ids { - last_entry.push_edit(self.operations[edit_id].as_edit().unwrap()); + last_entry.transaction.edit_ids.push(*edit_id); } } if let Some(entry) = entries_to_merge.last_mut() { last_entry.last_edit_at = entry.last_edit_at; - last_entry.transaction.end = entry.transaction.end.clone(); } } @@ -257,9 +244,9 @@ impl History { fn push_undo(&mut self, op_id: clock::Local) { assert_ne!(self.transaction_depth, 0); - if let Some(Operation::Edit(edit)) = self.operations.get(&op_id) { + if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { let last_transaction = self.undo_stack.last_mut().unwrap(); - last_transaction.push_edit(&edit); + last_transaction.transaction.edit_ids.push(op_id); } } From 555e705ccb5a84d84f27df3fa47d88ca1b338c61 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 23 Jul 2022 09:31:41 +0200 Subject: [PATCH 35/47] :art: --- crates/text/src/text.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 60ae833650519ae56b1490fd246219474786b090..838b20f2eec182c3728a025eacec6f2537ff9e14 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1083,8 +1083,7 @@ impl Buffer { let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let fragment_ids = self.fragment_ids_for_edits(undo.counts.keys()); - for fragment_id in fragment_ids { + for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) { let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None); new_ropes.push_tree(preceding_fragments.summary().text); new_fragments.push_tree(preceding_fragments, &None); From d3567e381cd944a172ed3e5d21f396017654bb0d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Jul 2022 09:53:51 +0200 Subject: [PATCH 36/47] Coalesce IME compositions into a single edit --- crates/editor/src/editor.rs | 87 +++++++++++++++++++++++++++++-- crates/editor/src/multi_buffer.rs | 51 ++++++++++++++++-- crates/language/src/buffer.rs | 4 ++ crates/text/src/tests.rs | 12 ++++- crates/text/src/text.rs | 29 +++++++++-- 5 files changed, 168 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aa185ae09be96f6e68af77049989d66fb1946518..bf3dfc220e4375e556f47419299b2769c3ce899e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -406,6 +406,7 @@ pub struct Editor { autoclose_stack: InvalidationStack, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, + ime_transaction: Option, active_diagnostics: Option, scroll_position: Vector2F, scroll_top_anchor: Anchor, @@ -993,6 +994,7 @@ impl Editor { autoclose_stack: Default::default(), snippet_stack: Default::default(), select_larger_syntax_node_stack: Vec::new(), + ime_transaction: Default::default(), active_diagnostics: None, soft_wrap_mode_override: None, get_field_editor_theme, @@ -3616,6 +3618,7 @@ impl Editor { }); } self.request_autoscroll(Autoscroll::Fit, cx); + self.unmark_text(cx); cx.emit(Event::Edited); } } @@ -3629,6 +3632,7 @@ impl Editor { }); } self.request_autoscroll(Autoscroll::Fit, cx); + self.unmark_text(cx); cx.emit(Event::Edited); } } @@ -5142,10 +5146,10 @@ impl Editor { &mut self, cx: &mut ViewContext, update: impl FnOnce(&mut Self, &mut ViewContext), - ) { + ) -> Option { self.start_transaction_at(Instant::now(), cx); update(self, cx); - self.end_transaction_at(Instant::now(), cx); + self.end_transaction_at(Instant::now(), cx) } fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { @@ -5159,7 +5163,11 @@ impl Editor { } } - fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut ViewContext, + ) -> Option { if let Some(tx_id) = self .buffer .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) @@ -5171,6 +5179,9 @@ impl Editor { } cx.emit(Event::Edited); + Some(tx_id) + } else { + None } } @@ -5908,6 +5919,7 @@ impl View for Editor { fn unmark_text(&mut self, cx: &mut ViewContext) { self.clear_text_highlights::(cx); + self.ime_transaction.take(); } fn replace_text_in_range( @@ -5928,8 +5940,15 @@ impl View for Editor { }); } this.handle_input(text, cx); - this.unmark_text(cx); }); + + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + self.unmark_text(cx); } fn replace_and_mark_text_in_range( @@ -5944,7 +5963,7 @@ impl View for Editor { return; } - self.transact(cx, |this, cx| { + let transaction = self.transact(cx, |this, cx| { let range_to_replace = if let Some(mut marked_range) = this.marked_text_range(cx) { if let Some(relative_range_utf16) = range_utf16.as_ref() { marked_range.end = marked_range.start + relative_range_utf16.end; @@ -5994,6 +6013,17 @@ impl View for Editor { }); } }); + + self.ime_transaction = self.ime_transaction.or(transaction); + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + if self.text_highlights::(cx).is_none() { + self.ime_transaction.take(); + } } } @@ -6591,6 +6621,53 @@ mod tests { }); } + #[gpui::test] + fn test_ime_composition(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); + let buffer = cx.add_model(|cx| { + let mut buffer = language::Buffer::new(0, "abcde", cx); + // Ensure automatic grouping doesn't occur. + buffer.set_group_interval(Duration::ZERO); + buffer + }); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(buffer.clone(), cx); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); + assert_eq!(editor.text(cx), "äbcde"); + assert_eq!(editor.marked_text_range(cx), Some(0..1)); + + // Finalize IME composition. + editor.replace_text_in_range(Some(0..1), "ā", cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_range(cx), None); + + // IME composition edits are grouped and are undone/redone at once. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "abcde"); + assert_eq!(editor.marked_text_range(cx), None); + editor.redo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_range(cx), None); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + assert_eq!(editor.marked_text_range(cx), Some(0..1)); + + // Undoing during an IME composition cancels it. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_range(cx), None); + + editor + }); + } + #[gpui::test] fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d52f693f1a1dc726bd9250e93cd2b83dd42b14a4..abf1689343712acd90116dbde952ce56fa9be490 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -558,6 +558,20 @@ impl MultiBuffer { self.history.finalize_last_transaction(); } + pub fn group_until_transaction( + &mut self, + transaction_id: TransactionId, + cx: &mut ModelContext, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.group_until_transaction(transaction_id) + }); + } else { + self.history.group_until(transaction_id); + } + } + pub fn set_active_selections( &mut self, selections: &[Selection], @@ -2701,9 +2715,8 @@ impl History { } fn group(&mut self) -> Option { - let mut new_len = self.undo_stack.len(); - let mut transactions = self.undo_stack.iter_mut(); - + let mut count = 0; + let mut transactions = self.undo_stack.iter(); if let Some(mut transaction) = transactions.next_back() { while let Some(prev_transaction) = transactions.next_back() { if !prev_transaction.suppress_grouping @@ -2711,13 +2724,31 @@ impl History { <= self.group_interval { transaction = prev_transaction; - new_len -= 1; + count += 1; } else { break; } } } + self.group_trailing(count) + } + + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for transaction in self.undo_stack.iter().rev() { + if transaction.id == transaction_id { + self.group_trailing(count); + break; + } else if transaction.suppress_grouping { + break; + } else { + count += 1; + } + } + } + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); if let Some(last_transaction) = transactions_to_keep.last_mut() { if let Some(transaction) = transactions_to_merge.last() { @@ -4143,7 +4174,7 @@ mod tests { let mut now = Instant::now(); multibuffer.update(cx, |multibuffer, cx| { - multibuffer.start_transaction_at(now, cx); + let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); multibuffer.edit( [ (Point::new(0, 0)..Point::new(0, 0), "A"), @@ -4226,6 +4257,16 @@ mod tests { assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); multibuffer.undo(cx); assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + // Transactions can be grouped manually. + multibuffer.redo(cx); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.group_until_transaction(transaction_1, cx); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); }); } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ee24539287aa97169a1b7ab49df4080c6240a40f..c413ef2de48e5b48ab4d4290c209a282ed7f377f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1076,6 +1076,10 @@ impl Buffer { self.text.finalize_last_transaction() } + pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { + self.text.group_until_transaction(transaction_id); + } + pub fn forget_transaction(&mut self, transaction_id: TransactionId) { self.text.forget_transaction(transaction_id); } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 2f67579f475e302b30a41981ed1fbbea111dd960..5d5fba2be01a055c9ee921b73afa80a8cd3e8a03 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -525,7 +525,7 @@ fn test_history() { let mut now = Instant::now(); let mut buffer = Buffer::new(0, 0, "123456".into()); - buffer.start_transaction_at(now); + let transaction_1 = buffer.start_transaction_at(now).unwrap(); buffer.edit([(2..4, "cd")]); buffer.end_transaction_at(now); assert_eq!(buffer.text(), "12cd56"); @@ -574,6 +574,16 @@ fn test_history() { assert_eq!(buffer.text(), "12cde6"); buffer.undo(); assert_eq!(buffer.text(), "123456"); + + // Transactions can be grouped manually. + buffer.redo(); + buffer.redo(); + assert_eq!(buffer.text(), "X12cde6"); + buffer.group_until_transaction(transaction_1); + buffer.undo(); + assert_eq!(buffer.text(), "123456"); + buffer.redo(); + assert_eq!(buffer.text(), "X12cde6"); } #[test] diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 838b20f2eec182c3728a025eacec6f2537ff9e14..1e56439fcccd9a6136a55b57125152f166e9eb15 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -191,22 +191,39 @@ impl History { } fn group(&mut self) -> Option { - let mut new_len = self.undo_stack.len(); - let mut entries = self.undo_stack.iter_mut(); - + let mut count = 0; + let mut entries = self.undo_stack.iter(); if let Some(mut entry) = entries.next_back() { while let Some(prev_entry) = entries.next_back() { if !prev_entry.suppress_grouping && entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval { entry = prev_entry; - new_len -= 1; + count += 1; } else { break; } } } + self.group_trailing(count) + } + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for entry in self.undo_stack.iter().rev() { + if entry.transaction_id() == transaction_id { + self.group_trailing(count); + break; + } else if entry.suppress_grouping { + break; + } else { + count += 1; + } + } + } + + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len); if let Some(last_entry) = entries_to_keep.last_mut() { for entry in &*entries_to_merge { @@ -1195,6 +1212,10 @@ impl Buffer { self.history.finalize_last_transaction() } + pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { + self.history.group_until(transaction_id); + } + pub fn base_text(&self) -> &Arc { &self.history.base_text } From acdfb933e86242cd8527105738dab1749409235d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Jul 2022 11:01:38 +0200 Subject: [PATCH 37/47] Honor shift if pressing command switches keyboard --- crates/gpui/src/platform/mac/event.rs | 30 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index f2bf5f8c913270fc1e7c6751863a74f9f2887a51..e0e178aa8c0833471d5cfb7be37a2a516138bfa6 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -216,7 +216,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let chars_ignoring_modifiers = + let mut chars_ignoring_modifiers = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() .unwrap(); @@ -249,14 +249,18 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { Some(NSF11FunctionKey) => "f11", Some(NSF12FunctionKey) => "f12", _ => { - let chars_ignoring_modifiers_and_shift = - chars_for_modified_key(native_event.keyCode(), CGEventFlags::empty()); - let chars_with_cmd = - chars_for_modified_key(native_event.keyCode(), CGEventFlags::CGEventFlagCommand); + let mut chars_ignoring_modifiers_and_shift = + chars_for_modified_key(native_event.keyCode(), false, false); + + // Honor ⌘ when Dvorak-QWERTY is used. + let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false); if cmd && chars_ignoring_modifiers_and_shift != chars_with_cmd { - // Honor ⌘ when Dvorak-QWERTY is used. - chars_with_cmd - } else if shift { + chars_ignoring_modifiers = + chars_for_modified_key(native_event.keyCode(), true, shift); + chars_ignoring_modifiers_and_shift = chars_with_cmd; + } + + if shift { if chars_ignoring_modifiers_and_shift == chars_ignoring_modifiers.to_ascii_lowercase() { @@ -282,7 +286,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { } } -fn chars_for_modified_key<'a>(code: CGKeyCode, flags: CGEventFlags) -> &'a str { +fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str { // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing // an event with the given flags instead lets us access `characters`, which always @@ -293,7 +297,15 @@ fn chars_for_modified_key<'a>(code: CGKeyCode, flags: CGEventFlags) -> &'a str { true, ) .unwrap(); + let mut flags = CGEventFlags::empty(); + if cmd { + flags |= CGEventFlags::CGEventFlagCommand; + } + if shift { + flags |= CGEventFlags::CGEventFlagShift; + } event.set_flags(flags); + let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] }; unsafe { CStr::from_ptr(event.characters().UTF8String()) From bb55d654ce83c444707b9cf8a7c6f7316b305de5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Jul 2022 14:50:09 +0200 Subject: [PATCH 38/47] Handle IME composition with multiple cursors --- crates/editor/src/editor.rs | 110 +++++++++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 27 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2a187c58b2f316bd8d1861abd023f83b22a5236c..29d51f6e0f17f269f967f693a1a65a7aefe7887b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5739,6 +5739,17 @@ impl Editor { }) .detach() } + + fn marked_text_ranges<'a>( + &'a self, + cx: &'a AppContext, + ) -> Option>> { + let snapshot = self.buffer.read(cx).read(cx); + let (_, ranges) = self.text_highlights::(cx)?; + Some(ranges.into_iter().map(move |range| { + range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) + })) + } } impl EditorSnapshot { @@ -5922,11 +5933,8 @@ impl View for Editor { } fn marked_text_range(&self, cx: &AppContext) -> Option> { - let range = self.text_highlights::(cx)?.1.get(0)?; - let snapshot = self.buffer.read(cx).read(cx); - let range_utf16 = - range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot); - Some(range_utf16.start.0..range_utf16.end.0) + let range = self.marked_text_ranges(cx)?.next()?; + Some(range.start.0..range.end.0) } fn unmark_text(&mut self, cx: &mut ViewContext) { @@ -5946,9 +5954,32 @@ impl View for Editor { } self.transact(cx, |this, cx| { - if let Some(range) = range_utf16.or_else(|| this.marked_text_range(cx)) { + let new_selected_ranges = if let Some(range_utf16) = range_utf16 { + let selected_range = this.selected_text_range(cx).unwrap(); + let start_delta = range_utf16.start as isize - selected_range.start as isize; + let end_delta = range_utf16.end as isize - selected_range.end as isize; + Some( + this.selections + .all::(cx) + .into_iter() + .map(|mut selection| { + selection.start.0 = + (selection.start.0 as isize).saturating_add(start_delta) as usize; + selection.end.0 = + (selection.end.0 as isize).saturating_add(end_delta) as usize; + selection.start..selection.end + }) + .collect::>(), + ) + } else if let Some(marked_ranges) = this.marked_text_ranges(cx) { + Some(marked_ranges.collect()) + } else { + None + }; + + if let Some(new_selected_ranges) = new_selected_ranges { this.change_selections(None, cx, |selections| { - selections.select_ranges([OffsetUtf16(range.start)..OffsetUtf16(range.end)]) + selections.select_ranges(new_selected_ranges) }); } this.handle_input(text, cx); @@ -5976,35 +6007,56 @@ impl View for Editor { } let transaction = self.transact(cx, |this, cx| { - let range_to_replace = if let Some(mut marked_range) = this.marked_text_range(cx) { + let ranges_to_replace = if let Some(marked_ranges) = this.marked_text_ranges(cx) { + let mut marked_ranges = marked_ranges.collect::>(); if let Some(relative_range_utf16) = range_utf16.as_ref() { - marked_range.end = marked_range.start + relative_range_utf16.end; - marked_range.start += relative_range_utf16.start; + for marked_range in &mut marked_ranges { + marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; + marked_range.start.0 += relative_range_utf16.start; + } } - Some(marked_range) + Some(marked_ranges) } else if let Some(range_utf16) = range_utf16 { - Some(range_utf16) + let selected_range = this.selected_text_range(cx).unwrap(); + let start_delta = range_utf16.start as isize - selected_range.start as isize; + let end_delta = range_utf16.end as isize - selected_range.end as isize; + Some( + this.selections + .all::(cx) + .into_iter() + .map(|mut selection| { + selection.start.0 = + (selection.start.0 as isize).saturating_add(start_delta) as usize; + selection.end.0 = + (selection.end.0 as isize).saturating_add(end_delta) as usize; + selection.start..selection.end + }) + .collect::>(), + ) } else { None }; - if let Some(range) = range_to_replace { - this.change_selections(None, cx, |s| { - s.select_ranges([OffsetUtf16(range.start)..OffsetUtf16(range.end)]) - }); + if let Some(ranges) = ranges_to_replace { + this.change_selections(None, cx, |s| s.select_ranges(ranges)); } - let selection = this.selections.newest_anchor(); - let marked_range = { + let marked_ranges = { let snapshot = this.buffer.read(cx).read(cx); - selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot) + this.selections + .disjoint_anchors() + .into_iter() + .map(|selection| { + selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot) + }) + .collect::>() }; if text.is_empty() { this.unmark_text(cx); } else { this.highlight_text::( - vec![marked_range.clone()], + marked_ranges.clone(), this.style(cx).composition_mark, cx, ); @@ -6012,16 +6064,20 @@ impl View for Editor { this.handle_input(text, cx); - if let Some(mut new_selected_range) = new_selected_range_utf16 { + if let Some(new_selected_range) = new_selected_range_utf16 { let snapshot = this.buffer.read(cx).read(cx); - let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; - new_selected_range.start += insertion_start; - new_selected_range.end += insertion_start; + let new_selected_ranges = marked_ranges + .into_iter() + .map(|marked_range| { + let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; + OffsetUtf16(new_selected_range.start + insertion_start) + ..OffsetUtf16(new_selected_range.end + insertion_start) + }) + .collect::>(); + drop(snapshot); this.change_selections(None, cx, |selections| { - selections - .select_ranges([OffsetUtf16(new_selected_range.start) - ..OffsetUtf16(new_selected_range.end)]) + selections.select_ranges(new_selected_ranges) }); } }); From c46be992e07e286699e5f2e8247120c46148f6a9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Jul 2022 15:02:45 +0200 Subject: [PATCH 39/47] Introduce `Rope::clip_offset_utf16` --- crates/editor/src/multi_buffer.rs | 4 +-- crates/text/src/rope.rs | 59 +++++++++++++++++++++++++++---- crates/text/src/tests.rs | 10 +++--- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index abf1689343712acd90116dbde952ce56fa9be490..71d0053e567194c7d0b7657626938a744e91d6ab 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1888,7 +1888,7 @@ impl MultiBufferSnapshot { .offset_to_offset_utf16(excerpt_start_offset + overshoot); *start_offset_utf16 + (buffer_offset_utf16 - excerpt_start_offset_utf16) } else { - OffsetUtf16(self.excerpts.summary().text.len_utf16) + self.excerpts.summary().text.len_utf16 } } @@ -2935,7 +2935,7 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for OffsetUtf16 { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - self.0 += summary.text.len_utf16; + *self += summary.text.len_utf16; } } diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index 8d278bb8f78ebd7d3c9fc953c86b4621088fe670..0cdb3b299e7f428aab7ae4d5952debf9b4597443 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -166,7 +166,7 @@ impl Rope { pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { if offset >= self.summary().len { - return OffsetUtf16(self.summary().len_utf16); + return self.summary().len_utf16; } let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(); cursor.seek(&offset, Bias::Left, &()); @@ -178,7 +178,7 @@ impl Rope { } pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { - if offset.0 >= self.summary().len_utf16 { + if offset >= self.summary().len_utf16 { return self.summary().len; } let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(); @@ -291,6 +291,17 @@ impl Rope { } } + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = offset - cursor.start(); + *cursor.start() + chunk.clip_offset_utf16(overshoot, bias) + } else { + self.summary().len_utf16 + } + } + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { let mut cursor = self.chunks.cursor::(); cursor.seek(&point, Bias::Right, &()); @@ -765,6 +776,18 @@ impl Chunk { } unreachable!() } + + fn clip_offset_utf16(&self, target: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut code_units = self.0.encode_utf16(); + let mut offset = code_units.by_ref().take(target.0 as usize).count(); + if char::decode_utf16(code_units).next().transpose().is_err() { + match bias { + Bias::Left => offset -= 1, + Bias::Right => offset += 1, + } + } + OffsetUtf16(offset) + } } impl sum_tree::Item for Chunk { @@ -802,7 +825,7 @@ impl sum_tree::Summary for ChunkSummary { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TextSummary { pub len: usize, - pub len_utf16: usize, + pub len_utf16: OffsetUtf16, pub lines: Point, pub lines_utf16: PointUtf16, pub first_line_chars: u32, @@ -813,7 +836,7 @@ pub struct TextSummary { impl<'a> From<&'a str> for TextSummary { fn from(text: &'a str) -> Self { - let mut len_utf16 = 0; + let mut len_utf16 = OffsetUtf16(0); let mut lines = Point::new(0, 0); let mut lines_utf16 = PointUtf16::new(0, 0); let mut first_line_chars = 0; @@ -821,7 +844,7 @@ impl<'a> From<&'a str> for TextSummary { let mut longest_row = 0; let mut longest_row_chars = 0; for c in text.chars() { - len_utf16 += c.len_utf16(); + len_utf16.0 += c.len_utf16(); if c == '\n' { lines += Point::new(1, 0); @@ -961,13 +984,13 @@ impl TextDimension for usize { impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 { fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { - self.0 += summary.text.len_utf16; + *self += summary.text.len_utf16; } } impl TextDimension for OffsetUtf16 { fn from_text_summary(summary: &TextSummary) -> Self { - Self(summary.len_utf16) + summary.len_utf16 } fn add_assign(&mut self, other: &Self) { @@ -1075,6 +1098,19 @@ mod tests { rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right), PointUtf16::new(0, 2) ); + + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Left), + OffsetUtf16(0) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Right), + OffsetUtf16(2) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(3), Bias::Right), + OffsetUtf16(2) + ); } #[gpui::test(iterations = 100)] @@ -1174,8 +1210,16 @@ mod tests { offset_utf16.0 += ch.len_utf16(); } + let mut offset_utf16 = OffsetUtf16(0); let mut point_utf16 = PointUtf16::zero(); for unit in expected.encode_utf16() { + let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left); + let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right); + assert!(right_offset >= left_offset); + // Ensure translating UTF-16 offsets to UTF-8 offsets doesn't panic. + actual.offset_utf16_to_offset(left_offset); + actual.offset_utf16_to_offset(right_offset); + let left_point = actual.clip_point_utf16(point_utf16, Bias::Left); let right_point = actual.clip_point_utf16(point_utf16, Bias::Right); assert!(right_point >= left_point); @@ -1183,6 +1227,7 @@ mod tests { actual.point_utf16_to_offset(left_point); actual.point_utf16_to_offset(right_point); + offset_utf16.0 += 1; if unit == b'\n' as u16 { point_utf16 += PointUtf16::new(1, 0); } else { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 5d5fba2be01a055c9ee921b73afa80a8cd3e8a03..d9f7440e8d2935f37e096610d7de0d289398eee6 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -248,7 +248,7 @@ fn test_text_summary_for_range() { buffer.text_summary_for_range::(1..3), TextSummary { len: 2, - len_utf16: 2, + len_utf16: OffsetUtf16(2), lines: Point::new(1, 0), lines_utf16: PointUtf16::new(1, 0), first_line_chars: 1, @@ -261,7 +261,7 @@ fn test_text_summary_for_range() { buffer.text_summary_for_range::(1..12), TextSummary { len: 11, - len_utf16: 11, + len_utf16: OffsetUtf16(11), lines: Point::new(3, 0), lines_utf16: PointUtf16::new(3, 0), first_line_chars: 1, @@ -274,7 +274,7 @@ fn test_text_summary_for_range() { buffer.text_summary_for_range::(0..20), TextSummary { len: 20, - len_utf16: 20, + len_utf16: OffsetUtf16(20), lines: Point::new(4, 1), lines_utf16: PointUtf16::new(4, 1), first_line_chars: 2, @@ -287,7 +287,7 @@ fn test_text_summary_for_range() { buffer.text_summary_for_range::(0..22), TextSummary { len: 22, - len_utf16: 22, + len_utf16: OffsetUtf16(22), lines: Point::new(4, 3), lines_utf16: PointUtf16::new(4, 3), first_line_chars: 2, @@ -300,7 +300,7 @@ fn test_text_summary_for_range() { buffer.text_summary_for_range::(7..22), TextSummary { len: 15, - len_utf16: 15, + len_utf16: OffsetUtf16(15), lines: Point::new(2, 3), lines_utf16: PointUtf16::new(2, 3), first_line_chars: 4, From 47e8bd5f4f57617840055244cbb935d9f75d25c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Jul 2022 15:06:04 +0200 Subject: [PATCH 40/47] Introduce `MultiBuffer::clip_offset_utf16` --- crates/editor/src/multi_buffer.rs | 19 +++++++++++++++++++ crates/text/src/text.rs | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 71d0053e567194c7d0b7657626938a744e91d6ab..78d7e94ae5b37060408d1747aee8fca0dee487db 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1692,6 +1692,25 @@ impl MultiBufferSnapshot { *cursor.start() + overshoot } + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_offset_utf16(offset, bias); + } + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + let overshoot = if let Some(excerpt) = cursor.item() { + let excerpt_start = excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); + let buffer_offset = excerpt + .buffer + .clip_offset_utf16(excerpt_start + (offset - cursor.start()), bias); + OffsetUtf16(buffer_offset.0.saturating_sub(excerpt_start.0)) + } else { + OffsetUtf16(0) + }; + *cursor.start() + overshoot + } + pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { if let Some((_, _, buffer)) = self.as_singleton() { return buffer.clip_point_utf16(point, bias); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 1e56439fcccd9a6136a55b57125152f166e9eb15..7da77b22ffff69a72b71937187cb97dee7003d71 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1901,6 +1901,10 @@ impl BufferSnapshot { self.visible_text.clip_point(point, bias) } + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + self.visible_text.clip_offset_utf16(offset, bias) + } + pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { self.visible_text.clip_point_utf16(point, bias) } From ff99f8d0cadbcf9e2ea1f2f7327e768d5e1cfa56 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Jul 2022 17:32:33 +0200 Subject: [PATCH 41/47] Clip UTF-16 offsets provided by Cocoa when composing IME input --- crates/editor/src/editor.rs | 110 +++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 29d51f6e0f17f269f967f693a1a65a7aefe7887b..8859d93c0709d048de59184edd01db53e896c4de 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5740,15 +5740,42 @@ impl Editor { .detach() } - fn marked_text_ranges<'a>( - &'a self, - cx: &'a AppContext, - ) -> Option>> { + fn marked_text_ranges(&self, cx: &AppContext) -> Option>> { let snapshot = self.buffer.read(cx).read(cx); let (_, ranges) = self.text_highlights::(cx)?; - Some(ranges.into_iter().map(move |range| { - range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) - })) + Some( + ranges + .into_iter() + .map(move |range| { + range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) + }) + .collect(), + ) + } + + fn selection_replacement_ranges( + &self, + range: Range, + cx: &AppContext, + ) -> Vec> { + let selections = self.selections.all::(cx); + let newest_selection = selections + .iter() + .max_by_key(|selection| selection.id) + .unwrap(); + let start_delta = range.start.0 as isize - newest_selection.start.0 as isize; + let end_delta = range.end.0 as isize - newest_selection.end.0 as isize; + let snapshot = self.buffer.read(cx).read(cx); + selections + .into_iter() + .map(|mut selection| { + selection.start.0 = + (selection.start.0 as isize).saturating_add(start_delta) as usize; + selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize; + snapshot.clip_offset_utf16(selection.start, Bias::Left) + ..snapshot.clip_offset_utf16(selection.end, Bias::Right) + }) + .collect() } } @@ -5933,8 +5960,9 @@ impl View for Editor { } fn marked_text_range(&self, cx: &AppContext) -> Option> { - let range = self.marked_text_ranges(cx)?.next()?; - Some(range.start.0..range.end.0) + let snapshot = self.buffer.read(cx).read(cx); + let range = self.text_highlights::(cx)?.1.get(0)?; + Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } fn unmark_text(&mut self, cx: &mut ViewContext) { @@ -5955,24 +5983,10 @@ impl View for Editor { self.transact(cx, |this, cx| { let new_selected_ranges = if let Some(range_utf16) = range_utf16 { - let selected_range = this.selected_text_range(cx).unwrap(); - let start_delta = range_utf16.start as isize - selected_range.start as isize; - let end_delta = range_utf16.end as isize - selected_range.end as isize; - Some( - this.selections - .all::(cx) - .into_iter() - .map(|mut selection| { - selection.start.0 = - (selection.start.0 as isize).saturating_add(start_delta) as usize; - selection.end.0 = - (selection.end.0 as isize).saturating_add(end_delta) as usize; - selection.start..selection.end - }) - .collect::>(), - ) + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) } else if let Some(marked_ranges) = this.marked_text_ranges(cx) { - Some(marked_ranges.collect()) + Some(marked_ranges) } else { None }; @@ -6007,32 +6021,22 @@ impl View for Editor { } let transaction = self.transact(cx, |this, cx| { - let ranges_to_replace = if let Some(marked_ranges) = this.marked_text_ranges(cx) { - let mut marked_ranges = marked_ranges.collect::>(); + let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { + let snapshot = this.buffer.read(cx).read(cx); if let Some(relative_range_utf16) = range_utf16.as_ref() { for marked_range in &mut marked_ranges { marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; marked_range.start.0 += relative_range_utf16.start; + marked_range.start = + snapshot.clip_offset_utf16(marked_range.start, Bias::Left); + marked_range.end = + snapshot.clip_offset_utf16(marked_range.end, Bias::Right); } } Some(marked_ranges) } else if let Some(range_utf16) = range_utf16 { - let selected_range = this.selected_text_range(cx).unwrap(); - let start_delta = range_utf16.start as isize - selected_range.start as isize; - let end_delta = range_utf16.end as isize - selected_range.end as isize; - Some( - this.selections - .all::(cx) - .into_iter() - .map(|mut selection| { - selection.start.0 = - (selection.start.0 as isize).saturating_add(start_delta) as usize; - selection.end.0 = - (selection.end.0 as isize).saturating_add(end_delta) as usize; - selection.start..selection.end - }) - .collect::>(), - ) + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) } else { None }; @@ -6070,8 +6074,10 @@ impl View for Editor { .into_iter() .map(|marked_range| { let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; - OffsetUtf16(new_selected_range.start + insertion_start) - ..OffsetUtf16(new_selected_range.end + insertion_start) + let new_start = OffsetUtf16(new_selected_range.start + insertion_start); + let new_end = OffsetUtf16(new_selected_range.end + insertion_start); + snapshot.clip_offset_utf16(new_start, Bias::Left) + ..snapshot.clip_offset_utf16(new_end, Bias::Right) }) .collect::>(); @@ -6711,7 +6717,7 @@ mod tests { assert_eq!(editor.marked_text_range(cx), Some(0..1)); // Finalize IME composition. - editor.replace_text_in_range(Some(0..1), "ā", cx); + editor.replace_text_in_range(None, "ā", cx); assert_eq!(editor.text(cx), "ābcde"); assert_eq!(editor.marked_text_range(cx), None); @@ -6732,6 +6738,16 @@ mod tests { assert_eq!(editor.text(cx), "ābcde"); assert_eq!(editor.marked_text_range(cx), None); + // Start a new IME composition with an invalid marked range, ensuring it gets clipped. + editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); + assert_eq!(editor.text(cx), "ābcdè"); + assert_eq!(editor.marked_text_range(cx), Some(4..5)); + + // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. + editor.replace_text_in_range(Some(4..999), "ę", cx); + assert_eq!(editor.text(cx), "ābcdę"); + assert_eq!(editor.marked_text_range(cx), None); + editor }); } From 6dc27cbba26306a67a874cbd9652233082ae051f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Jul 2022 17:47:10 +0200 Subject: [PATCH 42/47] Add test for IME composition with multiple cursors --- crates/editor/src/editor.rs | 61 ++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8859d93c0709d048de59184edd01db53e896c4de..263ba2f1c84da553322558294c0a1c3267ca532b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6714,39 +6714,84 @@ mod tests { editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); assert_eq!(editor.text(cx), "äbcde"); - assert_eq!(editor.marked_text_range(cx), Some(0..1)); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); // Finalize IME composition. editor.replace_text_in_range(None, "ā", cx); assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_range(cx), None); + assert_eq!(editor.marked_text_ranges(cx), None); // IME composition edits are grouped and are undone/redone at once. editor.undo(&Default::default(), cx); assert_eq!(editor.text(cx), "abcde"); - assert_eq!(editor.marked_text_range(cx), None); + assert_eq!(editor.marked_text_ranges(cx), None); editor.redo(&Default::default(), cx); assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_range(cx), None); + assert_eq!(editor.marked_text_ranges(cx), None); // Start a new IME composition. editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); - assert_eq!(editor.marked_text_range(cx), Some(0..1)); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); // Undoing during an IME composition cancels it. editor.undo(&Default::default(), cx); assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_range(cx), None); + assert_eq!(editor.marked_text_ranges(cx), None); // Start a new IME composition with an invalid marked range, ensuring it gets clipped. editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); assert_eq!(editor.text(cx), "ābcdè"); - assert_eq!(editor.marked_text_range(cx), Some(4..5)); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) + ); // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. editor.replace_text_in_range(Some(4..999), "ę", cx); assert_eq!(editor.text(cx), "ābcdę"); - assert_eq!(editor.marked_text_range(cx), None); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with multiple cursors. + editor.change_selections(None, cx, |s| { + s.select_ranges([ + OffsetUtf16(1)..OffsetUtf16(1), + OffsetUtf16(3)..OffsetUtf16(3), + OffsetUtf16(5)..OffsetUtf16(5), + ]) + }); + editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); + assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(0)..OffsetUtf16(3), + OffsetUtf16(4)..OffsetUtf16(7), + OffsetUtf16(8)..OffsetUtf16(11) + ]) + ); + + // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. + editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); + assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(1)..OffsetUtf16(2), + OffsetUtf16(5)..OffsetUtf16(6), + OffsetUtf16(9)..OffsetUtf16(10) + ]) + ); + + // Finalize IME composition with multiple cursors. + editor.replace_text_in_range(Some(9..10), "2", cx); + assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); + assert_eq!(editor.marked_text_ranges(cx), None); editor }); From 4c8d9384b17d95ab2109acbfff29b13940ef41d3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Jul 2022 11:35:42 -0700 Subject: [PATCH 43/47] Replaces lines_utf16 with a single u32 - last_line_len_utf16 --- crates/editor/src/multi_buffer.rs | 12 +++++------ crates/text/src/rope.rs | 36 ++++++++++++++++++++----------- crates/text/src/tests.rs | 10 ++++----- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 78d7e94ae5b37060408d1747aee8fca0dee487db..00ef7b11a0943d30deefc0c9a21f2e666da25c45 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1818,7 +1818,7 @@ impl MultiBufferSnapshot { .offset_to_point_utf16(excerpt_start_offset + overshoot); *start_point + (buffer_point - excerpt_start_point) } else { - self.excerpts.summary().text.lines_utf16 + self.excerpts.summary().text.lines_utf16() } } @@ -1840,7 +1840,7 @@ impl MultiBufferSnapshot { .point_to_point_utf16(excerpt_start_point + overshoot); *start_point + (buffer_point - excerpt_start_point_utf16) } else { - self.excerpts.summary().text.lines_utf16 + self.excerpts.summary().text.lines_utf16() } } @@ -2966,7 +2966,7 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point { impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.lines_utf16 + *self += summary.text.lines_utf16() } } @@ -3951,7 +3951,7 @@ mod tests { let mut buffer_offset = buffer_range.start; let mut point = excerpt_start.lines; let mut buffer_point = buffer_start_point; - let mut point_utf16 = excerpt_start.lines_utf16; + let mut point_utf16 = excerpt_start.lines_utf16(); let mut buffer_point_utf16 = buffer_start_point_utf16; for ch in buffer .snapshot() @@ -4034,7 +4034,7 @@ mod tests { buffer.clip_point_utf16(buffer_point_utf16, Bias::Right); assert_eq!( left_point_utf16, - excerpt_start.lines_utf16 + excerpt_start.lines_utf16() + (buffer_left_point_utf16 - buffer_start_point_utf16), "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}", point_utf16, @@ -4043,7 +4043,7 @@ mod tests { ); assert_eq!( right_point_utf16, - excerpt_start.lines_utf16 + excerpt_start.lines_utf16() + (buffer_right_point_utf16 - buffer_start_point_utf16), "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}", point_utf16, diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index 0cdb3b299e7f428aab7ae4d5952debf9b4597443..012b7fdd66afbb9294b98afb71ec3fc4ab0e58f6 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -205,7 +205,7 @@ impl Rope { pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { if offset >= self.summary().len { - return self.summary().lines_utf16; + return self.summary().lines_utf16(); } let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(); cursor.seek(&offset, Bias::Left, &()); @@ -218,7 +218,7 @@ impl Rope { pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { if point >= self.summary().lines { - return self.summary().lines_utf16; + return self.summary().lines_utf16(); } let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>(); cursor.seek(&point, Bias::Left, &()); @@ -243,7 +243,7 @@ impl Rope { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - if point >= self.summary().lines_utf16 { + if point >= self.summary().lines_utf16() { return self.summary().len; } let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(); @@ -256,7 +256,7 @@ impl Rope { } pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - if point >= self.summary().lines_utf16 { + if point >= self.summary().lines_utf16() { return self.summary().lines; } let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(); @@ -320,7 +320,7 @@ impl Rope { let overshoot = point - cursor.start(); *cursor.start() + chunk.clip_point_utf16(overshoot, bias) } else { - self.summary().lines_utf16 + self.summary().lines_utf16() } } @@ -827,20 +827,29 @@ pub struct TextSummary { pub len: usize, pub len_utf16: OffsetUtf16, pub lines: Point, - pub lines_utf16: PointUtf16, pub first_line_chars: u32, pub last_line_chars: u32, + pub last_line_len_utf16: u32, pub longest_row: u32, pub longest_row_chars: u32, } +impl TextSummary { + pub fn lines_utf16(&self) -> PointUtf16 { + PointUtf16 { + row: self.lines.row, + column: self.last_line_len_utf16, + } + } +} + impl<'a> From<&'a str> for TextSummary { fn from(text: &'a str) -> Self { let mut len_utf16 = OffsetUtf16(0); let mut lines = Point::new(0, 0); - let mut lines_utf16 = PointUtf16::new(0, 0); let mut first_line_chars = 0; let mut last_line_chars = 0; + let mut last_line_len_utf16 = 0; let mut longest_row = 0; let mut longest_row_chars = 0; for c in text.chars() { @@ -848,11 +857,11 @@ impl<'a> From<&'a str> for TextSummary { if c == '\n' { lines += Point::new(1, 0); - lines_utf16 += PointUtf16::new(1, 0); + last_line_len_utf16 = 0; last_line_chars = 0; } else { lines.column += c.len_utf8() as u32; - lines_utf16.column += c.len_utf16() as u32; + last_line_len_utf16 += c.len_utf16() as u32; last_line_chars += 1; } @@ -870,9 +879,9 @@ impl<'a> From<&'a str> for TextSummary { len: text.len(), len_utf16, lines, - lines_utf16, first_line_chars, last_line_chars, + last_line_len_utf16, longest_row, longest_row_chars, } @@ -914,14 +923,15 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { if other.lines.row == 0 { self.last_line_chars += other.first_line_chars; + self.last_line_len_utf16 += other.last_line_len_utf16; } else { self.last_line_chars = other.last_line_chars; + self.last_line_len_utf16 = other.last_line_len_utf16; } self.len += other.len; self.len_utf16 += other.len_utf16; self.lines += other.lines; - self.lines_utf16 += other.lines_utf16; } } @@ -1016,13 +1026,13 @@ impl TextDimension for Point { impl<'a> sum_tree::Dimension<'a, ChunkSummary> for PointUtf16 { fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { - *self += summary.text.lines_utf16; + *self += summary.text.lines_utf16(); } } impl TextDimension for PointUtf16 { fn from_text_summary(summary: &TextSummary) -> Self { - summary.lines_utf16 + summary.lines_utf16() } fn add_assign(&mut self, other: &Self) { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index d9f7440e8d2935f37e096610d7de0d289398eee6..33d02acf5a87d34781f743422ad515dc0b971870 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -250,9 +250,9 @@ fn test_text_summary_for_range() { len: 2, len_utf16: OffsetUtf16(2), lines: Point::new(1, 0), - lines_utf16: PointUtf16::new(1, 0), first_line_chars: 1, last_line_chars: 0, + last_line_len_utf16: 0, longest_row: 0, longest_row_chars: 1, } @@ -263,9 +263,9 @@ fn test_text_summary_for_range() { len: 11, len_utf16: OffsetUtf16(11), lines: Point::new(3, 0), - lines_utf16: PointUtf16::new(3, 0), first_line_chars: 1, last_line_chars: 0, + last_line_len_utf16: 0, longest_row: 2, longest_row_chars: 4, } @@ -276,9 +276,9 @@ fn test_text_summary_for_range() { len: 20, len_utf16: OffsetUtf16(20), lines: Point::new(4, 1), - lines_utf16: PointUtf16::new(4, 1), first_line_chars: 2, last_line_chars: 1, + last_line_len_utf16: 1, longest_row: 3, longest_row_chars: 6, } @@ -289,9 +289,9 @@ fn test_text_summary_for_range() { len: 22, len_utf16: OffsetUtf16(22), lines: Point::new(4, 3), - lines_utf16: PointUtf16::new(4, 3), first_line_chars: 2, last_line_chars: 3, + last_line_len_utf16: 3, longest_row: 3, longest_row_chars: 6, } @@ -302,9 +302,9 @@ fn test_text_summary_for_range() { len: 15, len_utf16: OffsetUtf16(15), lines: Point::new(2, 3), - lines_utf16: PointUtf16::new(2, 3), first_line_chars: 4, last_line_chars: 3, + last_line_len_utf16: 3, longest_row: 1, longest_row_chars: 6, } From 09c0211c967734808c71948f18547f24ced1a61e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Jul 2022 11:36:52 -0700 Subject: [PATCH 44/47] Bump the RPC protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 5fff19bb065b95b2da5eb3be25f579fcd41df1fa..015ac10707c7cd7960a30f3c13cf350f433e0ce6 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 27; +pub const PROTOCOL_VERSION: u32 = 28; From a54d9f16977ba04173da55883950270458b5fe9b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Jul 2022 11:42:54 -0700 Subject: [PATCH 45/47] Add comment about noop performKeyEquivalent function --- crates/gpui/src/platform/mac/window.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index a41a42f1587ee060cc258352679a134790d05664..b34da93c45d5b7015fd32e708ca5b7306c1614fe 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -253,6 +253,8 @@ unsafe fn build_classes() { attributed_substring_for_proposed_range as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, ); + + // Suppress beep on keystrokes with modifier keys. decl.add_method( sel!(doCommandBySelector:), do_command_by_selector as extern "C" fn(&Object, Sel, Sel), @@ -1145,9 +1147,7 @@ extern "C" fn attributed_substring_for_proposed_range( .unwrap_or(nil) } -extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) { - // -} +extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {} async fn synthetic_drag( window_state: Weak>, From ab037fe8443200d174e8eca0aedf87275b363eaf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Jul 2022 12:20:12 -0700 Subject: [PATCH 46/47] Simulate calling of text-insertion APIs in TestAppContext::dispatch_keystroke --- crates/editor/src/editor.rs | 16 ++++++++-------- crates/editor/src/test.rs | 6 ------ crates/gpui/src/app.rs | 27 ++++++++++++++++++++++----- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 263ba2f1c84da553322558294c0a1c3267ca532b..d92bd04251b34fae0a739afa56aad04101e3079e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9917,7 +9917,7 @@ mod tests { one| two three"}); - cx.simulate_input("."); + cx.simulate_keystroke("."); handle_completion_request( &mut cx, indoc! {" @@ -9963,9 +9963,9 @@ mod tests { two| three| additional edit"}); - cx.simulate_input(" "); + cx.simulate_keystroke(" "); assert!(cx.editor(|e, _| e.context_menu.is_none())); - cx.simulate_input("s"); + cx.simulate_keystroke("s"); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.assert_editor_state(indoc! {" @@ -9986,7 +9986,7 @@ mod tests { cx.condition(|editor, _| editor.context_menu_visible()) .await; - cx.simulate_input("i"); + cx.simulate_keystroke("i"); handle_completion_request( &mut cx, @@ -10021,11 +10021,11 @@ mod tests { }) }); cx.set_state("editor|"); - cx.simulate_input("."); + cx.simulate_keystroke("."); assert!(cx.editor(|e, _| e.context_menu.is_none())); - cx.simulate_input("c"); - cx.simulate_input("l"); - cx.simulate_input("o"); + cx.simulate_keystroke("c"); + cx.simulate_keystroke("l"); + cx.simulate_keystroke("o"); cx.assert_editor_state("editor.clo|"); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.update_editor(|editor, cx| { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 86928e5b8c3ef1b87e52f9cceb896f9151d56835..18c13a4ba6dbe7e060f8e00eb7f4b7e3a75f70db 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -165,12 +165,6 @@ impl<'a> EditorTestContext<'a> { }) } - pub fn simulate_input(&mut self, input: &str) { - self.editor.update(self.cx, |editor, cx| { - editor.handle_input(input, cx); - }); - } - pub fn update_buffer(&mut self, update: F) -> T where F: FnOnce(&mut Buffer, &mut ModelContext) -> T, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f4968ed2bc35cd5d171395d1e10fc1d2f5450d12..b06bb642a242cc68ec6758ef4c090f93f412240b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -475,7 +475,7 @@ impl TestAppContext { } pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { - self.cx.borrow_mut().update(|cx| { + let handled = self.cx.borrow_mut().update(|cx| { let presenter = cx .presenters_and_platform_windows .get(&window_id) @@ -484,12 +484,29 @@ impl TestAppContext { .clone(); let dispatch_path = presenter.borrow().dispatch_path(cx.as_ref()); - if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) { - presenter - .borrow_mut() - .dispatch_event(Event::KeyDown(KeyDownEvent { keystroke, is_held }), cx); + if cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) { + return true; } + if presenter.borrow_mut().dispatch_event( + Event::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held, + }), + cx, + ) { + return true; + } + + false }); + + if !handled && !keystroke.cmd && !keystroke.ctrl { + WindowInputHandler { + app: self.cx.clone(), + window_id, + } + .replace_text_in_range(None, &keystroke.key) + } } pub fn add_model(&mut self, build_model: F) -> ModelHandle From 13097ea1106e077e813b75b653a2930970744e2f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Jul 2022 12:21:26 -0700 Subject: [PATCH 47/47] Update terminal test to reflect new text insertion approach --- crates/terminal/src/mappings/keys.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 93fd7b34543b3cee73e10a9a6f098b6433cd385a..f88bfa927ac7f650251bd04c5762a71365562626 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -305,17 +305,15 @@ mod test { } #[test] - fn test_multi_char_fallthrough() { + fn test_plain_inputs() { let ks = Keystroke { ctrl: false, alt: false, shift: false, cmd: false, - key: "🖖🏻".to_string(), //2 char string }; - - assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("🖖🏻".to_string())); + assert_eq!(to_esc_str(&ks, &TermMode::NONE), None); } #[test]