Merge pull request #1944 from zed-industries/vim-page-movement

Add scroll commands to vim mode
This commit is contained in:
Kay Simmons 2022-12-08 14:58:19 -08:00 committed by GitHub
commit b1e37378dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1251 additions and 676 deletions

View File

@ -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": {

View File

@ -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,

View File

@ -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;
}

View File

@ -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]);

View File

@ -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(

View File

@ -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
View 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
}
}

View 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,
)
}
}

View 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();
}
}

View 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.,
}
}
}

View File

@ -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();

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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::{

View File

@ -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::{

View File

@ -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::{

View File

@ -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,

View File

@ -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) => {

View File

@ -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);
}
}
}));

View File

@ -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;

View File

@ -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;

View File

@ -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
ˇ

View File

@ -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) {

View File

@ -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",

View File

@ -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);

View File

@ -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>>,

View File

@ -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;

View File

@ -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();
}

View File

@ -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,
};