diff --git a/Cargo.lock b/Cargo.lock index 8eb8d6987a30fde77d616ff1ac2b363799003a53..82e9197631ac8497fbf998641dbf7f779bedca98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6104,7 +6104,6 @@ dependencies = [ "libsqlite3-sys", "parking_lot 0.11.2", "smol", - "sqlez_macros", "thread_local", "uuid 1.2.2", ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 12873a3e4e6a4515a02fe161dce4c57a49efabe4..824fb63c0f35969efeda1bb9cb2ded01e386d539 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -315,7 +315,9 @@ { "context": "Editor && VimWaiting", "bindings": { - "*": "gpui::KeyPressed" + "tab": "vim::Tab", + "enter": "vim::Enter", + "escape": "editor::Cancel" } } ] \ No newline at end of file diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index d4625cbce0525e845ff2b85def695bcb707a029f..5b5d8f1162f352166ca2e9f733dabd2d3b129906 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -65,7 +65,7 @@ impl CommandPalette { action, keystrokes: bindings .iter() - .filter_map(|binding| binding.keystrokes()) + .map(|binding| binding.keystrokes()) .last() .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()), }) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index e32276df41f78fa5365ea3fdfea2ec717143d043..99a74fe7f22f037efe142f0b4c7ca6e48539cae3 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -337,7 +337,7 @@ impl DisplaySnapshot { .map(|h| h.text) } - // Returns text chunks starting at the end of the given display row in reverse until the start of the file + /// Returns text chunks starting at the end of the given display row in reverse until the start of the file pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.blocks_snapshot @@ -411,6 +411,67 @@ impl DisplaySnapshot { }) } + /// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from` + /// Stops if `condition` returns false for any of the character position pairs observed. + pub fn find_while<'a>( + &'a self, + from: DisplayPoint, + target: &str, + condition: impl FnMut(char, DisplayPoint) -> bool + 'a, + ) -> impl Iterator + 'a { + Self::find_internal(self.chars_at(from), target.chars().collect(), condition) + } + + /// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from` + /// Stops if `condition` returns false for any of the character position pairs observed. + pub fn reverse_find_while<'a>( + &'a self, + from: DisplayPoint, + target: &str, + condition: impl FnMut(char, DisplayPoint) -> bool + 'a, + ) -> impl Iterator + 'a { + Self::find_internal( + self.reverse_chars_at(from), + target.chars().rev().collect(), + condition, + ) + } + + fn find_internal<'a>( + iterator: impl Iterator + 'a, + target: Vec, + mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a, + ) -> impl Iterator + 'a { + // List of partial matches with the index of the last seen character in target and the starting point of the match + let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new(); + iterator + .take_while(move |(ch, point)| condition(*ch, *point)) + .filter_map(move |(ch, point)| { + if Some(&ch) == target.get(0) { + partial_matches.push((0, point)); + } + + let mut found = None; + // Keep partial matches that have the correct next character + partial_matches.retain_mut(|(match_position, match_start)| { + if target.get(*match_position) == Some(&ch) { + *match_position += 1; + if *match_position == target.len() { + found = Some(match_start.clone()); + // This match is completed. No need to keep tracking it + false + } else { + true + } + } else { + false + } + }); + + found + }) + } + pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 { let mut count = 0; let mut column = 0; @@ -627,7 +688,7 @@ pub mod tests { use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; - use util::test::{marked_text_ranges, sample_text}; + use util::test::{marked_text_offsets, marked_text_ranges, sample_text}; use Bias::*; #[gpui::test(iterations = 100)] @@ -1418,6 +1479,32 @@ pub mod tests { ) } + #[test] + fn test_find_internal() { + assert("This is a ˇtest of find internal", "test"); + assert("Some text ˇaˇaˇaa with repeated characters", "aa"); + + fn assert(marked_text: &str, target: &str) { + let (text, expected_offsets) = marked_text_offsets(marked_text); + + let chars = text + .chars() + .enumerate() + .map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32))); + let target = target.chars(); + + assert_eq!( + expected_offsets + .into_iter() + .map(|offset| offset as u32) + .collect::>(), + DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true) + .map(|point| point.column()) + .collect::>() + ) + } + } + fn syntax_chunks<'a>( rows: Range, map: &ModelHandle, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f25eefcb7d365e6ce13007e1fbd3a0bc2de4dd26..c649fed7ce81f47e538668e7c87751326a2eb9be 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -400,7 +400,7 @@ pub enum SelectMode { All, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum EditorMode { SingleLine, AutoHeight { max_lines: usize }, @@ -1732,11 +1732,13 @@ impl Editor { } pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { + let text: Arc = text.into(); + if !self.input_enabled { + cx.emit(Event::InputIgnored { text }); return; } - let text: Arc = text.into(); let selections = self.selections.all_adjusted(cx); let mut edits = Vec::new(); let mut new_selections = Vec::with_capacity(selections.len()); @@ -6187,6 +6189,9 @@ impl Deref for EditorSnapshot { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + InputIgnored { + text: Arc, + }, ExcerptsAdded { buffer: ModelHandle, predecessor: ExcerptId, @@ -6253,8 +6258,10 @@ impl View for Editor { } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - let focused_event = EditorFocused(cx.handle()); - cx.emit_global(focused_event); + if cx.is_self_focused() { + let focused_event = EditorFocused(cx.handle()); + cx.emit_global(focused_event); + } if let Some(rename) = self.pending_rename.as_ref() { cx.focus(&rename.editor); } else { @@ -6393,26 +6400,29 @@ impl View for Editor { text: &str, cx: &mut ViewContext, ) { - if !self.input_enabled { - return; - } - self.transact(cx, |this, cx| { - let new_selected_ranges = if let Some(range_utf16) = range_utf16 { - let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); - Some(this.selection_replacement_ranges(range_utf16, cx)) - } else { - this.marked_text_ranges(cx) - }; + if this.input_enabled { + let new_selected_ranges = if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + this.marked_text_ranges(cx) + }; - if let Some(new_selected_ranges) = new_selected_ranges { - this.change_selections(None, cx, |selections| { - selections.select_ranges(new_selected_ranges) - }); + if let Some(new_selected_ranges) = new_selected_ranges { + this.change_selections(None, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } } + this.handle_input(text, cx); }); + if !self.input_enabled { + return; + } + if let Some(transaction) = self.ime_transaction { self.buffer.update(cx, |buffer, cx| { buffer.group_until_transaction(transaction, cx); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 6f7199aa33cc7a08cb5478a20c114e27a9cdc3cc..60adadb96cfec3572f5dfec7d20e0cb4ef6f3a98 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1,7 +1,10 @@ pub mod action; mod callback_collection; +mod menu; +pub(crate) mod ref_counts; #[cfg(any(test, feature = "test-support"))] pub mod test_app_context; +mod window_input_handler; use std::{ any::{type_name, Any, TypeId}, @@ -19,34 +22,38 @@ use std::{ }; use anyhow::{anyhow, Context, Result}; -use lazy_static::lazy_static; use parking_lot::Mutex; use pathfinder_geometry::vector::Vector2F; use postage::oneshot; use smallvec::SmallVec; use smol::prelude::*; +use uuid::Uuid; pub use action::*; use callback_collection::CallbackCollection; use collections::{hash_map::Entry, HashMap, HashSet, VecDeque}; +pub use menu::*; use platform::Event; #[cfg(any(test, feature = "test-support"))] +use ref_counts::LeakDetector; +#[cfg(any(test, feature = "test-support"))] pub use test_app_context::{ContextHandle, TestAppContext}; -use uuid::Uuid; +use window_input_handler::WindowInputHandler; use crate::{ elements::ElementBox, executor::{self, Task}, - geometry::rect::RectF, keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult}, platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, - Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, KeyUpEvent, + Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache, WindowBounds, }; +use self::ref_counts::RefCounts; + pub trait Entity: 'static { type Event; @@ -174,31 +181,12 @@ pub trait UpdateView { T: View; } -pub struct Menu<'a> { - pub name: &'a str, - pub items: Vec>, -} - -pub enum MenuItem<'a> { - Separator, - Submenu(Menu<'a>), - Action { - name: &'a str, - action: Box, - }, -} - #[derive(Clone)] pub struct App(Rc>); #[derive(Clone)] pub struct AsyncAppContext(Rc>); -pub struct WindowInputHandler { - app: Rc>, - window_id: usize, -} - impl App { pub fn new(asset_source: impl AssetSource) -> Result { let platform = platform::current::platform(); @@ -220,33 +208,7 @@ impl App { cx.borrow_mut().quit(); } })); - foreground_platform.on_will_open_menu(Box::new({ - let cx = app.0.clone(); - move || { - let mut cx = cx.borrow_mut(); - cx.keystroke_matcher.clear_pending(); - } - })); - foreground_platform.on_validate_menu_command(Box::new({ - let cx = app.0.clone(); - move |action| { - let cx = cx.borrow_mut(); - !cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action) - } - })); - foreground_platform.on_menu_command(Box::new({ - let cx = app.0.clone(); - move |action| { - let mut cx = cx.borrow_mut(); - if let Some(key_window_id) = cx.cx.platform.key_window_id() { - if let Some(view_id) = cx.focused_view_id(key_window_id) { - cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action); - return; - } - } - cx.dispatch_global_action_any(action); - } - })); + setup_menu_handlers(foreground_platform.as_ref(), &app); app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0)); Ok(app) @@ -349,94 +311,6 @@ impl App { } } -impl WindowInputHandler { - fn read_focused_view(&self, f: F) -> Option - where - F: FnOnce(&dyn AnyView, &AppContext) -> T, - { - // Input-related application hooks are sometimes called by the OS during - // a call to a window-manipulation API, like prompting the user for file - // paths. In that case, the AppContext will already be borrowed, so any - // InputHandler methods need to fail gracefully. - // - // See https://github.com/zed-industries/community/issues/444 - let app = self.app.try_borrow().ok()?; - - 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.try_borrow_mut().ok()?; - 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 { - self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) - .flatten() - } - - fn selected_text_range(&self) -> Option> { - self.read_focused_view(|view, cx| view.selected_text_range(cx)) - .flatten() - } - - fn replace_text_in_range(&mut self, range: Option>, text: &str) { - 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> { - self.read_focused_view(|view, cx| view.marked_text_range(cx)) - .flatten() - } - - fn unmark_text(&mut self) { - 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>, - ) { - 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, - ); - }); - } - - 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) - } -} - impl AsyncAppContext { pub fn spawn(&self, f: F) -> Task where @@ -984,11 +858,6 @@ impl MutableAppContext { result } - pub fn set_menus(&mut self, menus: Vec) { - self.foreground_platform - .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(); @@ -1350,7 +1219,7 @@ impl MutableAppContext { .bindings_for_action_type(action.as_any().type_id()) .find_map(|b| { if b.match_context(&contexts) { - b.keystrokes().map(|s| s.into()) + Some(b.keystrokes().into()) } else { None } @@ -4025,7 +3894,7 @@ impl<'a, T: View> ViewContext<'a, T> { }) } - pub fn observe_keystroke(&mut self, mut callback: F) -> Subscription + pub fn observe_keystrokes(&mut self, mut callback: F) -> Subscription where F: 'static + FnMut( @@ -5280,205 +5149,6 @@ impl Subscription { } } -lazy_static! { - static ref LEAK_BACKTRACE: bool = - std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()); -} - -#[cfg(any(test, feature = "test-support"))] -#[derive(Default)] -pub struct LeakDetector { - next_handle_id: usize, - #[allow(clippy::type_complexity)] - handle_backtraces: HashMap< - usize, - ( - Option<&'static str>, - HashMap>, - ), - >, -} - -#[cfg(any(test, feature = "test-support"))] -impl LeakDetector { - fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize { - let handle_id = post_inc(&mut self.next_handle_id); - let entry = self.handle_backtraces.entry(entity_id).or_default(); - let backtrace = if *LEAK_BACKTRACE { - Some(backtrace::Backtrace::new_unresolved()) - } else { - None - }; - if let Some(type_name) = type_name { - entry.0.get_or_insert(type_name); - } - entry.1.insert(handle_id, backtrace); - handle_id - } - - fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) { - if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { - assert!(backtraces.remove(&handle_id).is_some()); - if backtraces.is_empty() { - self.handle_backtraces.remove(&entity_id); - } - } - } - - pub fn assert_dropped(&mut self, entity_id: usize) { - if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { - for trace in backtraces.values_mut().flatten() { - trace.resolve(); - eprintln!("{:?}", crate::util::CwdBacktrace(trace)); - } - - let hint = if *LEAK_BACKTRACE { - "" - } else { - " – set LEAK_BACKTRACE=1 for more information" - }; - - panic!( - "{} handles to {} {} still exist{}", - backtraces.len(), - type_name.unwrap_or("entity"), - entity_id, - hint - ); - } - } - - pub fn detect(&mut self) { - let mut found_leaks = false; - for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() { - eprintln!( - "leaked {} handles to {} {}", - backtraces.len(), - type_name.unwrap_or("entity"), - id - ); - for trace in backtraces.values_mut().flatten() { - trace.resolve(); - eprintln!("{:?}", crate::util::CwdBacktrace(trace)); - } - found_leaks = true; - } - - let hint = if *LEAK_BACKTRACE { - "" - } else { - " – set LEAK_BACKTRACE=1 for more information" - }; - assert!(!found_leaks, "detected leaked handles{}", hint); - } -} - -#[derive(Default)] -struct RefCounts { - entity_counts: HashMap, - element_state_counts: HashMap, - dropped_models: HashSet, - dropped_views: HashSet<(usize, usize)>, - dropped_element_states: HashSet, - - #[cfg(any(test, feature = "test-support"))] - leak_detector: Arc>, -} - -struct ElementStateRefCount { - ref_count: usize, - frame_id: usize, -} - -impl RefCounts { - fn inc_model(&mut self, model_id: usize) { - match self.entity_counts.entry(model_id) { - Entry::Occupied(mut entry) => { - *entry.get_mut() += 1; - } - Entry::Vacant(entry) => { - entry.insert(1); - self.dropped_models.remove(&model_id); - } - } - } - - fn inc_view(&mut self, window_id: usize, view_id: usize) { - match self.entity_counts.entry(view_id) { - Entry::Occupied(mut entry) => *entry.get_mut() += 1, - Entry::Vacant(entry) => { - entry.insert(1); - self.dropped_views.remove(&(window_id, view_id)); - } - } - } - - fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) { - match self.element_state_counts.entry(id) { - Entry::Occupied(mut entry) => { - let entry = entry.get_mut(); - if entry.frame_id == frame_id || entry.ref_count >= 2 { - panic!("used the same element state more than once in the same frame"); - } - entry.ref_count += 1; - entry.frame_id = frame_id; - } - Entry::Vacant(entry) => { - entry.insert(ElementStateRefCount { - ref_count: 1, - frame_id, - }); - self.dropped_element_states.remove(&id); - } - } - } - - fn dec_model(&mut self, model_id: usize) { - let count = self.entity_counts.get_mut(&model_id).unwrap(); - *count -= 1; - if *count == 0 { - self.entity_counts.remove(&model_id); - self.dropped_models.insert(model_id); - } - } - - fn dec_view(&mut self, window_id: usize, view_id: usize) { - let count = self.entity_counts.get_mut(&view_id).unwrap(); - *count -= 1; - if *count == 0 { - self.entity_counts.remove(&view_id); - self.dropped_views.insert((window_id, view_id)); - } - } - - fn dec_element_state(&mut self, id: ElementStateId) { - let entry = self.element_state_counts.get_mut(&id).unwrap(); - entry.ref_count -= 1; - if entry.ref_count == 0 { - self.element_state_counts.remove(&id); - self.dropped_element_states.insert(id); - } - } - - fn is_entity_alive(&self, entity_id: usize) -> bool { - self.entity_counts.contains_key(&entity_id) - } - - fn take_dropped( - &mut self, - ) -> ( - HashSet, - HashSet<(usize, usize)>, - HashSet, - ) { - ( - std::mem::take(&mut self.dropped_models), - std::mem::take(&mut self.dropped_views), - std::mem::take(&mut self.dropped_element_states), - ) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/gpui/src/app/menu.rs b/crates/gpui/src/app/menu.rs new file mode 100644 index 0000000000000000000000000000000000000000..2234bfa3911cf892792470c446e0a25c3e56af7e --- /dev/null +++ b/crates/gpui/src/app/menu.rs @@ -0,0 +1,52 @@ +use crate::{Action, App, ForegroundPlatform, MutableAppContext}; + +pub struct Menu<'a> { + pub name: &'a str, + pub items: Vec>, +} + +pub enum MenuItem<'a> { + Separator, + Submenu(Menu<'a>), + Action { + name: &'a str, + action: Box, + }, +} + +impl MutableAppContext { + pub fn set_menus(&mut self, menus: Vec) { + self.foreground_platform + .set_menus(menus, &self.keystroke_matcher); + } +} + +pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, app: &App) { + foreground_platform.on_will_open_menu(Box::new({ + let cx = app.0.clone(); + move || { + let mut cx = cx.borrow_mut(); + cx.keystroke_matcher.clear_pending(); + } + })); + foreground_platform.on_validate_menu_command(Box::new({ + let cx = app.0.clone(); + move |action| { + let cx = cx.borrow_mut(); + !cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action) + } + })); + foreground_platform.on_menu_command(Box::new({ + let cx = app.0.clone(); + move |action| { + let mut cx = cx.borrow_mut(); + if let Some(key_window_id) = cx.cx.platform.key_window_id() { + if let Some(view_id) = cx.focused_view_id(key_window_id) { + cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action); + return; + } + } + cx.dispatch_global_action_any(action); + } + })); +} diff --git a/crates/gpui/src/app/ref_counts.rs b/crates/gpui/src/app/ref_counts.rs new file mode 100644 index 0000000000000000000000000000000000000000..a9ae6e7a6c90456559dab8594ffa26c1960d9f11 --- /dev/null +++ b/crates/gpui/src/app/ref_counts.rs @@ -0,0 +1,217 @@ +use std::sync::Arc; + +use lazy_static::lazy_static; +use parking_lot::Mutex; + +use collections::{hash_map::Entry, HashMap, HashSet}; + +use crate::{util::post_inc, ElementStateId}; + +lazy_static! { + static ref LEAK_BACKTRACE: bool = + std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()); +} + +struct ElementStateRefCount { + ref_count: usize, + frame_id: usize, +} + +#[derive(Default)] +pub struct RefCounts { + entity_counts: HashMap, + element_state_counts: HashMap, + dropped_models: HashSet, + dropped_views: HashSet<(usize, usize)>, + dropped_element_states: HashSet, + + #[cfg(any(test, feature = "test-support"))] + pub leak_detector: Arc>, +} + +impl RefCounts { + pub fn new( + #[cfg(any(test, feature = "test-support"))] leak_detector: Arc>, + ) -> Self { + Self { + #[cfg(any(test, feature = "test-support"))] + leak_detector, + ..Default::default() + } + } + + pub fn inc_model(&mut self, model_id: usize) { + match self.entity_counts.entry(model_id) { + Entry::Occupied(mut entry) => { + *entry.get_mut() += 1; + } + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_models.remove(&model_id); + } + } + } + + pub fn inc_view(&mut self, window_id: usize, view_id: usize) { + match self.entity_counts.entry(view_id) { + Entry::Occupied(mut entry) => *entry.get_mut() += 1, + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_views.remove(&(window_id, view_id)); + } + } + } + + pub fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) { + match self.element_state_counts.entry(id) { + Entry::Occupied(mut entry) => { + let entry = entry.get_mut(); + if entry.frame_id == frame_id || entry.ref_count >= 2 { + panic!("used the same element state more than once in the same frame"); + } + entry.ref_count += 1; + entry.frame_id = frame_id; + } + Entry::Vacant(entry) => { + entry.insert(ElementStateRefCount { + ref_count: 1, + frame_id, + }); + self.dropped_element_states.remove(&id); + } + } + } + + pub fn dec_model(&mut self, model_id: usize) { + let count = self.entity_counts.get_mut(&model_id).unwrap(); + *count -= 1; + if *count == 0 { + self.entity_counts.remove(&model_id); + self.dropped_models.insert(model_id); + } + } + + pub fn dec_view(&mut self, window_id: usize, view_id: usize) { + let count = self.entity_counts.get_mut(&view_id).unwrap(); + *count -= 1; + if *count == 0 { + self.entity_counts.remove(&view_id); + self.dropped_views.insert((window_id, view_id)); + } + } + + pub fn dec_element_state(&mut self, id: ElementStateId) { + let entry = self.element_state_counts.get_mut(&id).unwrap(); + entry.ref_count -= 1; + if entry.ref_count == 0 { + self.element_state_counts.remove(&id); + self.dropped_element_states.insert(id); + } + } + + pub fn is_entity_alive(&self, entity_id: usize) -> bool { + self.entity_counts.contains_key(&entity_id) + } + + pub fn take_dropped( + &mut self, + ) -> ( + HashSet, + HashSet<(usize, usize)>, + HashSet, + ) { + ( + std::mem::take(&mut self.dropped_models), + std::mem::take(&mut self.dropped_views), + std::mem::take(&mut self.dropped_element_states), + ) + } +} + +#[cfg(any(test, feature = "test-support"))] +#[derive(Default)] +pub struct LeakDetector { + next_handle_id: usize, + #[allow(clippy::type_complexity)] + handle_backtraces: HashMap< + usize, + ( + Option<&'static str>, + HashMap>, + ), + >, +} + +#[cfg(any(test, feature = "test-support"))] +impl LeakDetector { + pub fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize { + let handle_id = post_inc(&mut self.next_handle_id); + let entry = self.handle_backtraces.entry(entity_id).or_default(); + let backtrace = if *LEAK_BACKTRACE { + Some(backtrace::Backtrace::new_unresolved()) + } else { + None + }; + if let Some(type_name) = type_name { + entry.0.get_or_insert(type_name); + } + entry.1.insert(handle_id, backtrace); + handle_id + } + + pub fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) { + if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { + assert!(backtraces.remove(&handle_id).is_some()); + if backtraces.is_empty() { + self.handle_backtraces.remove(&entity_id); + } + } + } + + pub fn assert_dropped(&mut self, entity_id: usize) { + if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { + for trace in backtraces.values_mut().flatten() { + trace.resolve(); + eprintln!("{:?}", crate::util::CwdBacktrace(trace)); + } + + let hint = if *LEAK_BACKTRACE { + "" + } else { + " – set LEAK_BACKTRACE=1 for more information" + }; + + panic!( + "{} handles to {} {} still exist{}", + backtraces.len(), + type_name.unwrap_or("entity"), + entity_id, + hint + ); + } + } + + pub fn detect(&mut self) { + let mut found_leaks = false; + for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() { + eprintln!( + "leaked {} handles to {} {}", + backtraces.len(), + type_name.unwrap_or("entity"), + id + ); + for trace in backtraces.values_mut().flatten() { + trace.resolve(); + eprintln!("{:?}", crate::util::CwdBacktrace(trace)); + } + found_leaks = true; + } + + let hint = if *LEAK_BACKTRACE { + "" + } else { + " – set LEAK_BACKTRACE=1 for more information" + }; + assert!(!found_leaks, "detected leaked handles{}", hint); + } +} diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 67455cd2a7e501765f8947105e24045575b5a2a7..0805cdd865baa765093c1019a6aba5cac6a9a666 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -19,13 +19,14 @@ use smol::stream::StreamExt; use crate::{ executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action, AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, - LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, - ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, - WeakHandle, WindowInputHandler, + ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith, + RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle, }; use collections::BTreeMap; -use super::{AsyncAppContext, RefCounts}; +use super::{ + ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts, +}; pub struct TestAppContext { cx: Rc>, @@ -52,11 +53,7 @@ impl TestAppContext { platform, foreground_platform.clone(), font_cache, - RefCounts { - #[cfg(any(test, feature = "test-support"))] - leak_detector, - ..Default::default() - }, + RefCounts::new(leak_detector), (), ); cx.next_entity_id = first_entity_id; diff --git a/crates/gpui/src/app/window_input_handler.rs b/crates/gpui/src/app/window_input_handler.rs new file mode 100644 index 0000000000000000000000000000000000000000..855f0e3041b117ec0812044b131524ce27cbc075 --- /dev/null +++ b/crates/gpui/src/app/window_input_handler.rs @@ -0,0 +1,98 @@ +use std::{cell::RefCell, ops::Range, rc::Rc}; + +use pathfinder_geometry::rect::RectF; + +use crate::{AnyView, AppContext, InputHandler, MutableAppContext}; + +pub struct WindowInputHandler { + pub app: Rc>, + pub window_id: usize, +} + +impl WindowInputHandler { + fn read_focused_view(&self, f: F) -> Option + where + F: FnOnce(&dyn AnyView, &AppContext) -> T, + { + // Input-related application hooks are sometimes called by the OS during + // a call to a window-manipulation API, like prompting the user for file + // paths. In that case, the AppContext will already be borrowed, so any + // InputHandler methods need to fail gracefully. + // + // See https://github.com/zed-industries/community/issues/444 + let app = self.app.try_borrow().ok()?; + + 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.try_borrow_mut().ok()?; + 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 { + self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) + .flatten() + } + + fn selected_text_range(&self) -> Option> { + self.read_focused_view(|view, cx| view.selected_text_range(cx)) + .flatten() + } + + fn replace_text_in_range(&mut self, range: Option>, text: &str) { + 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> { + self.read_focused_view(|view, cx| view.marked_text_range(cx)) + .flatten() + } + + fn unmark_text(&mut self) { + 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>, + ) { + 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, + ); + }); + } + + 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) + } +} diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index c7de0352328d287b1248b80699c06df7fd07ae0e..edcc458658eeff81358171ecbdf9a6cf324056e4 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -6,24 +6,15 @@ mod keystroke; use std::{any::TypeId, fmt::Debug}; use collections::HashMap; -use serde::Deserialize; use smallvec::SmallVec; -use crate::{impl_actions, Action}; +use crate::Action; pub use binding::{Binding, BindingMatchResult}; pub use keymap::Keymap; pub use keymap_context::{KeymapContext, KeymapContextPredicate}; pub use keystroke::Keystroke; -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)] -pub struct KeyPressed { - #[serde(default)] - pub keystroke: Keystroke, -} - -impl_actions!(gpui, [KeyPressed]); - pub struct KeymapMatcher { pub contexts: Vec, pending_views: HashMap, @@ -102,13 +93,7 @@ impl KeymapMatcher { for binding in self.keymap.bindings().iter().rev() { match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..]) { - BindingMatchResult::Complete(mut action) => { - // Swap in keystroke for special KeyPressed action - if action.name() == "KeyPressed" && action.namespace() == "gpui" { - action = Box::new(KeyPressed { - keystroke: keystroke.clone(), - }); - } + BindingMatchResult::Complete(action) => { matched_bindings.push((view_id, action)) } BindingMatchResult::Partial => { diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index 81464378848e4ee25bdd4f3150391757035d6d98..c1cfd14e82d8d3e3235a7a2c3e7d507cd78d9648 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -7,7 +7,7 @@ use super::{KeymapContext, KeymapContextPredicate, Keystroke}; pub struct Binding { action: Box, - keystrokes: Option>, + keystrokes: SmallVec<[Keystroke; 2]>, context_predicate: Option, } @@ -23,16 +23,10 @@ impl Binding { None }; - let keystrokes = if keystrokes == "*" { - None // Catch all context - } else { - Some( - keystrokes - .split_whitespace() - .map(Keystroke::parse) - .collect::>()?, - ) - }; + let keystrokes = keystrokes + .split_whitespace() + .map(Keystroke::parse) + .collect::>()?; Ok(Self { keystrokes, @@ -53,20 +47,10 @@ impl Binding { pending_keystrokes: &Vec, contexts: &[KeymapContext], ) -> BindingMatchResult { - if self - .keystrokes - .as_ref() - .map(|keystrokes| keystrokes.starts_with(&pending_keystrokes)) - .unwrap_or(true) - && self.match_context(contexts) + if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.match_context(contexts) { // If the binding is completed, push it onto the matches list - if self - .keystrokes - .as_ref() - .map(|keystrokes| keystrokes.len() == pending_keystrokes.len()) - .unwrap_or(true) - { + if self.keystrokes.as_ref().len() == pending_keystrokes.len() { BindingMatchResult::Complete(self.action.boxed_clone()) } else { BindingMatchResult::Partial @@ -82,14 +66,14 @@ impl Binding { contexts: &[KeymapContext], ) -> Option> { if self.action.eq(action) && self.match_context(contexts) { - self.keystrokes.clone() + Some(self.keystrokes.clone()) } else { None } } - pub fn keystrokes(&self) -> Option<&[Keystroke]> { - self.keystrokes.as_deref() + pub fn keystrokes(&self) -> &[Keystroke] { + self.keystrokes.as_slice() } pub fn action(&self) -> &dyn Action { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 5d132275854c8b92e317b8226cf4d869dd92f422..3d57a7fe2ae458248179d16d3ecbdddfc784fa5b 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -184,7 +184,7 @@ impl MacForegroundPlatform { .map(|binding| binding.keystrokes()); let item; - if let Some(keystrokes) = keystrokes.flatten() { + if let Some(keystrokes) = keystrokes { if keystrokes.len() == 1 { let keystroke = &keystrokes[0]; let mut mask = NSEventModifierFlags::empty(); diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index aa73aebc90d67e7fffdaca4c967d2a7979872d6f..173c8d8505c1a6b7593262458fdaf02fa585b9b3 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, keymap_matcher::KeymapMatcher, - Action, ClipboardItem, + Action, ClipboardItem, Menu, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -77,7 +77,7 @@ impl super::ForegroundPlatform for ForegroundPlatform { fn on_menu_command(&self, _: Box) {} fn on_validate_menu_command(&self, _: Box bool>) {} fn on_will_open_menu(&self, _: Box) {} - fn set_menus(&self, _: Vec, _: &KeymapMatcher) {} + fn set_menus(&self, _: Vec, _: &KeymapMatcher) {} fn prompt_for_paths( &self, diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index eb992b638ac9906a337f23c93b97f0b945d2ef9d..d784d43ece50602b53f1d019c2b6dd57d3463537 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -1,14 +1,3 @@ -use crate::{ - elements::Empty, - executor::{self, ExecutorEvent}, - platform, - util::CwdBacktrace, - Element, ElementBox, Entity, FontCache, Handle, LeakDetector, MutableAppContext, Platform, - RenderContext, Subscription, TestAppContext, View, -}; -use futures::StreamExt; -use parking_lot::Mutex; -use smol::channel; use std::{ fmt::Write, panic::{self, RefUnwindSafe}, @@ -19,6 +8,20 @@ use std::{ }, }; +use futures::StreamExt; +use parking_lot::Mutex; +use smol::channel; + +use crate::{ + app::ref_counts::LeakDetector, + elements::Empty, + executor::{self, ExecutorEvent}, + platform, + util::CwdBacktrace, + Element, ElementBox, Entity, FontCache, Handle, MutableAppContext, Platform, RenderContext, + Subscription, TestAppContext, View, +}; + #[cfg(test)] #[ctor::ctor] fn init_logger() { diff --git a/crates/pando/Cargo.toml b/crates/pando/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8521c4fd818e4a09425095c09352b6a7134bdb6e --- /dev/null +++ b/crates/pando/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pando" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/pando.rs" + +[features] +test-support = [] + +[dependencies] +anyhow = "1.0.38" +client = { path = "../client" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +sqlez = { path = "../sqlez" } +sqlez_macros = { path = "../sqlez_macros" } \ No newline at end of file diff --git a/crates/pando/src/file_format.rs b/crates/pando/src/file_format.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/pando/src/pando.rs b/crates/pando/src/pando.rs new file mode 100644 index 0000000000000000000000000000000000000000..e75f84372024a32b62059bf37fe53f19cc7cf2bf --- /dev/null +++ b/crates/pando/src/pando.rs @@ -0,0 +1,15 @@ +//! ## Goals +//! - Opinionated Subset of Obsidian. Only the things that cant be done other ways in zed +//! - Checked in .zp file is an sqlite db containing graph metadata +//! - All nodes are file urls +//! - Markdown links auto add soft linked nodes to the db +//! - Links create positioning data regardless of if theres a file +//! - Lock links to make structure that doesn't rotate or spread +//! - Drag from file finder to pando item to add it in +//! - For linked files, zoom out to see closest linking pando file + +//! ## Plan +//! - [ ] Make item backed by .zp sqlite file with camera position by user account +//! - [ ] Render grid of dots and allow scrolling around the grid +//! - [ ] Add scale property to layer canvas and manipulate it with pinch zooming +//! - [ ] Allow dropping files onto .zp pane. Their relative path is recorded into the file along with diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index f247f3e5377bab8c124b27f9f2a66ec0ffe48085..716ec766443bc997c57fb77f78d94f0290aad579 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -15,7 +15,4 @@ thread_local = "1.1.4" lazy_static = "1.4" parking_lot = "0.11.1" futures = "0.3" -uuid = { version = "1.1.2", features = ["v4"] } - -[dev-dependencies] -sqlez_macros = { path = "../sqlez_macros"} \ No newline at end of file +uuid = { version = "1.1.2", features = ["v4"] } \ No newline at end of file diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index b3aaef95b1d52b3e5e005f72be9fd6914c1528ca..b8e589e268c8f02693b11b7915b32eb8e0975d2f 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -83,7 +83,6 @@ impl Connection { #[cfg(test)] mod test { use indoc::indoc; - use sqlez_macros::sql; use crate::connection::Connection; @@ -288,21 +287,18 @@ mod test { let connection = Connection::open_memory(Some("test_create_alter_drop")); connection - .migrate( - "first_migration", - &[sql!( CREATE TABLE table1(a TEXT) STRICT; )], - ) + .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) .unwrap(); connection - .exec(sql!( INSERT INTO table1(a) VALUES ("test text"); )) + .exec("INSERT INTO table1(a) VALUES (\"test text\");") .unwrap()() .unwrap(); connection .migrate( "second_migration", - &[sql!( + &[indoc! {" CREATE TABLE table2(b TEXT) STRICT; INSERT INTO table2 (b) @@ -311,16 +307,11 @@ mod test { DROP TABLE table1; ALTER TABLE table2 RENAME TO table1; - )], + "}], ) .unwrap(); - let res = &connection - .select::(sql!( - SELECT b FROM table1 - )) - .unwrap()() - .unwrap()[0]; + let res = &connection.select::("SELECT b FROM table1").unwrap()().unwrap()[0]; assert_eq!(res, "test text"); } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index c526e3b1dc7eba5a2edbed82bcaf7408bf0574d8..c58f66478fa682694d1da269afb7d299a89d5e2e 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -1,62 +1,66 @@ -use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased}; +use editor::{EditorBlurred, EditorFocused, EditorMode, EditorReleased, Event}; use gpui::MutableAppContext; use crate::{state::Mode, Vim}; pub fn init(cx: &mut MutableAppContext) { - cx.subscribe_global(editor_created).detach(); - cx.subscribe_global(editor_focused).detach(); - cx.subscribe_global(editor_blurred).detach(); - cx.subscribe_global(editor_released).detach(); + cx.subscribe_global(focused).detach(); + cx.subscribe_global(blurred).detach(); + cx.subscribe_global(released).detach(); } -fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { - cx.update_default_global(|vim: &mut Vim, cx| { - vim.editors.insert(editor.id(), editor.downgrade()); - vim.sync_vim_settings(cx); - }) -} - -fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { +fn focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { + if let Some(previously_active_editor) = vim + .active_editor + .as_ref() + .and_then(|editor| editor.upgrade(cx)) + { + vim.unhook_vim_settings(previously_active_editor, cx); + } + 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); + vim.editor_subscription = Some(cx.subscribe(editor, |editor, event, cx| match event { + Event::SelectionsChanged { local: true } => { + let editor = editor.read(cx); + if editor.leader_replica_id().is_none() { + let newest_empty = editor.selections.newest::(cx).is_empty(); + local_selections_changed(newest_empty, cx); } } + Event::InputIgnored { text } => { + Vim::active_editor_input_ignored(text.clone(), cx); + } + _ => {} })); - if !vim.enabled { - return; - } - - let editor = editor.read(cx); - let editor_mode = editor.mode(); - let newest_selection_empty = editor.selections.newest::(cx).is_empty(); + if vim.enabled { + let editor = editor.read(cx); + let editor_mode = editor.mode(); + let newest_selection_empty = editor.selections.newest::(cx).is_empty(); - if editor_mode == EditorMode::Full && !newest_selection_empty { - vim.switch_mode(Mode::Visual { line: false }, true, cx); + if editor_mode == EditorMode::Full && !newest_selection_empty { + vim.switch_mode(Mode::Visual { line: false }, true, cx); + } } + + vim.sync_vim_settings(cx); }); } -fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { +fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { vim.active_editor = None; } } - vim.sync_vim_settings(cx); + vim.unhook_vim_settings(editor.clone(), cx); }) } -fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { +fn released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { cx.update_default_global(|vim: &mut Vim, _| { - vim.editors.remove(&editor.id()); if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { vim.active_editor = None; @@ -65,7 +69,7 @@ fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppC }); } -fn editor_local_selections_changed(newest_empty: bool, cx: &mut MutableAppContext) { +fn local_selections_changed(newest_empty: bool, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { vim.switch_mode(Mode::Visual { line: false }, false, cx) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 62b30730e8ea8d4a3a4d9f28120359872c5e8211..8bc7c756e066cb632e9e8ca7a3254f7c28f595ef 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use editor::{ char_kind, display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -15,7 +17,7 @@ use crate::{ Vim, }; -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Motion { Left, Backspace, @@ -32,8 +34,8 @@ pub enum Motion { StartOfDocument, EndOfDocument, Matching, - FindForward { before: bool, character: char }, - FindBackward { after: bool, character: char }, + FindForward { before: bool, text: Arc }, + FindBackward { after: bool, text: Arc }, } #[derive(Clone, Deserialize, PartialEq)] @@ -134,7 +136,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) { // Motion handling is specified here: // https://github.com/vim/vim/blob/master/runtime/doc/motion.txt impl Motion { - pub fn linewise(self) -> bool { + pub fn linewise(&self) -> bool { use Motion::*; matches!( self, @@ -142,12 +144,12 @@ impl Motion { ) } - pub fn infallible(self) -> bool { + pub fn infallible(&self) -> bool { use Motion::*; matches!(self, StartOfDocument | CurrentLine | EndOfDocument) } - pub fn inclusive(self) -> bool { + pub fn inclusive(&self) -> bool { use Motion::*; match self { Down @@ -171,13 +173,14 @@ impl Motion { } pub fn move_point( - self, + &self, map: &DisplaySnapshot, point: DisplayPoint, goal: SelectionGoal, times: usize, ) -> Option<(DisplayPoint, SelectionGoal)> { use Motion::*; + let infallible = self.infallible(); let (new_point, goal) = match self { Left => (left(map, point, times), SelectionGoal::None), Backspace => (backspace(map, point, times), SelectionGoal::None), @@ -185,15 +188,15 @@ impl Motion { Up => up(map, point, goal, times), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( - next_word_start(map, point, ignore_punctuation, times), + next_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), NextWordEnd { ignore_punctuation } => ( - next_word_end(map, point, ignore_punctuation, times), + next_word_end(map, point, *ignore_punctuation, times), SelectionGoal::None, ), PreviousWordStart { ignore_punctuation } => ( - previous_word_start(map, point, ignore_punctuation, times), + previous_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), @@ -203,22 +206,22 @@ impl Motion { StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None), Matching => (matching(map, point), SelectionGoal::None), - FindForward { before, character } => ( - find_forward(map, point, before, character, times), + FindForward { before, text } => ( + find_forward(map, point, *before, text.clone(), times), SelectionGoal::None, ), - FindBackward { after, character } => ( - find_backward(map, point, after, character, times), + FindBackward { after, text } => ( + find_backward(map, point, *after, text.clone(), times), SelectionGoal::None, ), }; - (new_point != point || self.infallible()).then_some((new_point, goal)) + (new_point != point || infallible).then_some((new_point, goal)) } // Expands a selection using self motion for an operator pub fn expand_selection( - self, + &self, map: &DisplaySnapshot, selection: &mut Selection, times: usize, @@ -254,7 +257,7 @@ impl Motion { // but "d}" will not include that line. let mut inclusive = self.inclusive(); if !inclusive - && self != Motion::Backspace + && self != &Motion::Backspace && selection.end.row() > selection.start.row() && selection.end.column() == 0 { @@ -466,45 +469,42 @@ fn find_forward( map: &DisplaySnapshot, from: DisplayPoint, before: bool, - target: char, - mut times: usize, + target: Arc, + times: usize, ) -> DisplayPoint { - let mut previous_point = from; - - for (ch, point) in map.chars_at(from) { - if ch == target && point != from { - times -= 1; - if times == 0 { - return if before { previous_point } else { point }; + map.find_while(from, target.as_ref(), |ch, _| ch != '\n') + .skip_while(|found_at| found_at == &from) + .nth(times - 1) + .map(|mut found| { + if before { + *found.column_mut() -= 1; + found = map.clip_point(found, Bias::Right); + found + } else { + found } - } else if ch == '\n' { - break; - } - previous_point = point; - } - - from + }) + .unwrap_or(from) } fn find_backward( map: &DisplaySnapshot, from: DisplayPoint, after: bool, - target: char, - mut times: usize, + target: Arc, + times: usize, ) -> DisplayPoint { - let mut previous_point = from; - for (ch, point) in map.reverse_chars_at(from) { - if ch == target && point != from { - times -= 1; - if times == 0 { - return if after { previous_point } else { point }; + map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n') + .skip_while(|found_at| found_at == &from) + .nth(times - 1) + .map(|mut found| { + if after { + *found.column_mut() += 1; + found = map.clip_point(found, Bias::Left); + found + } else { + found } - } else if ch == '\n' { - break; - } - previous_point = point; - } - - from + }) + .unwrap_or(from) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index d6391353cf7fc43980dd2866f69adf4d379721c8..742f2426c8aa6ba5d447eb0bcb7a8a2d92487762 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,7 +2,7 @@ mod change; mod delete; mod yank; -use std::{borrow::Cow, cmp::Ordering}; +use std::{borrow::Cow, cmp::Ordering, sync::Arc}; use crate::{ motion::Motion, @@ -424,7 +424,7 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -453,7 +453,7 @@ pub(crate) fn normal_replace(text: &str, cx: &mut MutableAppContext) { ( range.start.to_offset(&map, Bias::Left) ..range.end.to_offset(&map, Bias::Left), - text, + text.clone(), ) }) .collect::>(); diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 723dac0581e33318ab0d47585fe93d39494374a6..539ab0a8ff7f16f45700670065ae919d6160a427 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -53,7 +53,7 @@ impl<'a> VimTestContext<'a> { // Setup search toolbars and keypress hook workspace.update(cx, |workspace, cx| { - observe_keypresses(window_id, cx); + observe_keystrokes(window_id, cx); workspace.active_pane().update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let buffer_search_bar = cx.add_view(BufferSearchBar::new); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9f799ef37f9f3dbcecb168420d6892e3b8534587..33f142c21e692294a498d951c39b3ee05a0b1cf8 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -10,13 +10,12 @@ mod state; mod utils; mod visual; -use collections::HashMap; +use std::sync::Arc; + use command_palette::CommandPaletteFilter; use editor::{Bias, Cancel, Editor, EditorMode}; use gpui::{ - impl_actions, - keymap_matcher::{KeyPressed, Keystroke}, - MutableAppContext, Subscription, ViewContext, WeakViewHandle, + actions, impl_actions, MutableAppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, }; use language::CursorShape; use motion::Motion; @@ -36,6 +35,7 @@ pub struct PushOperator(pub Operator); #[derive(Clone, Deserialize, PartialEq)] struct Number(u8); +actions!(vim, [Tab, Enter]); impl_actions!(vim, [Number, SwitchMode, PushOperator]); pub fn init(cx: &mut MutableAppContext) { @@ -58,11 +58,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|_: &mut Workspace, n: &Number, cx: _| { Vim::update(cx, |vim, cx| vim.push_number(n, cx)); }); - cx.add_action( - |_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| { - Vim::key_pressed(keystroke, cx); - }, - ); // Editor Actions cx.add_action(|_: &mut Editor, _: &Cancel, cx| { @@ -80,8 +75,16 @@ pub fn init(cx: &mut MutableAppContext) { } }); + cx.add_action(|_: &mut Workspace, _: &Tab, cx| { + Vim::active_editor_input_ignored(" ".into(), cx) + }); + + cx.add_action(|_: &mut Workspace, _: &Enter, cx| { + Vim::active_editor_input_ignored("\n".into(), cx) + }); + // Sync initial settings with the rest of the app - Vim::update(cx, |state, cx| state.sync_vim_settings(cx)); + Vim::update(cx, |vim, cx| vim.sync_vim_settings(cx)); // Any time settings change, update vim mode to match cx.observe_global::(|cx| { @@ -92,7 +95,7 @@ pub fn init(cx: &mut MutableAppContext) { .detach(); } -pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) { +pub fn observe_keystrokes(window_id: usize, cx: &mut MutableAppContext) { cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| { if let Some(handled_by) = handled_by { // Keystroke is handled by the vim system, so continue forward @@ -104,11 +107,14 @@ pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) { } } - Vim::update(cx, |vim, cx| { - if vim.active_operator().is_some() { - // If the keystroke is not handled by vim, we should clear the operator + Vim::update(cx, |vim, cx| match vim.active_operator() { + Some( + Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace, + ) => {} + Some(_) => { vim.clear_operator(cx); } + _ => {} }); true }) @@ -117,9 +123,8 @@ pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) { #[derive(Default)] pub struct Vim { - editors: HashMap>, active_editor: Option>, - selection_subscription: Option, + editor_subscription: Option, enabled: bool, state: VimState, @@ -160,24 +165,26 @@ impl Vim { } // Adjust selections - for editor in self.editors.values() { - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if self.state.empty_selections_only() { - let new_head = map.clip_point(selection.head(), Bias::Left); - selection.collapse_to(new_head, selection.goal) - } else { - selection.set_head( - map.clip_point(selection.head(), Bias::Left), - selection.goal, - ); - } - }); - }) + if let Some(editor) = self + .active_editor + .as_ref() + .and_then(|editor| editor.upgrade(cx)) + { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if self.state.empty_selections_only() { + let new_head = map.clip_point(selection.head(), Bias::Left); + selection.collapse_to(new_head, selection.goal) + } else { + selection.set_head( + map.clip_point(selection.head(), Bias::Left), + selection.goal, + ); + } + }); }) - } + }) } } @@ -220,24 +227,24 @@ impl Vim { self.state.operator_stack.last().copied() } - fn key_pressed(keystroke: &Keystroke, cx: &mut ViewContext) { + fn active_editor_input_ignored(text: Arc, cx: &mut MutableAppContext) { + if text.is_empty() { + return; + } + match Vim::read(cx).active_operator() { Some(Operator::FindForward { before }) => { - if let Some(character) = keystroke.key.chars().next() { - motion::motion(Motion::FindForward { before, character }, cx) - } + motion::motion(Motion::FindForward { before, text }, cx) } Some(Operator::FindBackward { after }) => { - if let Some(character) = keystroke.key.chars().next() { - motion::motion(Motion::FindBackward { after, character }, cx) - } + motion::motion(Motion::FindBackward { after, text }, cx) } Some(Operator::Replace) => match Vim::read(cx).state.mode { - Mode::Normal => normal_replace(&keystroke.key, cx), - Mode::Visual { line } => visual_replace(&keystroke.key, line, cx), + Mode::Normal => normal_replace(text, cx), + Mode::Visual { line } => visual_replace(text, line, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, - _ => cx.propagate_action(), + _ => {} } } @@ -264,26 +271,33 @@ impl Vim { } }); - for editor in self.editors.values() { - if let Some(editor) = editor.upgrade(cx) { + if let Some(editor) = self + .active_editor + .as_ref() + .and_then(|editor| editor.upgrade(cx)) + { + if self.enabled && editor.read(cx).mode() == EditorMode::Full { editor.update(cx, |editor, cx| { - if self.enabled && editor.mode() == EditorMode::Full { - editor.set_cursor_shape(cursor_shape, cx); - editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); - editor.set_input_enabled(!state.vim_controlled()); - editor.selections.line_mode = - matches!(state.mode, Mode::Visual { line: true }); - let context_layer = state.keymap_context_layer(); - editor.set_keymap_context_layer::(context_layer); - } else { - editor.set_cursor_shape(CursorShape::Bar, cx); - editor.set_clip_at_line_ends(false, cx); - editor.set_input_enabled(true); - editor.selections.line_mode = false; - editor.remove_keymap_context_layer::(); - } + editor.set_cursor_shape(cursor_shape, cx); + editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); + editor.set_input_enabled(!state.vim_controlled()); + editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); + let context_layer = state.keymap_context_layer(); + editor.set_keymap_context_layer::(context_layer); }); + } else { + self.unhook_vim_settings(editor, cx); } } } + + fn unhook_vim_settings(&self, editor: ViewHandle, cx: &mut MutableAppContext) { + editor.update(cx, |editor, cx| { + editor.set_cursor_shape(CursorShape::Bar, cx); + editor.set_clip_at_line_ends(false, cx); + editor.set_input_enabled(true); + editor.selections.line_mode = false; + editor.remove_keymap_context_layer::(); + }); + } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ac8771f969ef40d7f37f6b66f429cde7b3085247..b890e4e41b503ad595cd3281a7cac533b67fc64f 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; use collections::HashMap; use editor::{ @@ -313,7 +313,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext }); } -pub(crate) fn visual_replace(text: &str, line: bool, cx: &mut MutableAppContext) { +pub(crate) fn visual_replace(text: Arc, line: bool, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9195c36940071a7eb3351d60cd474169cbd07f88..bf9afe136e9cf1e65b160b738eded9e1a574c3f8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -363,7 +363,7 @@ pub fn initialize_workspace( auto_update::notify_of_any_new_update(cx.weak_handle(), cx); let window_id = cx.window_id(); - vim::observe_keypresses(window_id, cx); + vim::observe_keystrokes(window_id, cx); cx.on_window_should_close(|workspace, cx| { if let Some(task) = workspace.close(&Default::default(), cx) {