Detailed changes
@@ -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": {
@@ -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,
@@ -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<Axis>,
-}
-
#[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::<Editor>(cx);
workspace::register_followable_item::<Editor>(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<HighlightStyle>;
-#[derive(Clone, Copy)]
-pub struct OngoingScroll {
- last_timestamp: Instant,
- axis: Option<Axis>,
-}
-
-impl OngoingScroll {
- fn initial() -> OngoingScroll {
- OngoingScroll {
- last_timestamp: Instant::now() - SCROLL_EVENT_SEPARATION,
- axis: None,
- }
- }
-
- fn update(&mut self, axis: Option<Axis>) {
- self.last_timestamp = Instant::now();
- self.axis = axis;
- }
-
- pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
- 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<Self>,
buffer: ModelHandle<MultiBuffer>,
display_map: ModelHandle<DisplayMap>,
pub selections: SelectionsCollection,
+ pub scroll_manager: ScrollManager,
columnar_selection_tail: Option<Anchor>,
add_selections_state: Option<AddSelectionsState>,
select_next_state: Option<SelectNextState>,
@@ -554,10 +441,6 @@ pub struct Editor {
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
ime_transaction: Option<TransactionId>,
active_diagnostics: Option<ActiveDiagnosticGroup>,
- ongoing_scroll: OngoingScroll,
- scroll_position: Vector2F,
- scroll_top_anchor: Anchor,
- autoscroll_request: Option<(Autoscroll, bool)>,
soft_wrap_mode_override: Option<settings::SoftWrap>,
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
override_text_style: Option<Box<OverrideTextStyle>>,
@@ -565,10 +448,7 @@ pub struct Editor {
focused: bool,
blink_manager: ModelHandle<BlinkManager>,
show_local_selections: bool,
- show_scrollbars: bool,
- hide_scrollbar_task: Option<Task<()>>,
mode: EditorMode,
- vertical_scroll_margin: f32,
placeholder_text: Option<Arc<str>>,
highlighted_rows: Option<Range<u32>>,
#[allow(clippy::type_complexity)]
@@ -590,8 +470,6 @@ pub struct Editor {
leader_replica_id: Option<u16>,
hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
- visible_line_count: Option<f32>,
- last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
_subscriptions: Vec<Subscription>,
}
@@ -600,9 +478,8 @@ pub struct EditorSnapshot {
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
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>) {
- self.vertical_scroll_margin = margin_rows as f32;
- cx.notify();
- }
-
- pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
- self.set_scroll_position_internal(scroll_position, true, cx);
- }
-
- fn set_scroll_position_internal(
- &mut self,
- scroll_position: Vector2F,
- local: bool,
- cx: &mut ViewContext<Self>,
- ) {
- 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>,
- ) {
- 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>) {
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<Self>) -> 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<Self>,
- ) -> 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::<Point>(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::<Point>(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<Self>,
- ) -> bool {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let selections = self.selections.all::<Point>(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>) {
- 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>) {
self.hide_context_menu(cx);
@@ -4073,23 +3681,6 @@ impl Editor {
})
}
- pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) {
- 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<Self>) {
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<Self>) {
- 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<Self>) -> 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<Self>) {
- 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<Self>) {
@@ -4216,26 +3775,25 @@ impl Editor {
});
}
- pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext<Self>) {
+ pub fn move_page_down(
+ &mut self,
+ action: &MovePageDown,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) {
- 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<Self>) {
@@ -4602,18 +4136,19 @@ impl Editor {
fn push_to_nav_history(
&self,
- position: Anchor,
+ cursor_anchor: Anchor,
new_position: Option<Point>,
cx: &mut ViewContext<Self>,
) {
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>) {
- self.autoscroll_request = Some((autoscroll, true));
- cx.notify();
- }
-
- fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
- self.autoscroll_request = Some((autoscroll, false));
- cx.notify();
- }
-
pub fn transact(
&mut self,
cx: &mut ViewContext<Self>,
@@ -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<Self>) {
- if !self.show_scrollbars {
- self.show_scrollbars = true;
- cx.notify();
- }
-
- if cx.default_global::<ScrollbarAutoHide>().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<MultiBuffer>, cx: &mut ViewContext<Self>) {
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<Editor>);
@@ -6789,7 +6269,6 @@ impl View for Editor {
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
- cx.emit(Event::IgnoredInput);
return;
}
@@ -6826,7 +6305,6 @@ impl View for Editor {
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
- cx.emit(Event::IgnoredInput);
return;
}
@@ -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]);
@@ -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::<Settings>().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::<Vec<_>, _>(|(_, 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(
@@ -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<proto::view::Variant> {
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])
});
@@ -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<Axis>,
+}
+
+impl OngoingScroll {
+ fn new() -> Self {
+ Self {
+ last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
+ axis: None,
+ }
+ }
+
+ pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
+ 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<Task<()>>,
+ visible_line_count: Option<f32>,
+}
+
+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<Axis>) {
+ 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<Editor>,
+ ) {
+ 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<Editor>) {
+ 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<Editor>) {
+ if !self.show_scrollbars {
+ self.show_scrollbars = true;
+ cx.notify();
+ }
+
+ if cx.default_global::<ScrollbarAutoHide>().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>) {
+ self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
+ cx.notify();
+ }
+
+ pub fn visible_line_count(&self) -> Option<f32> {
+ 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>) {
+ 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<Self>,
+ ) {
+ 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<Self>) -> 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<Self>) {
+ hide_hover(self, cx);
+ self.scroll_manager.set_anchor(scroll_anchor, true, cx);
+ }
+
+ pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
+ 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
+ }
+}
@@ -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<Axis>,
+}
+
+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<Editor>) -> 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>) {
+ 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<Editor>) {
+ 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<Editor>,
+ ) {
+ 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<Editor>,
+ ) {
+ 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,
+ )
+ }
+}
@@ -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<Editor>,
+ ) -> 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::<Point>(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::<Point>(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<Self>,
+ ) -> bool {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all::<Point>(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>) {
+ self.scroll_manager.autoscroll_request = Some((autoscroll, true));
+ cx.notify();
+ }
+
+ pub(crate) fn request_autoscroll_remotely(
+ &mut self,
+ autoscroll: Autoscroll,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.scroll_manager.autoscroll_request = Some((autoscroll, false));
+ cx.notify();
+ }
+}
@@ -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<Editor>,
+ ) -> 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.,
+ }
+ }
+}
@@ -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();
@@ -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,
@@ -594,6 +594,9 @@ type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContex
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
+type KeystrokeCallback = Box<
+ dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
+>;
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
@@ -619,6 +622,7 @@ pub struct MutableAppContext {
observations: CallbackCollection<usize, ObservationCallback>,
window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
+ keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
action_dispatch_observations: Arc<Mutex<BTreeMap<usize, ActionObservationCallback>>>,
@@ -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<A, V, F>(&mut self, handler: F)
+ pub fn add_action<A, V, F, R>(&mut self, handler: F)
where
A: Action,
V: View,
- F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
+ F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
{
self.add_action_internal(handler, false)
}
@@ -781,11 +786,11 @@ impl MutableAppContext {
self.add_action_internal(handler, true)
}
- fn add_action_internal<A, V, F>(&mut self, mut handler: F, capture: bool)
+ fn add_action_internal<A, V, F, R>(&mut self, mut handler: F, capture: bool)
where
A: Action,
V: View,
- F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
+ F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
{
let handler = Box::new(
move |view: &mut dyn AnyView,
@@ -1255,6 +1260,27 @@ impl MutableAppContext {
}
}
+ pub fn observe_keystrokes<F>(&mut self, window_id: usize, callback: F) -> Subscription
+ where
+ F: 'static
+ + FnMut(
+ &Keystroke,
+ &MatchResult,
+ Option<&Box<dyn Action>>,
+ &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<Box<dyn Action>>,
+ 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<Box<dyn Action>>,
+ 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<Box<dyn Action>>,
+ 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<F>(&mut self, mut callback: F) -> Subscription
+ where
+ F: 'static
+ + FnMut(
+ &mut T,
+ &Keystroke,
+ Option<&Box<dyn Action>>,
+ &MatchResult,
+ &mut ViewContext<T>,
+ ) -> 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<Weak<Mapping<usize, WindowFullscreenCallback>>>,
},
+ KeystrokeObservation {
+ id: usize,
+ window_id: usize,
+ observations: Option<Weak<Mapping<usize, KeystrokeCallback>>>,
+ },
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,
@@ -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 {
@@ -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::{
@@ -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::{
@@ -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::{
@@ -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,
@@ -216,6 +216,8 @@ pub fn unzip_option<T, U>(option: Option<(T, U)>) -> (Option<T>, Option<U>) {
}
}
+/// 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) => {
@@ -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::<usize>(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::<usize>(cx).is_empty();
+ editor_local_selections_changed(newest_empty, cx);
}
}
}));
@@ -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;
@@ -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<Workspace>) {
});
}
+fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
+ 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;
@@ -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;
@@ -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) {
@@ -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",
@@ -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<usize, WeakViewHandle<Editor>>,
@@ -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;
@@ -175,21 +175,16 @@ impl Dock {
new_position: DockPosition,
cx: &mut ViewContext<Workspace>,
) {
- 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();
}
@@ -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,
};