mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Merge pull request #1944 from zed-industries/vim-page-movement
Add scroll commands to vim mode
This commit is contained in:
commit
b1e37378dc
@ -8,6 +8,22 @@
|
||||
"Namespace": "G"
|
||||
}
|
||||
],
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
@ -38,22 +54,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",
|
||||
@ -110,6 +110,12 @@
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"z": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Namespace": "Z"
|
||||
}
|
||||
],
|
||||
"i": [
|
||||
"vim::SwitchMode",
|
||||
"Insert"
|
||||
@ -147,6 +153,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 +218,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;
|
||||
@ -4123,10 +3714,13 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||
if context_menu.select_first(cx) {
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.context_menu
|
||||
.as_mut()
|
||||
.map(|menu| menu.select_first(cx))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
@ -4134,9 +3728,10 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let row_count = match self.visible_line_count {
|
||||
Some(row_count) => row_count as u32 - 1,
|
||||
None => return,
|
||||
let row_count = if let Some(row_count) = self.visible_line_count() {
|
||||
row_count as u32 - 1
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
@ -4158,32 +3753,6 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
|
||||
@ -4221,10 +3790,13 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||
if context_menu.select_last(cx) {
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.context_menu
|
||||
.as_mut()
|
||||
.map(|menu| menu.select_last(cx))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
@ -4232,9 +3804,10 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let row_count = match self.visible_line_count {
|
||||
Some(row_count) => row_count as u32 - 1,
|
||||
None => return,
|
||||
let row_count = if let Some(row_count) = self.visible_line_count() {
|
||||
row_count as u32 - 1
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
@ -4256,32 +3829,6 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
|
||||
@ -4602,18 +4149,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 +4169,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 +5469,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 +5877,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 +6073,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 +6085,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 +6097,6 @@ pub enum Event {
|
||||
SelectionsChanged { local: bool },
|
||||
ScrollPositionChanged { local: bool },
|
||||
Closed,
|
||||
IgnoredInput,
|
||||
}
|
||||
|
||||
pub struct EditorFocused(pub ViewHandle<Editor>);
|
||||
@ -6789,7 +6282,6 @@ impl View for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::IgnoredInput);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -6826,7 +6318,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,17 @@ 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_internal(
|
||||
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),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@ -132,13 +136,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 +165,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 +213,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 +287,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 +300,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(scroll_anchor, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([offset..offset])
|
||||
});
|
||||
|
348
crates/editor/src/scroll.rs
Normal file
348
crates/editor/src/scroll.rs
Normal file
@ -0,0 +1,348 @@
|
||||
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>) {
|
||||
self.set_scroll_anchor_internal(scroll_anchor, true, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_anchor_internal(
|
||||
&mut self,
|
||||
scroll_anchor: ScrollAnchor,
|
||||
local: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
hide_hover(self, cx);
|
||||
self.scroll_manager.set_anchor(scroll_anchor, local, 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
|
||||
}
|
||||
}
|
159
crates/editor/src/scroll/actions.rs
Normal file
159
crates/editor/src/scroll/actions.rs
Normal file
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
246
crates/editor/src/scroll/autoscroll.rs
Normal file
246
crates/editor/src/scroll/autoscroll.rs
Normal file
@ -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();
|
||||
}
|
||||
}
|
48
crates/editor/src/scroll/scroll_amount.rs
Normal file
48
crates/editor/src/scroll/scroll_amount.rs
Normal file
@ -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;
|
||||
@ -199,7 +200,6 @@ mod test {
|
||||
Test test
|
||||
ˇtest"})
|
||||
.await;
|
||||
println!("Marker");
|
||||
cx.assert(indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
|
@ -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",
|
||||
|
@ -51,8 +51,9 @@ impl<'a> VimTestContext<'a> {
|
||||
)
|
||||
});
|
||||
|
||||
// Setup search toolbars
|
||||
// Setup search toolbars and keypress hook
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
observe_keypresses(window_id, cx);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||
|
@ -81,6 +81,25 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
// Any keystrokes not mapped to vim should clear the active operator
|
||||
pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
|
||||
cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
|
||||
if let Some(handled_by) = handled_by {
|
||||
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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user