diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 94729af21f..5cdd4fc7d4 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -38,22 +38,6 @@ ], "%": "vim::Matching", "escape": "editor::Cancel", - "i": [ - "vim::PushOperator", - { - "Object": { - "around": false - } - } - ], - "a": [ - "vim::PushOperator", - { - "Object": { - "around": true - } - } - ], "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ "vim::Number", @@ -93,6 +77,28 @@ ] } }, + { + //Operators + "context": "Editor && VimControl && vim_operator == none", + "bindings": { + "i": [ + "vim::PushOperator", + { + "Object": { + "around": false + } + } + ], + "a": [ + "vim::PushOperator", + { + "Object": { + "around": true + } + } + ] + } + }, { "context": "Editor && vim_mode == normal && vim_operator == none", "bindings": { @@ -110,6 +116,12 @@ "vim::PushOperator", "Yank" ], + "z": [ + "vim::PushOperator", + { + "Namespace": "Z" + } + ], "i": [ "vim::SwitchMode", "Insert" @@ -147,6 +159,30 @@ { "focus": true } + ], + "ctrl-f": [ + "vim::Scroll", + "PageDown" + ], + "ctrl-b": [ + "vim::Scroll", + "PageUp" + ], + "ctrl-d": [ + "vim::Scroll", + "HalfPageDown" + ], + "ctrl-u": [ + "vim::Scroll", + "HalfPageUp" + ], + "ctrl-e": [ + "vim::Scroll", + "LineDown" + ], + "ctrl-y": [ + "vim::Scroll", + "LineUp" ] } }, @@ -188,6 +224,18 @@ "y": "vim::CurrentLine" } }, + { + "context": "Editor && vim_operator == z", + "bindings": { + "t": "editor::ScrollCursorTop", + "z": "editor::ScrollCursorCenter", + "b": "editor::ScrollCursorBottom", + "escape": [ + "vim::SwitchMode", + "Normal" + ] + } + }, { "context": "Editor && VimObject", "bindings": { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f1c612a58d..9122706ad3 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -5,8 +5,9 @@ use collections::{BTreeMap, HashSet}; use editor::{ diagnostic_block_renderer, display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, - highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer, - ToOffset, + highlight_diagnostic_message, + scroll::autoscroll::Autoscroll, + Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, }; use gpui::{ actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 63db71edae..1aee1e246d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10,6 +10,7 @@ mod mouse_context_menu; pub mod movement; mod multi_buffer; mod persistence; +pub mod scroll; pub mod selections_collection; #[cfg(test)] @@ -33,13 +34,13 @@ use gpui::{ elements::*, executor, fonts::{self, HighlightStyle, TextStyle}, - geometry::vector::{vec2f, Vector2F}, + geometry::vector::Vector2F, impl_actions, impl_internal_actions, platform::CursorStyle, serde_json::json, - text_layout, AnyViewHandle, AppContext, AsyncAppContext, Axis, ClipboardItem, Element, - ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, - Task, View, ViewContext, ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, + ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -61,11 +62,13 @@ pub use multi_buffer::{ use multi_buffer::{MultiBufferChunks, ToOffsetUtf16}; use ordered_float::OrderedFloat; use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction}; +use scroll::{ + autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, +}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; use settings::Settings; use smallvec::SmallVec; -use smol::Timer; use snippet::Snippet; use std::{ any::TypeId, @@ -86,11 +89,9 @@ use workspace::{ItemNavHistory, Workspace, WorkspaceId}; use crate::git::diff_hunk_to_display; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; -pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); @@ -100,12 +101,6 @@ pub struct SelectNext { pub replace_newest: bool, } -#[derive(Clone, PartialEq)] -pub struct Scroll { - pub scroll_position: Vector2F, - pub axis: Option, -} - #[derive(Clone, PartialEq)] pub struct Select(pub SelectPhase); @@ -258,7 +253,7 @@ impl_actions!( ] ); -impl_internal_actions!(editor, [Scroll, Select, Jump]); +impl_internal_actions!(editor, [Select, Jump]); enum DocumentHighlightRead {} enum DocumentHighlightWrite {} @@ -270,12 +265,8 @@ pub enum Direction { Next, } -#[derive(Default)] -struct ScrollbarAutoHide(bool); - pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::new_file); - cx.add_action(Editor::scroll); cx.add_action(Editor::select); cx.add_action(Editor::cancel); cx.add_action(Editor::newline); @@ -305,12 +296,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::redo); cx.add_action(Editor::move_up); cx.add_action(Editor::move_page_up); - cx.add_action(Editor::page_up); cx.add_action(Editor::move_down); cx.add_action(Editor::move_page_down); - cx.add_action(Editor::page_down); cx.add_action(Editor::next_screen); - cx.add_action(Editor::move_left); cx.add_action(Editor::move_right); cx.add_action(Editor::move_to_previous_word_start); @@ -370,6 +358,7 @@ pub fn init(cx: &mut MutableAppContext) { hover_popover::init(cx); link_go_to_definition::init(cx); mouse_context_menu::init(cx); + scroll::actions::init(cx); workspace::register_project_item::(cx); workspace::register_followable_item::(cx); @@ -411,46 +400,6 @@ pub enum SelectMode { All, } -#[derive(PartialEq, Eq)] -pub enum Autoscroll { - Next, - Strategy(AutoscrollStrategy), -} - -impl Autoscroll { - pub fn fit() -> Self { - Self::Strategy(AutoscrollStrategy::Fit) - } - - pub fn newest() -> Self { - Self::Strategy(AutoscrollStrategy::Newest) - } - - pub fn center() -> Self { - Self::Strategy(AutoscrollStrategy::Center) - } -} - -#[derive(PartialEq, Eq, Default)] -pub enum AutoscrollStrategy { - Fit, - Newest, - #[default] - Center, - Top, - Bottom, -} - -impl AutoscrollStrategy { - fn next(&self) -> Self { - match self { - AutoscrollStrategy::Center => AutoscrollStrategy::Top, - AutoscrollStrategy::Top => AutoscrollStrategy::Bottom, - _ => AutoscrollStrategy::Center, - } - } -} - #[derive(Copy, Clone, PartialEq, Eq)] pub enum EditorMode { SingleLine, @@ -477,74 +426,12 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; -#[derive(Clone, Copy)] -pub struct OngoingScroll { - last_timestamp: Instant, - axis: Option, -} - -impl OngoingScroll { - fn initial() -> OngoingScroll { - OngoingScroll { - last_timestamp: Instant::now() - SCROLL_EVENT_SEPARATION, - axis: None, - } - } - - fn update(&mut self, axis: Option) { - self.last_timestamp = Instant::now(); - self.axis = axis; - } - - pub fn filter(&self, delta: &mut Vector2F) -> Option { - const UNLOCK_PERCENT: f32 = 1.9; - const UNLOCK_LOWER_BOUND: f32 = 6.; - let mut axis = self.axis; - - let x = delta.x().abs(); - let y = delta.y().abs(); - let duration = Instant::now().duration_since(self.last_timestamp); - if duration > SCROLL_EVENT_SEPARATION { - //New ongoing scroll will start, determine axis - axis = if x <= y { - Some(Axis::Vertical) - } else { - Some(Axis::Horizontal) - }; - } else if x.max(y) >= UNLOCK_LOWER_BOUND { - //Check if the current ongoing will need to unlock - match axis { - Some(Axis::Vertical) => { - if x > y && x >= y * UNLOCK_PERCENT { - axis = None; - } - } - - Some(Axis::Horizontal) => { - if y > x && y >= x * UNLOCK_PERCENT { - axis = None; - } - } - - None => {} - } - } - - match axis { - Some(Axis::Vertical) => *delta = vec2f(0., delta.y()), - Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.), - None => {} - } - - axis - } -} - pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, display_map: ModelHandle, pub selections: SelectionsCollection, + pub scroll_manager: ScrollManager, columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, @@ -554,10 +441,6 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, active_diagnostics: Option, - ongoing_scroll: OngoingScroll, - scroll_position: Vector2F, - scroll_top_anchor: Anchor, - autoscroll_request: Option<(Autoscroll, bool)>, soft_wrap_mode_override: Option, get_field_editor_theme: Option>, override_text_style: Option>, @@ -565,10 +448,7 @@ pub struct Editor { focused: bool, blink_manager: ModelHandle, show_local_selections: bool, - show_scrollbars: bool, - hide_scrollbar_task: Option>, mode: EditorMode, - vertical_scroll_margin: f32, placeholder_text: Option>, highlighted_rows: Option>, #[allow(clippy::type_complexity)] @@ -590,8 +470,6 @@ pub struct Editor { leader_replica_id: Option, hover_state: HoverState, link_go_to_definition_state: LinkGoToDefinitionState, - visible_line_count: Option, - last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>, _subscriptions: Vec, } @@ -600,9 +478,8 @@ pub struct EditorSnapshot { pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, is_focused: bool, + scroll_anchor: ScrollAnchor, ongoing_scroll: OngoingScroll, - scroll_position: Vector2F, - scroll_top_anchor: Anchor, } #[derive(Clone, Debug)] @@ -1090,12 +967,9 @@ pub struct ClipboardSelection { #[derive(Debug)] pub struct NavigationData { - // Matching offsets for anchor and scroll_top_anchor allows us to recreate the anchor if the buffer - // has since been closed cursor_anchor: Anchor, cursor_position: Point, - scroll_position: Vector2F, - scroll_top_anchor: Anchor, + scroll_anchor: ScrollAnchor, scroll_top_row: u32, } @@ -1163,9 +1037,8 @@ impl Editor { display_map.set_state(&snapshot, cx); }); }); - clone.selections.set_state(&self.selections); - clone.scroll_position = self.scroll_position; - clone.scroll_top_anchor = self.scroll_top_anchor; + clone.selections.clone_state(&self.selections); + clone.scroll_manager.clone_state(&self.scroll_manager); clone.searchable = self.searchable; clone } @@ -1200,6 +1073,7 @@ impl Editor { buffer: buffer.clone(), display_map: display_map.clone(), selections, + scroll_manager: ScrollManager::new(), columnar_selection_tail: None, add_selections_state: None, select_next_state: None, @@ -1212,17 +1086,10 @@ impl Editor { soft_wrap_mode_override: None, get_field_editor_theme, project, - ongoing_scroll: OngoingScroll::initial(), - scroll_position: Vector2F::zero(), - scroll_top_anchor: Anchor::min(), - autoscroll_request: None, focused: false, blink_manager: blink_manager.clone(), show_local_selections: true, - show_scrollbars: true, - hide_scrollbar_task: None, mode, - vertical_scroll_margin: 3.0, placeholder_text: None, highlighted_rows: None, background_highlights: Default::default(), @@ -1244,8 +1111,6 @@ impl Editor { leader_replica_id: None, hover_state: Default::default(), link_go_to_definition_state: Default::default(), - visible_line_count: None, - last_autoscroll: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), @@ -1254,7 +1119,7 @@ impl Editor { ], }; this.end_selection(cx); - this.make_scrollbar_visible(cx); + this.scroll_manager.show_scrollbar(cx); let editor_created_event = EditorCreated(cx.handle()); cx.emit_global(editor_created_event); @@ -1307,9 +1172,8 @@ impl Editor { EditorSnapshot { mode: self.mode, display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), - ongoing_scroll: self.ongoing_scroll, - scroll_position: self.scroll_position, - scroll_top_anchor: self.scroll_top_anchor, + scroll_anchor: self.scroll_manager.anchor(), + ongoing_scroll: self.scroll_manager.ongoing_scroll(), placeholder_text: self.placeholder_text.clone(), is_focused: self .handle @@ -1348,64 +1212,6 @@ impl Editor { cx.notify(); } - pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext) { - self.vertical_scroll_margin = margin_rows as f32; - cx.notify(); - } - - pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { - self.set_scroll_position_internal(scroll_position, true, cx); - } - - fn set_scroll_position_internal( - &mut self, - scroll_position: Vector2F, - local: bool, - cx: &mut ViewContext, - ) { - let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if scroll_position.y() <= 0. { - self.scroll_top_anchor = Anchor::min(); - self.scroll_position = scroll_position.max(vec2f(0., 0.)); - } else { - let scroll_top_buffer_offset = - DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); - let anchor = map - .buffer_snapshot - .anchor_at(scroll_top_buffer_offset, Bias::Right); - self.scroll_position = vec2f( - scroll_position.x(), - scroll_position.y() - anchor.to_display_point(&map).row() as f32, - ); - self.scroll_top_anchor = anchor; - } - - self.make_scrollbar_visible(cx); - self.autoscroll_request.take(); - hide_hover(self, cx); - - cx.emit(Event::ScrollPositionChanged { local }); - cx.notify(); - } - - fn set_visible_line_count(&mut self, lines: f32) { - self.visible_line_count = Some(lines) - } - - fn set_scroll_top_anchor( - &mut self, - anchor: Anchor, - position: Vector2F, - cx: &mut ViewContext, - ) { - self.scroll_top_anchor = anchor; - self.scroll_position = position; - self.make_scrollbar_visible(cx); - cx.emit(Event::ScrollPositionChanged { local: false }); - cx.notify(); - } - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { self.cursor_shape = cursor_shape; cx.notify(); @@ -1431,199 +1237,6 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) - } - - pub fn clamp_scroll_left(&mut self, max: f32) -> bool { - if max < self.scroll_position.x() { - self.scroll_position.set_x(max); - true - } else { - false - } - } - - pub fn autoscroll_vertically( - &mut self, - viewport_height: f32, - line_height: f32, - cx: &mut ViewContext, - ) -> bool { - let visible_lines = viewport_height / line_height; - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut scroll_position = - compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor); - let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { - (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) - } else { - display_map.max_point().row() as f32 - }; - if scroll_position.y() > max_scroll_top { - scroll_position.set_y(max_scroll_top); - self.set_scroll_position(scroll_position, cx); - } - - let (autoscroll, local) = if let Some(autoscroll) = self.autoscroll_request.take() { - autoscroll - } else { - return false; - }; - - let first_cursor_top; - let last_cursor_bottom; - if let Some(highlighted_rows) = &self.highlighted_rows { - first_cursor_top = highlighted_rows.start as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else if autoscroll == Autoscroll::newest() { - let newest_selection = self.selections.newest::(cx); - first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else { - let selections = self.selections.all::(cx); - first_cursor_top = selections - .first() - .unwrap() - .head() - .to_display_point(&display_map) - .row() as f32; - last_cursor_bottom = selections - .last() - .unwrap() - .head() - .to_display_point(&display_map) - .row() as f32 - + 1.0; - } - - let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { - 0. - } else { - ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() - }; - if margin < 0.0 { - return false; - } - - let strategy = match autoscroll { - Autoscroll::Strategy(strategy) => strategy, - Autoscroll::Next => { - let last_autoscroll = &self.last_autoscroll; - if let Some(last_autoscroll) = last_autoscroll { - if self.scroll_position == last_autoscroll.0 - && first_cursor_top == last_autoscroll.1 - && last_cursor_bottom == last_autoscroll.2 - { - last_autoscroll.3.next() - } else { - AutoscrollStrategy::default() - } - } else { - AutoscrollStrategy::default() - } - } - }; - - match strategy { - AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { - let margin = margin.min(self.vertical_scroll_margin); - let target_top = (first_cursor_top - margin).max(0.0); - let target_bottom = last_cursor_bottom + margin; - let start_row = scroll_position.y(); - let end_row = start_row + visible_lines; - - if target_top < start_row { - scroll_position.set_y(target_top); - self.set_scroll_position_internal(scroll_position, local, cx); - } else if target_bottom >= end_row { - scroll_position.set_y(target_bottom - visible_lines); - self.set_scroll_position_internal(scroll_position, local, cx); - } - } - AutoscrollStrategy::Center => { - scroll_position.set_y((first_cursor_top - margin).max(0.0)); - self.set_scroll_position_internal(scroll_position, local, cx); - } - AutoscrollStrategy::Top => { - scroll_position.set_y((first_cursor_top).max(0.0)); - self.set_scroll_position_internal(scroll_position, local, cx); - } - AutoscrollStrategy::Bottom => { - scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0)); - self.set_scroll_position_internal(scroll_position, local, cx); - } - } - - self.last_autoscroll = Some(( - self.scroll_position, - first_cursor_top, - last_cursor_bottom, - strategy, - )); - - true - } - - pub fn autoscroll_horizontally( - &mut self, - start_row: u32, - viewport_width: f32, - scroll_width: f32, - max_glyph_width: f32, - layouts: &[text_layout::Line], - cx: &mut ViewContext, - ) -> bool { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); - - let mut target_left; - let mut target_right; - - if self.highlighted_rows.is_some() { - target_left = 0.0_f32; - target_right = 0.0_f32; - } else { - target_left = std::f32::INFINITY; - target_right = 0.0_f32; - for selection in selections { - let head = selection.head().to_display_point(&display_map); - if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { - let start_column = head.column().saturating_sub(3); - let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); - target_left = target_left.min( - layouts[(head.row() - start_row) as usize] - .x_for_index(start_column as usize), - ); - target_right = target_right.max( - layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) - + max_glyph_width, - ); - } - } - } - - target_right = target_right.min(scroll_width); - - if target_right - target_left > viewport_width { - return false; - } - - let scroll_left = self.scroll_position.x() * max_glyph_width; - let scroll_right = scroll_left + viewport_width; - - if target_left < scroll_left { - self.scroll_position.set_x(target_left / max_glyph_width); - true - } else if target_right > scroll_right { - self.scroll_position - .set_x((target_right - viewport_width) / max_glyph_width); - true - } else { - false - } - } - fn selections_did_change( &mut self, local: bool, @@ -1746,11 +1359,6 @@ impl Editor { }); } - fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext) { - self.ongoing_scroll.update(action.axis); - self.set_scroll_position(action.scroll_position, cx); - } - fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { self.hide_context_menu(cx); @@ -4073,23 +3681,6 @@ impl Editor { }) } - pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { - return; - } - - if let Some(_) = self.context_menu.as_mut() { - return; - } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - self.request_autoscroll(Autoscroll::Next, cx); - } - pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { if self.take_rename(true, cx).is_some() { return; @@ -4118,26 +3709,18 @@ impl Editor { }) } - pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { - return; - } - - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_first(cx) { - return; - } + pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext) -> Option<()> { + self.take_rename(true, cx)?; + if self.context_menu.as_mut()?.select_first(cx) { + return None; } if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); - return; + return None; } - let row_count = match self.visible_line_count { - Some(row_count) => row_count as u32 - 1, - None => return, - }; + let row_count = self.visible_line_count()? as u32 - 1; let autoscroll = if action.center_cursor { Autoscroll::center() @@ -4156,32 +3739,8 @@ impl Editor { selection.collapse_to(cursor, goal); }); }); - } - pub fn page_up(&mut self, _: &PageUp, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { - return; - } - - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_first(cx) { - return; - } - } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let lines = match self.visible_line_count { - Some(lines) => lines, - None => return, - }; - - let cur_position = self.scroll_position(cx); - let new_pos = cur_position - vec2f(0., lines + 1.); - self.set_scroll_position(new_pos, cx); + Some(()) } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { @@ -4216,26 +3775,25 @@ impl Editor { }); } - pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext) { + pub fn move_page_down( + &mut self, + action: &MovePageDown, + cx: &mut ViewContext, + ) -> Option<()> { if self.take_rename(true, cx).is_some() { - return; + return None; } - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_last(cx) { - return; - } + if self.context_menu.as_mut()?.select_last(cx) { + return None; } if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); - return; + return None; } - let row_count = match self.visible_line_count { - Some(row_count) => row_count as u32 - 1, - None => return, - }; + let row_count = self.visible_line_count()? as u32 - 1; let autoscroll = if action.center_cursor { Autoscroll::center() @@ -4254,32 +3812,8 @@ impl Editor { selection.collapse_to(cursor, goal); }); }); - } - pub fn page_down(&mut self, _: &PageDown, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { - return; - } - - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_last(cx) { - return; - } - } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let lines = match self.visible_line_count { - Some(lines) => lines, - None => return, - }; - - let cur_position = self.scroll_position(cx); - let new_pos = cur_position + vec2f(0., lines - 1.); - self.set_scroll_position(new_pos, cx); + Some(()) } pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { @@ -4602,18 +4136,19 @@ impl Editor { fn push_to_nav_history( &self, - position: Anchor, + cursor_anchor: Anchor, new_position: Option, cx: &mut ViewContext, ) { if let Some(nav_history) = &self.nav_history { let buffer = self.buffer.read(cx).read(cx); - let point = position.to_point(&buffer); - let scroll_top_row = self.scroll_top_anchor.to_point(&buffer).row; + let cursor_position = cursor_anchor.to_point(&buffer); + let scroll_state = self.scroll_manager.anchor(); + let scroll_top_row = scroll_state.top_row(&buffer); drop(buffer); if let Some(new_position) = new_position { - let row_delta = (new_position.row as i64 - point.row as i64).abs(); + let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { return; } @@ -4621,10 +4156,9 @@ impl Editor { nav_history.push( Some(NavigationData { - cursor_anchor: position, - cursor_position: point, - scroll_position: self.scroll_position, - scroll_top_anchor: self.scroll_top_anchor, + cursor_anchor, + cursor_position, + scroll_anchor: scroll_state, scroll_top_row, }), cx, @@ -5922,16 +5456,6 @@ impl Editor { }); } - pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { - self.autoscroll_request = Some((autoscroll, true)); - cx.notify(); - } - - fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { - self.autoscroll_request = Some((autoscroll, false)); - cx.notify(); - } - pub fn transact( &mut self, cx: &mut ViewContext, @@ -6340,31 +5864,6 @@ impl Editor { self.blink_manager.read(cx).visible() && self.focused } - pub fn show_scrollbars(&self) -> bool { - self.show_scrollbars - } - - fn make_scrollbar_visible(&mut self, cx: &mut ViewContext) { - if !self.show_scrollbars { - self.show_scrollbars = true; - cx.notify(); - } - - if cx.default_global::().0 { - self.hide_scrollbar_task = Some(cx.spawn_weak(|this, mut cx| async move { - Timer::after(SCROLLBAR_SHOW_INTERVAL).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.show_scrollbars = false; - cx.notify(); - }); - } - })); - } else { - self.hide_scrollbar_task = None; - } - } - fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { cx.notify(); } @@ -6561,11 +6060,7 @@ impl EditorSnapshot { } pub fn scroll_position(&self) -> Vector2F { - compute_scroll_position( - &self.display_snapshot, - self.scroll_position, - &self.scroll_top_anchor, - ) + self.scroll_anchor.scroll_position(&self.display_snapshot) } } @@ -6577,20 +6072,6 @@ impl Deref for EditorSnapshot { } } -fn compute_scroll_position( - snapshot: &DisplaySnapshot, - mut scroll_position: Vector2F, - scroll_top_anchor: &Anchor, -) -> Vector2F { - if *scroll_top_anchor != Anchor::min() { - let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32; - scroll_position.set_y(scroll_top + scroll_position.y()); - } else { - scroll_position.set_y(0.); - } - scroll_position -} - #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Event { BufferEdited, @@ -6603,7 +6084,6 @@ pub enum Event { SelectionsChanged { local: bool }, ScrollPositionChanged { local: bool }, Closed, - IgnoredInput, } pub struct EditorFocused(pub ViewHandle); @@ -6789,7 +6269,6 @@ impl View for Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::IgnoredInput); return; } @@ -6826,7 +6305,6 @@ impl View for Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::IgnoredInput); return; } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ca66ae7dc9..9a6cd23453 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12,7 +12,7 @@ use crate::test::{ }; use gpui::{ executor::Deterministic, - geometry::rect::RectF, + geometry::{rect::RectF, vector::vec2f}, platform::{WindowBounds, WindowOptions}, }; use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry, Point}; @@ -544,31 +544,30 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) { // Set scroll position to check later editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx); - let original_scroll_position = editor.scroll_position; - let original_scroll_top_anchor = editor.scroll_top_anchor; + let original_scroll_position = editor.scroll_manager.anchor(); // Jump to the end of the document and adjust scroll editor.move_to_end(&MoveToEnd, cx); editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx); - assert_ne!(editor.scroll_position, original_scroll_position); - assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor); + assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); let nav_entry = pop_history(&mut editor, cx).unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(editor.scroll_position, original_scroll_position); - assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor); + assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); // Ensure we don't panic when navigation data contains invalid anchors *and* points. - let mut invalid_anchor = editor.scroll_top_anchor; + let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor; invalid_anchor.text_anchor.buffer_id = Some(999); let invalid_point = Point::new(9999, 0); editor.navigate( Box::new(NavigationData { cursor_anchor: invalid_anchor, cursor_position: invalid_point, - scroll_top_anchor: invalid_anchor, + scroll_anchor: ScrollAnchor { + top_anchor: invalid_anchor, + offset: Default::default(), + }, scroll_top_row: invalid_point.row, - scroll_position: Default::default(), }), cx, ); @@ -5034,7 +5033,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) { .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); assert_eq!(follower.scroll_position(cx), initial_scroll_position); - assert!(follower.autoscroll_request.is_some()); + assert!(follower.scroll_manager.has_autoscroll_request()); }); assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8409786637..7d69d3833c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,7 +1,7 @@ use super::{ display_map::{BlockContext, ToDisplayPoint}, - Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Scroll, Select, SelectPhase, - SoftWrap, ToPoint, MAX_LINE_LEN, + Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Select, SelectPhase, SoftWrap, + ToPoint, MAX_LINE_LEN, }; use crate::{ display_map::{BlockStyle, DisplaySnapshot, TransformBlock}, @@ -13,6 +13,7 @@ use crate::{ GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink, }, mouse_context_menu::DeployMouseContextMenu, + scroll::actions::Scroll, EditorStyle, }; use clock::ReplicaId; @@ -955,7 +956,7 @@ impl EditorElement { move |_, cx| { if let Some(view) = view.upgrade(cx.deref_mut()) { view.update(cx.deref_mut(), |view, cx| { - view.make_scrollbar_visible(cx); + view.scroll_manager.show_scrollbar(cx); }); } } @@ -977,7 +978,7 @@ impl EditorElement { position.set_y(top_row as f32); view.set_scroll_position(position, cx); } else { - view.make_scrollbar_visible(cx); + view.scroll_manager.show_scrollbar(cx); } }); } @@ -1298,7 +1299,7 @@ impl EditorElement { }; let tooltip_style = cx.global::().theme.tooltip.clone(); - let scroll_x = snapshot.scroll_position.x(); + let scroll_x = snapshot.scroll_anchor.offset.x(); let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) .partition::, _>(|(_, block)| match block { @@ -1670,7 +1671,7 @@ impl Element for EditorElement { )); } - show_scrollbars = view.show_scrollbars(); + show_scrollbars = view.scroll_manager.scrollbars_visible(); include_root = view .project .as_ref() @@ -1725,7 +1726,7 @@ impl Element for EditorElement { ); self.update_view(cx.app, |view, cx| { - let clamped = view.clamp_scroll_left(scroll_max.x()); + let clamped = view.scroll_manager.clamp_scroll_left(scroll_max.x()); let autoscrolled = if autoscroll_horizontally { view.autoscroll_horizontally( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index afe659af61..4779fe73b8 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -26,8 +26,9 @@ use workspace::{ use crate::{ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, - movement::surrounding_word, persistence::DB, Anchor, Autoscroll, Editor, Event, ExcerptId, - MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT, + movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, + Event, ExcerptId, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, + FORMAT_TIMEOUT, }; pub const MAX_TAB_TITLE_LEN: usize = 24; @@ -87,14 +88,16 @@ impl FollowableItem for Editor { } if let Some(anchor) = state.scroll_top_anchor { - editor.set_scroll_top_anchor( - Anchor { - buffer_id: Some(state.buffer_id as usize), - excerpt_id, - text_anchor: language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?, + editor.set_scroll_anchor( + ScrollAnchor { + top_anchor: Anchor { + buffer_id: Some(state.buffer_id as usize), + excerpt_id, + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }, + offset: vec2f(state.scroll_x, state.scroll_y), }, - vec2f(state.scroll_x, state.scroll_y), cx, ); } @@ -132,13 +135,14 @@ impl FollowableItem for Editor { fn to_state_proto(&self, cx: &AppContext) -> Option { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); + let scroll_anchor = self.scroll_manager.anchor(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, scroll_top_anchor: Some(language::proto::serialize_anchor( - &self.scroll_top_anchor.text_anchor, + &scroll_anchor.top_anchor.text_anchor, )), - scroll_x: self.scroll_position.x(), - scroll_y: self.scroll_position.y(), + scroll_x: scroll_anchor.offset.x(), + scroll_y: scroll_anchor.offset.y(), selections: self .selections .disjoint_anchors() @@ -160,11 +164,12 @@ impl FollowableItem for Editor { match update { proto::update_view::Variant::Editor(update) => match event { Event::ScrollPositionChanged { .. } => { + let scroll_anchor = self.scroll_manager.anchor(); update.scroll_top_anchor = Some(language::proto::serialize_anchor( - &self.scroll_top_anchor.text_anchor, + &scroll_anchor.top_anchor.text_anchor, )); - update.scroll_x = self.scroll_position.x(); - update.scroll_y = self.scroll_position.y(); + update.scroll_x = scroll_anchor.offset.x(); + update.scroll_y = scroll_anchor.offset.y(); true } Event::SelectionsChanged { .. } => { @@ -207,14 +212,16 @@ impl FollowableItem for Editor { self.set_selections_from_remote(selections, cx); self.request_autoscroll_remotely(Autoscroll::newest(), cx); } else if let Some(anchor) = message.scroll_top_anchor { - self.set_scroll_top_anchor( - Anchor { - buffer_id: Some(buffer_id), - excerpt_id, - text_anchor: language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?, + self.set_scroll_anchor( + ScrollAnchor { + top_anchor: Anchor { + buffer_id: Some(buffer_id), + excerpt_id, + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }, + offset: vec2f(message.scroll_x, message.scroll_y), }, - vec2f(message.scroll_x, message.scroll_y), cx, ); } @@ -279,13 +286,12 @@ impl Item for Editor { buffer.clip_point(data.cursor_position, Bias::Left) }; - let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) { - data.scroll_top_anchor - } else { - buffer.anchor_before( + let mut scroll_anchor = data.scroll_anchor; + if !buffer.can_resolve(&scroll_anchor.top_anchor) { + scroll_anchor.top_anchor = buffer.anchor_before( buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), - ) - }; + ); + } drop(buffer); @@ -293,8 +299,7 @@ impl Item for Editor { false } else { let nav_history = self.nav_history.take(); - self.scroll_position = data.scroll_position; - self.scroll_top_anchor = scroll_top_anchor; + self.set_scroll_anchor(data.scroll_anchor, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([offset..offset]) }); diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs new file mode 100644 index 0000000000..78bc3685c1 --- /dev/null +++ b/crates/editor/src/scroll.rs @@ -0,0 +1,339 @@ +pub mod actions; +pub mod autoscroll; +pub mod scroll_amount; + +use std::{ + cmp::Ordering, + time::{Duration, Instant}, +}; + +use gpui::{ + geometry::vector::{vec2f, Vector2F}, + Axis, MutableAppContext, Task, ViewContext, +}; +use language::Bias; + +use crate::{ + display_map::{DisplaySnapshot, ToDisplayPoint}, + hover_popover::hide_hover, + Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint, +}; + +use self::{ + autoscroll::{Autoscroll, AutoscrollStrategy}, + scroll_amount::ScrollAmount, +}; + +pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); +const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); + +#[derive(Default)] +pub struct ScrollbarAutoHide(pub bool); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ScrollAnchor { + pub offset: Vector2F, + pub top_anchor: Anchor, +} + +impl ScrollAnchor { + fn new() -> Self { + Self { + offset: Vector2F::zero(), + top_anchor: Anchor::min(), + } + } + + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + let mut scroll_position = self.offset; + if self.top_anchor != Anchor::min() { + let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32; + scroll_position.set_y(scroll_top + scroll_position.y()); + } else { + scroll_position.set_y(0.); + } + scroll_position + } + + pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { + self.top_anchor.to_point(buffer).row + } +} + +#[derive(Clone, Copy, Debug)] +pub struct OngoingScroll { + last_event: Instant, + axis: Option, +} + +impl OngoingScroll { + fn new() -> Self { + Self { + last_event: Instant::now() - SCROLL_EVENT_SEPARATION, + axis: None, + } + } + + pub fn filter(&self, delta: &mut Vector2F) -> Option { + const UNLOCK_PERCENT: f32 = 1.9; + const UNLOCK_LOWER_BOUND: f32 = 6.; + let mut axis = self.axis; + + let x = delta.x().abs(); + let y = delta.y().abs(); + let duration = Instant::now().duration_since(self.last_event); + if duration > SCROLL_EVENT_SEPARATION { + //New ongoing scroll will start, determine axis + axis = if x <= y { + Some(Axis::Vertical) + } else { + Some(Axis::Horizontal) + }; + } else if x.max(y) >= UNLOCK_LOWER_BOUND { + //Check if the current ongoing will need to unlock + match axis { + Some(Axis::Vertical) => { + if x > y && x >= y * UNLOCK_PERCENT { + axis = None; + } + } + + Some(Axis::Horizontal) => { + if y > x && y >= x * UNLOCK_PERCENT { + axis = None; + } + } + + None => {} + } + } + + match axis { + Some(Axis::Vertical) => *delta = vec2f(0., delta.y()), + Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.), + None => {} + } + + axis + } +} + +pub struct ScrollManager { + vertical_scroll_margin: f32, + anchor: ScrollAnchor, + ongoing: OngoingScroll, + autoscroll_request: Option<(Autoscroll, bool)>, + last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>, + show_scrollbars: bool, + hide_scrollbar_task: Option>, + visible_line_count: Option, +} + +impl ScrollManager { + pub fn new() -> Self { + ScrollManager { + vertical_scroll_margin: 3.0, + anchor: ScrollAnchor::new(), + ongoing: OngoingScroll::new(), + autoscroll_request: None, + show_scrollbars: true, + hide_scrollbar_task: None, + last_autoscroll: None, + visible_line_count: None, + } + } + + pub fn clone_state(&mut self, other: &Self) { + self.anchor = other.anchor; + self.ongoing = other.ongoing; + } + + pub fn anchor(&self) -> ScrollAnchor { + self.anchor + } + + pub fn ongoing_scroll(&self) -> OngoingScroll { + self.ongoing + } + + pub fn update_ongoing_scroll(&mut self, axis: Option) { + self.ongoing.last_event = Instant::now(); + self.ongoing.axis = axis; + } + + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + self.anchor.scroll_position(snapshot) + } + + fn set_scroll_position( + &mut self, + scroll_position: Vector2F, + map: &DisplaySnapshot, + local: bool, + cx: &mut ViewContext, + ) { + let new_anchor = if scroll_position.y() <= 0. { + ScrollAnchor { + top_anchor: Anchor::min(), + offset: scroll_position.max(vec2f(0., 0.)), + } + } else { + let scroll_top_buffer_offset = + DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); + let top_anchor = map + .buffer_snapshot + .anchor_at(scroll_top_buffer_offset, Bias::Right); + + ScrollAnchor { + top_anchor, + offset: vec2f( + scroll_position.x(), + scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, + ), + } + }; + + self.set_anchor(new_anchor, local, cx); + } + + fn set_anchor(&mut self, anchor: ScrollAnchor, local: bool, cx: &mut ViewContext) { + self.anchor = anchor; + cx.emit(Event::ScrollPositionChanged { local }); + self.show_scrollbar(cx); + self.autoscroll_request.take(); + cx.notify(); + } + + pub fn show_scrollbar(&mut self, cx: &mut ViewContext) { + if !self.show_scrollbars { + self.show_scrollbars = true; + cx.notify(); + } + + if cx.default_global::().0 { + self.hide_scrollbar_task = Some(cx.spawn_weak(|editor, mut cx| async move { + cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await; + if let Some(editor) = editor.upgrade(&cx) { + editor.update(&mut cx, |editor, cx| { + editor.scroll_manager.show_scrollbars = false; + cx.notify(); + }); + } + })); + } else { + self.hide_scrollbar_task = None; + } + } + + pub fn scrollbars_visible(&self) -> bool { + self.show_scrollbars + } + + pub fn has_autoscroll_request(&self) -> bool { + self.autoscroll_request.is_some() + } + + pub fn clamp_scroll_left(&mut self, max: f32) -> bool { + if max < self.anchor.offset.x() { + self.anchor.offset.set_x(max); + true + } else { + false + } + } +} + +impl Editor { + pub fn vertical_scroll_margin(&mut self) -> usize { + self.scroll_manager.vertical_scroll_margin as usize + } + + pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext) { + self.scroll_manager.vertical_scroll_margin = margin_rows as f32; + cx.notify(); + } + + pub fn visible_line_count(&self) -> Option { + self.scroll_manager.visible_line_count + } + + pub(crate) fn set_visible_line_count(&mut self, lines: f32) { + self.scroll_manager.visible_line_count = Some(lines) + } + + pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + self.set_scroll_position_internal(scroll_position, true, cx); + } + + pub(crate) fn set_scroll_position_internal( + &mut self, + scroll_position: Vector2F, + local: bool, + cx: &mut ViewContext, + ) { + let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + hide_hover(self, cx); + self.scroll_manager + .set_scroll_position(scroll_position, &map, local, cx); + } + + pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + self.scroll_manager.anchor.scroll_position(&display_map) + } + + pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext) { + hide_hover(self, cx); + self.scroll_manager.set_anchor(scroll_anchor, true, cx); + } + + pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + if self.take_rename(true, cx).is_some() { + return; + } + + if amount.move_context_menu_selection(self, cx) { + return; + } + + let cur_position = self.scroll_position(cx); + let new_pos = cur_position + vec2f(0., amount.lines(self) - 1.); + self.set_scroll_position(new_pos, cx); + } + + /// Returns an ordering. The newest selection is: + /// Ordering::Equal => on screen + /// Ordering::Less => above the screen + /// Ordering::Greater => below the screen + pub fn newest_selection_on_screen(&self, cx: &mut MutableAppContext) -> Ordering { + let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let newest_head = self + .selections + .newest_anchor() + .head() + .to_display_point(&snapshot); + let screen_top = self + .scroll_manager + .anchor + .top_anchor + .to_display_point(&snapshot); + + if screen_top > newest_head { + return Ordering::Less; + } + + if let Some(visible_lines) = self.visible_line_count() { + if newest_head.row() < screen_top.row() + visible_lines as u32 { + return Ordering::Equal; + } + } + + Ordering::Greater + } +} diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs new file mode 100644 index 0000000000..8e57402532 --- /dev/null +++ b/crates/editor/src/scroll/actions.rs @@ -0,0 +1,159 @@ +use gpui::{ + actions, geometry::vector::Vector2F, impl_internal_actions, Axis, MutableAppContext, + ViewContext, +}; +use language::Bias; + +use crate::{Editor, EditorMode}; + +use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor}; + +actions!( + editor, + [ + LineDown, + LineUp, + HalfPageDown, + HalfPageUp, + PageDown, + PageUp, + NextScreen, + ScrollCursorTop, + ScrollCursorCenter, + ScrollCursorBottom, + ] +); + +#[derive(Clone, PartialEq)] +pub struct Scroll { + pub scroll_position: Vector2F, + pub axis: Option, +} + +impl_internal_actions!(editor, [Scroll]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(Editor::next_screen); + cx.add_action(Editor::scroll); + cx.add_action(Editor::scroll_cursor_top); + cx.add_action(Editor::scroll_cursor_center); + cx.add_action(Editor::scroll_cursor_bottom); + cx.add_action(|this: &mut Editor, _: &LineDown, cx| { + this.scroll_screen(&ScrollAmount::LineDown, cx) + }); + cx.add_action(|this: &mut Editor, _: &LineUp, cx| { + this.scroll_screen(&ScrollAmount::LineUp, cx) + }); + cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| { + this.scroll_screen(&ScrollAmount::HalfPageDown, cx) + }); + cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| { + this.scroll_screen(&ScrollAmount::HalfPageUp, cx) + }); + cx.add_action(|this: &mut Editor, _: &PageDown, cx| { + this.scroll_screen(&ScrollAmount::PageDown, cx) + }); + cx.add_action(|this: &mut Editor, _: &PageUp, cx| { + this.scroll_screen(&ScrollAmount::PageUp, cx) + }); +} + +impl Editor { + pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext) -> Option<()> { + if self.take_rename(true, cx).is_some() { + return None; + } + + self.context_menu.as_mut()?; + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return None; + } + + self.request_autoscroll(Autoscroll::Next, cx); + + Some(()) + } + + fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext) { + self.scroll_manager.update_ongoing_scroll(action.axis); + self.set_scroll_position(action.scroll_position, cx); + } + + fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext) { + let snapshot = editor.snapshot(cx).display_snapshot; + let scroll_margin_rows = editor.vertical_scroll_margin() as u32; + + let mut new_screen_top = editor.selections.newest_display(cx).head(); + *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows); + *new_screen_top.column_mut() = 0; + let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); + let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); + + editor.set_scroll_anchor( + ScrollAnchor { + top_anchor: new_anchor, + offset: Default::default(), + }, + cx, + ) + } + + fn scroll_cursor_center( + editor: &mut Editor, + _: &ScrollCursorCenter, + cx: &mut ViewContext, + ) { + let snapshot = editor.snapshot(cx).display_snapshot; + let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { + visible_rows as u32 + } else { + return; + }; + + let mut new_screen_top = editor.selections.newest_display(cx).head(); + *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2); + *new_screen_top.column_mut() = 0; + let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); + let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); + + editor.set_scroll_anchor( + ScrollAnchor { + top_anchor: new_anchor, + offset: Default::default(), + }, + cx, + ) + } + + fn scroll_cursor_bottom( + editor: &mut Editor, + _: &ScrollCursorBottom, + cx: &mut ViewContext, + ) { + let snapshot = editor.snapshot(cx).display_snapshot; + let scroll_margin_rows = editor.vertical_scroll_margin() as u32; + let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { + visible_rows as u32 + } else { + return; + }; + + let mut new_screen_top = editor.selections.newest_display(cx).head(); + *new_screen_top.row_mut() = new_screen_top + .row() + .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows)); + *new_screen_top.column_mut() = 0; + let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); + let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); + + editor.set_scroll_anchor( + ScrollAnchor { + top_anchor: new_anchor, + offset: Default::default(), + }, + cx, + ) + } +} diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs new file mode 100644 index 0000000000..63ee7c56ca --- /dev/null +++ b/crates/editor/src/scroll/autoscroll.rs @@ -0,0 +1,246 @@ +use std::cmp; + +use gpui::{text_layout, ViewContext}; +use language::Point; + +use crate::{display_map::ToDisplayPoint, Editor, EditorMode}; + +#[derive(PartialEq, Eq)] +pub enum Autoscroll { + Next, + Strategy(AutoscrollStrategy), +} + +impl Autoscroll { + pub fn fit() -> Self { + Self::Strategy(AutoscrollStrategy::Fit) + } + + pub fn newest() -> Self { + Self::Strategy(AutoscrollStrategy::Newest) + } + + pub fn center() -> Self { + Self::Strategy(AutoscrollStrategy::Center) + } +} + +#[derive(PartialEq, Eq, Default)] +pub enum AutoscrollStrategy { + Fit, + Newest, + #[default] + Center, + Top, + Bottom, +} + +impl AutoscrollStrategy { + fn next(&self) -> Self { + match self { + AutoscrollStrategy::Center => AutoscrollStrategy::Top, + AutoscrollStrategy::Top => AutoscrollStrategy::Bottom, + _ => AutoscrollStrategy::Center, + } + } +} + +impl Editor { + pub fn autoscroll_vertically( + &mut self, + viewport_height: f32, + line_height: f32, + cx: &mut ViewContext, + ) -> bool { + let visible_lines = viewport_height / line_height; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut scroll_position = self.scroll_manager.scroll_position(&display_map); + let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) + } else { + display_map.max_point().row() as f32 + }; + if scroll_position.y() > max_scroll_top { + scroll_position.set_y(max_scroll_top); + self.set_scroll_position(scroll_position, cx); + } + + let (autoscroll, local) = + if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() { + autoscroll + } else { + return false; + }; + + let first_cursor_top; + let last_cursor_bottom; + if let Some(highlighted_rows) = &self.highlighted_rows { + first_cursor_top = highlighted_rows.start as f32; + last_cursor_bottom = first_cursor_top + 1.; + } else if autoscroll == Autoscroll::newest() { + let newest_selection = self.selections.newest::(cx); + first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; + last_cursor_bottom = first_cursor_top + 1.; + } else { + let selections = self.selections.all::(cx); + first_cursor_top = selections + .first() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + last_cursor_bottom = selections + .last() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32 + + 1.0; + } + + let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + 0. + } else { + ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() + }; + if margin < 0.0 { + return false; + } + + let strategy = match autoscroll { + Autoscroll::Strategy(strategy) => strategy, + Autoscroll::Next => { + let last_autoscroll = &self.scroll_manager.last_autoscroll; + if let Some(last_autoscroll) = last_autoscroll { + if self.scroll_manager.anchor.offset == last_autoscroll.0 + && first_cursor_top == last_autoscroll.1 + && last_cursor_bottom == last_autoscroll.2 + { + last_autoscroll.3.next() + } else { + AutoscrollStrategy::default() + } + } else { + AutoscrollStrategy::default() + } + } + }; + + match strategy { + AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { + let margin = margin.min(self.scroll_manager.vertical_scroll_margin); + let target_top = (first_cursor_top - margin).max(0.0); + let target_bottom = last_cursor_bottom + margin; + let start_row = scroll_position.y(); + let end_row = start_row + visible_lines; + + if target_top < start_row { + scroll_position.set_y(target_top); + self.set_scroll_position_internal(scroll_position, local, cx); + } else if target_bottom >= end_row { + scroll_position.set_y(target_bottom - visible_lines); + self.set_scroll_position_internal(scroll_position, local, cx); + } + } + AutoscrollStrategy::Center => { + scroll_position.set_y((first_cursor_top - margin).max(0.0)); + self.set_scroll_position_internal(scroll_position, local, cx); + } + AutoscrollStrategy::Top => { + scroll_position.set_y((first_cursor_top).max(0.0)); + self.set_scroll_position_internal(scroll_position, local, cx); + } + AutoscrollStrategy::Bottom => { + scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0)); + self.set_scroll_position_internal(scroll_position, local, cx); + } + } + + self.scroll_manager.last_autoscroll = Some(( + self.scroll_manager.anchor.offset, + first_cursor_top, + last_cursor_bottom, + strategy, + )); + + true + } + + pub fn autoscroll_horizontally( + &mut self, + start_row: u32, + viewport_width: f32, + scroll_width: f32, + max_glyph_width: f32, + layouts: &[text_layout::Line], + cx: &mut ViewContext, + ) -> bool { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + + let mut target_left; + let mut target_right; + + if self.highlighted_rows.is_some() { + target_left = 0.0_f32; + target_right = 0.0_f32; + } else { + target_left = std::f32::INFINITY; + target_right = 0.0_f32; + for selection in selections { + let head = selection.head().to_display_point(&display_map); + if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { + let start_column = head.column().saturating_sub(3); + let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); + target_left = target_left.min( + layouts[(head.row() - start_row) as usize] + .x_for_index(start_column as usize), + ); + target_right = target_right.max( + layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) + + max_glyph_width, + ); + } + } + } + + target_right = target_right.min(scroll_width); + + if target_right - target_left > viewport_width { + return false; + } + + let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width; + let scroll_right = scroll_left + viewport_width; + + if target_left < scroll_left { + self.scroll_manager + .anchor + .offset + .set_x(target_left / max_glyph_width); + true + } else if target_right > scroll_right { + self.scroll_manager + .anchor + .offset + .set_x((target_right - viewport_width) / max_glyph_width); + true + } else { + false + } + } + + pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + self.scroll_manager.autoscroll_request = Some((autoscroll, true)); + cx.notify(); + } + + pub(crate) fn request_autoscroll_remotely( + &mut self, + autoscroll: Autoscroll, + cx: &mut ViewContext, + ) { + self.scroll_manager.autoscroll_request = Some((autoscroll, false)); + cx.notify(); + } +} diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs new file mode 100644 index 0000000000..6f6c21f0d4 --- /dev/null +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -0,0 +1,48 @@ +use gpui::ViewContext; +use serde::Deserialize; +use util::iife; + +use crate::Editor; + +#[derive(Clone, PartialEq, Deserialize)] +pub enum ScrollAmount { + LineUp, + LineDown, + HalfPageUp, + HalfPageDown, + PageUp, + PageDown, +} + +impl ScrollAmount { + pub fn move_context_menu_selection( + &self, + editor: &mut Editor, + cx: &mut ViewContext, + ) -> bool { + iife!({ + let context_menu = editor.context_menu.as_mut()?; + + match self { + Self::LineDown | Self::HalfPageDown => context_menu.select_next(cx), + Self::LineUp | Self::HalfPageUp => context_menu.select_prev(cx), + Self::PageDown => context_menu.select_last(cx), + Self::PageUp => context_menu.select_first(cx), + } + .then_some(()) + }) + .is_some() + } + + pub fn lines(&self, editor: &mut Editor) -> f32 { + match self { + Self::LineDown => 1., + Self::LineUp => -1., + Self::HalfPageDown => editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.), + Self::HalfPageUp => -editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.), + // Minus 1. here so that there is a pivot line that stays on the screen + Self::PageDown => editor.visible_line_count().unwrap_or(1.) - 1., + Self::PageUp => -editor.visible_line_count().unwrap_or(1.) - 1., + } + } +} diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index facc1b0491..f1c19bca8a 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -61,7 +61,7 @@ impl SelectionsCollection { self.buffer.read(cx).read(cx) } - pub fn set_state(&mut self, other: &SelectionsCollection) { + pub fn clone_state(&mut self, other: &SelectionsCollection) { self.next_selection_id = other.next_selection_id; self.line_mode = other.line_mode; self.disjoint = other.disjoint.clone(); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 4db3d1310b..32c7d3c810 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor}; +use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use gpui::{ actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c62305f572..bf78399914 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -594,6 +594,9 @@ type ReleaseObservationCallback = Box; type WindowActivationCallback = Box bool>; type WindowFullscreenCallback = Box bool>; +type KeystrokeCallback = Box< + dyn FnMut(&Keystroke, &MatchResult, Option<&Box>, &mut MutableAppContext) -> bool, +>; type DeserializeActionCallback = fn(json: &str) -> anyhow::Result>; type WindowShouldCloseSubscriptionCallback = Box bool>; @@ -619,6 +622,7 @@ pub struct MutableAppContext { observations: CallbackCollection, window_activation_observations: CallbackCollection, window_fullscreen_observations: CallbackCollection, + keystroke_observations: CallbackCollection, release_observations: Arc>>>, action_dispatch_observations: Arc>>, @@ -678,6 +682,7 @@ impl MutableAppContext { global_observations: Default::default(), window_activation_observations: Default::default(), window_fullscreen_observations: Default::default(), + keystroke_observations: Default::default(), action_dispatch_observations: Default::default(), presenters_and_platform_windows: Default::default(), foreground, @@ -763,11 +768,11 @@ impl MutableAppContext { .with_context(|| format!("invalid data for action {}", name)) } - pub fn add_action(&mut self, handler: F) + pub fn add_action(&mut self, handler: F) where A: Action, V: View, - F: 'static + FnMut(&mut V, &A, &mut ViewContext), + F: 'static + FnMut(&mut V, &A, &mut ViewContext) -> R, { self.add_action_internal(handler, false) } @@ -781,11 +786,11 @@ impl MutableAppContext { self.add_action_internal(handler, true) } - fn add_action_internal(&mut self, mut handler: F, capture: bool) + fn add_action_internal(&mut self, mut handler: F, capture: bool) where A: Action, V: View, - F: 'static + FnMut(&mut V, &A, &mut ViewContext), + F: 'static + FnMut(&mut V, &A, &mut ViewContext) -> R, { let handler = Box::new( move |view: &mut dyn AnyView, @@ -1255,6 +1260,27 @@ impl MutableAppContext { } } + pub fn observe_keystrokes(&mut self, window_id: usize, callback: F) -> Subscription + where + F: 'static + + FnMut( + &Keystroke, + &MatchResult, + Option<&Box>, + &mut MutableAppContext, + ) -> bool, + { + let subscription_id = post_inc(&mut self.next_subscription_id); + self.keystroke_observations + .add_callback(window_id, subscription_id, Box::new(callback)); + + Subscription::KeystrokeObservation { + id: subscription_id, + window_id, + observations: Some(self.keystroke_observations.downgrade()), + } + } + pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) { self.pending_effects.push_back(Effect::Deferred { callback: Box::new(callback), @@ -1538,27 +1564,39 @@ impl MutableAppContext { }) .collect(); - match self + let match_result = self .keystroke_matcher - .push_keystroke(keystroke.clone(), dispatch_path) - { + .push_keystroke(keystroke.clone(), dispatch_path); + let mut handled_by = None; + + let keystroke_handled = match &match_result { MatchResult::None => false, MatchResult::Pending => true, MatchResult::Matches(matches) => { for (view_id, action) in matches { if self.handle_dispatch_action_from_effect( window_id, - Some(view_id), + Some(*view_id), action.as_ref(), ) { self.keystroke_matcher.clear_pending(); - return true; + handled_by = Some(action.boxed_clone()); + break; } } - false + handled_by.is_some() } - } + }; + + self.keystroke( + window_id, + keystroke.clone(), + handled_by, + match_result.clone(), + ); + keystroke_handled } else { + self.keystroke(window_id, keystroke.clone(), None, MatchResult::None); false } } @@ -2110,6 +2148,12 @@ impl MutableAppContext { } => { self.handle_window_should_close_subscription_effect(window_id, callback) } + Effect::Keystroke { + window_id, + keystroke, + handled_by, + result, + } => self.handle_keystroke_effect(window_id, keystroke, handled_by, result), } self.pending_notifications.clear(); self.remove_dropped_entities(); @@ -2188,6 +2232,21 @@ impl MutableAppContext { }); } + fn keystroke( + &mut self, + window_id: usize, + keystroke: Keystroke, + handled_by: Option>, + result: MatchResult, + ) { + self.pending_effects.push_back(Effect::Keystroke { + window_id, + keystroke, + handled_by, + result, + }); + } + pub fn refresh_windows(&mut self) { self.pending_effects.push_back(Effect::RefreshWindows); } @@ -2299,6 +2358,21 @@ impl MutableAppContext { }); } + fn handle_keystroke_effect( + &mut self, + window_id: usize, + keystroke: Keystroke, + handled_by: Option>, + result: MatchResult, + ) { + self.update(|this| { + let mut observations = this.keystroke_observations.clone(); + observations.emit_and_cleanup(window_id, this, { + move |callback, this| callback(&keystroke, &result, handled_by.as_ref(), this) + }); + }); + } + fn handle_window_activation_effect(&mut self, window_id: usize, active: bool) { //Short circuit evaluation if we're already g2g if self @@ -2852,6 +2926,12 @@ pub enum Effect { subscription_id: usize, callback: WindowFullscreenCallback, }, + Keystroke { + window_id: usize, + keystroke: Keystroke, + handled_by: Option>, + result: MatchResult, + }, RefreshWindows, DispatchActionFrom { window_id: usize, @@ -2995,6 +3075,21 @@ impl Debug for Effect { .debug_struct("Effect::WindowShouldCloseSubscription") .field("window_id", window_id) .finish(), + Effect::Keystroke { + window_id, + keystroke, + handled_by, + result, + } => f + .debug_struct("Effect::Keystroke") + .field("window_id", window_id) + .field("keystroke", keystroke) + .field( + "keystroke", + &handled_by.as_ref().map(|handled_by| handled_by.name()), + ) + .field("result", result) + .finish(), } } } @@ -3826,6 +3921,33 @@ impl<'a, T: View> ViewContext<'a, T> { }) } + pub fn observe_keystroke(&mut self, mut callback: F) -> Subscription + where + F: 'static + + FnMut( + &mut T, + &Keystroke, + Option<&Box>, + &MatchResult, + &mut ViewContext, + ) -> bool, + { + let observer = self.weak_handle(); + self.app.observe_keystrokes( + self.window_id(), + move |keystroke, result, handled_by, cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, keystroke, handled_by, result, cx); + }); + true + } else { + false + } + }, + ) + } + pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.view_id, @@ -5018,6 +5140,11 @@ pub enum Subscription { window_id: usize, observations: Option>>, }, + KeystrokeObservation { + id: usize, + window_id: usize, + observations: Option>>, + }, ReleaseObservation { id: usize, @@ -5056,6 +5183,9 @@ impl Subscription { Subscription::ActionObservation { observations, .. } => { observations.take(); } + Subscription::KeystrokeObservation { observations, .. } => { + observations.take(); + } Subscription::WindowActivationObservation { observations, .. } => { observations.take(); } @@ -5175,6 +5305,27 @@ impl Drop for Subscription { observations.lock().remove(id); } } + Subscription::KeystrokeObservation { + id, + window_id, + observations, + } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + match observations + .lock() + .entry(*window_id) + .or_default() + .entry(*id) + { + btree_map::Entry::Vacant(entry) => { + entry.insert(None); + } + btree_map::Entry::Occupied(entry) => { + entry.remove(); + } + } + } + } Subscription::WindowActivationObservation { id, window_id, diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index fc97f69624..e9bc228757 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -112,6 +112,21 @@ impl PartialEq for MatchResult { impl Eq for MatchResult {} +impl Clone for MatchResult { + fn clone(&self) -> Self { + match self { + MatchResult::None => MatchResult::None, + MatchResult::Pending => MatchResult::Pending, + MatchResult::Matches(matches) => MatchResult::Matches( + matches + .iter() + .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref()))) + .collect(), + ), + } + } +} + impl Matcher { pub fn new(keymap: Keymap) -> Self { Self { diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index ef1dbdc15c..76a56af93d 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -1,5 +1,5 @@ use chrono::{Datelike, Local, NaiveTime, Timelike}; -use editor::{Autoscroll, Editor}; +use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{actions, MutableAppContext}; use settings::{HourFormat, Settings}; use std::{ diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index abb5e8d3df..f6698e23be 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,6 +1,6 @@ use editor::{ - combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, Anchor, AnchorRangeExt, - Autoscroll, DisplayPoint, Editor, ToPoint, + combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, + scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor, ToPoint, }; use fuzzy::StringMatch; use gpui::{ diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 273230fe26..957292f035 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -1,5 +1,6 @@ use editor::{ - combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor, + combine_syntax_and_fuzzy_match_highlights, scroll::autoscroll::Autoscroll, + styled_runs_for_code_label, Bias, Editor, }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6fa7d07d6f..13b754a417 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -4,8 +4,8 @@ use crate::{ }; use collections::HashMap; use editor::{ - items::active_match_index, Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, - MAX_TAB_TITLE_LEN, + items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, + SelectAll, MAX_TAB_TITLE_LEN, }; use gpui::{ actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 0e83bb5f19..d9015ca6c0 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -216,6 +216,8 @@ pub fn unzip_option(option: Option<(T, U)>) -> (Option, Option) { } } +/// Immediately invoked function expression. Good for using the ? operator +/// in functions which do not return an Option or Result #[macro_export] macro_rules! iife { ($block:block) => { @@ -223,6 +225,8 @@ macro_rules! iife { }; } +/// Async lImmediately invoked function expression. Good for using the ? operator +/// in functions which do not return an Option or Result. Async version of above #[macro_export] macro_rules! async_iife { ($block:block) => { diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 68f36e8fc6..7b777a50ed 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -22,20 +22,9 @@ 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() { - 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); - } - }); - } - _ => (), + 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); } } })); diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 8bfb8952d5..d8aea4aa33 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -1,5 +1,5 @@ use crate::{state::Mode, Vim}; -use editor::{Autoscroll, Bias}; +use editor::{scroll::autoscroll::Autoscroll, Bias}; use gpui::{actions, MutableAppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index e4a2749d75..bc65fbd09e 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; +use std::{borrow::Cow, cmp::Ordering}; use crate::{ motion::Motion, @@ -12,10 +12,13 @@ use crate::{ }; use collections::{HashMap, HashSet}; use editor::{ - display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, ClipboardSelection, DisplayPoint, + display_map::ToDisplayPoint, + scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, + Anchor, Bias, ClipboardSelection, DisplayPoint, Editor, }; -use gpui::{actions, MutableAppContext, ViewContext}; +use gpui::{actions, impl_actions, MutableAppContext, ViewContext}; use language::{AutoindentMode, Point, SelectionGoal}; +use serde::Deserialize; use workspace::Workspace; use self::{ @@ -24,6 +27,9 @@ use self::{ yank::{yank_motion, yank_object}, }; +#[derive(Clone, PartialEq, Deserialize)] +struct Scroll(ScrollAmount); + actions!( vim, [ @@ -41,6 +47,8 @@ actions!( ] ); +impl_actions!(vim, [Scroll]); + pub fn init(cx: &mut MutableAppContext) { cx.add_action(insert_after); cx.add_action(insert_first_non_whitespace); @@ -72,6 +80,13 @@ pub fn init(cx: &mut MutableAppContext) { }) }); cx.add_action(paste); + cx.add_action(|_: &mut Workspace, Scroll(amount): &Scroll, cx| { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |editor, cx| { + scroll(editor, amount, cx); + }) + }) + }); } pub fn normal_motion( @@ -367,6 +382,46 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { }); } +fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext) { + let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq(); + editor.scroll_screen(amount, cx); + if should_move_cursor { + let selection_ordering = editor.newest_selection_on_screen(cx); + if selection_ordering.is_eq() { + return; + } + + let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { + visible_rows as u32 + } else { + return; + }; + + let scroll_margin_rows = editor.vertical_scroll_margin() as u32; + let top_anchor = editor.scroll_manager.anchor().top_anchor; + + editor.change_selections(None, cx, |s| { + s.replace_cursors_with(|snapshot| { + let mut new_point = top_anchor.to_display_point(&snapshot); + + match selection_ordering { + Ordering::Less => { + *new_point.row_mut() += scroll_margin_rows; + new_point = snapshot.clip_point(new_point, Bias::Right); + } + Ordering::Greater => { + *new_point.row_mut() += visible_rows - scroll_margin_rows as u32; + new_point = snapshot.clip_point(new_point, Bias::Left); + } + Ordering::Equal => unreachable!(), + } + + vec![new_point] + }) + }); + } +} + #[cfg(test)] mod test { use indoc::indoc; diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 59c0a654a4..a32888f59e 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,6 +1,7 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim}; use editor::{ - char_kind, display_map::DisplaySnapshot, movement, Autoscroll, CharKind, DisplayPoint, + char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind, + DisplayPoint, }; use gpui::MutableAppContext; use language::Selection; diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 6b6349578f..b22579438f 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,6 +1,6 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::{HashMap, HashSet}; -use editor::{display_map::ToDisplayPoint, Autoscroll, Bias}; +use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias}; use gpui::MutableAppContext; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index b5acb50e7c..6bbab1ae42 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -18,6 +18,7 @@ impl Default for Mode { #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] pub enum Namespace { G, + Z, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] @@ -95,6 +96,7 @@ impl Operator { let operator_context = match operator { Some(Operator::Number(_)) => "n", Some(Operator::Namespace(Namespace::G)) => "g", + Some(Operator::Namespace(Namespace::Z)) => "z", Some(Operator::Object { around: false }) => "i", Some(Operator::Object { around: true }) => "a", Some(Operator::Change) => "c", diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index ce3a7e2366..4121d6f4bb 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -81,6 +81,28 @@ pub fn init(cx: &mut MutableAppContext) { .detach(); } +// Any keystrokes not mapped to vim should clar the active operator +pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) { + cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| { + dbg!(_keystroke); + dbg!(_result); + if let Some(handled_by) = handled_by { + dbg!(handled_by.name()); + if handled_by.namespace() == "vim" { + return true; + } + } + + Vim::update(cx, |vim, cx| { + if vim.active_operator().is_some() { + vim.clear_operator(cx); + } + }); + true + }) + .detach() +} + #[derive(Default)] pub struct Vim { editors: HashMap>, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 95f6c3d8b4..ef5bb6ddd8 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,7 +1,9 @@ use std::borrow::Cow; use collections::HashMap; -use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection}; +use editor::{ + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection, +}; use gpui::{actions, MutableAppContext, ViewContext}; use language::{AutoindentMode, SelectionGoal}; use workspace::Workspace; diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 9b1342ecd9..0879166bbe 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -175,21 +175,16 @@ impl Dock { new_position: DockPosition, cx: &mut ViewContext, ) { - dbg!("starting", &new_position); workspace.dock.position = new_position; // Tell the pane about the new anchor position workspace.dock.pane.update(cx, |pane, cx| { - dbg!("setting docked"); pane.set_docked(Some(new_position.anchor()), cx) }); if workspace.dock.position.is_visible() { - dbg!("dock is visible"); // Close the right sidebar if the dock is on the right side and the right sidebar is open if workspace.dock.position.anchor() == DockAnchor::Right { - dbg!("dock anchor is right"); if workspace.right_sidebar().read(cx).is_open() { - dbg!("Toggling right sidebar"); workspace.toggle_sidebar(SidebarSide::Right, cx); } } @@ -199,10 +194,8 @@ impl Dock { if pane.read(cx).items().next().is_none() { let item_to_add = (workspace.dock.default_item_factory)(workspace, cx); // Adding the item focuses the pane by default - dbg!("Adding item to dock"); Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx); } else { - dbg!("just focusing dock"); cx.focus(pane); } } else if let Some(last_active_center_pane) = workspace @@ -214,7 +207,6 @@ impl Dock { } cx.emit(crate::Event::DockAnchorChanged); workspace.serialize_workspace(cx); - dbg!("Serializing workspace after dock position changed"); cx.notify(); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d86e449ff2..9a827da8b7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -324,6 +324,9 @@ 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); + cx.on_window_should_close(|workspace, cx| { if let Some(task) = workspace.close(&Default::default(), cx) { task.detach_and_log_err(cx); @@ -613,7 +616,7 @@ fn schema_file_match(path: &Path) -> &Path { mod tests { use super::*; use assets::Assets; - use editor::{Autoscroll, DisplayPoint, Editor}; + use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use gpui::{ executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle, };