
This Pull Request introduces various changes to the editor's horizontal scrolling, mostly focused on vim mode's horizontal scroll motions (`z l`, `z h`, `z shift-l`, `z shift-h`). In order to make it easier to review, the logical changes have been split into different sections. ## Cursor Position Update Changes introduced on https://github.com/zed-industries/zed/pull/32558 added both `z l` and `z h` to vim mode but it only scrolled the editor's content, without changing the cursor position. This doesn't reflect the actual behavior of those motions in vim, so these two commits tackled that, ensuring that the cursor position is updated, only when the cursor is on the left or right edges of the editor: -ea3b866a76
-805f41a913
## Horizontal Autoscroll Fix After introducing the cursor position update to both `z l` and `z h` it was noted that there was a bug with using `z l`, followed by `0` and then `z l` again, as on the second use `z l` the cursor would not be updated. This would only happen on the first line in the editor, and it was concluded that it was because the `editor:📜:autoscroll::Editor.autoscroll_horizontally` method was directly updating the scroll manager's anchor offset, instead of using the `editor:📜:Editor.set_scroll_position_internal` method, like is being done by the vertical autoscroll (`editor:📜:autoscroll::Editor.autoscroll_vertically`). This wouldn't update the scroll manager's anchor, which would still think it was at `(0, 1)` so the cursor position would not be updated. The changes in [this commit](3957f02e18
) updated the horizontal autoscrolling method to also leverage `set_scroll_position_internal`. ## Visible Column Count & Page Width Scroll Amount The changes ind83652c3ae
add a `visible_column_count` field to `editor:📜:ScrollManager` struct, which allowed the introduction of the `ScrollAmount::PageWidth` enum. With these changes, two new actions are introduced, `vim::normal:📜:HalfPageRight` and `vim::normal:📜:HalfPageLeft` (in7f344304d5
), which move the editor half page to the right and half page to the left, as well as the cursor position, which have also been mapped to `z shift-l` and `z shift-h`, respectively. Closes #17219 Release Notes: - Improved `z l` and `z h` to actually move the cursor position, similar to vim's behavior - Added `z shift-l` and `z shift-h` to scroll half of the page width's to the right or to the left, respectively --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
354 lines
12 KiB
Rust
354 lines
12 KiB
Rust
use crate::{
|
|
DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
|
|
display_map::ToDisplayPoint,
|
|
};
|
|
use gpui::{Bounds, Context, Pixels, Window, px};
|
|
use language::Point;
|
|
use multi_buffer::Anchor;
|
|
use std::{cmp, f32};
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum Autoscroll {
|
|
Next,
|
|
Strategy(AutoscrollStrategy, Option<Anchor>),
|
|
}
|
|
|
|
impl Autoscroll {
|
|
/// scrolls the minimal amount to (try) and fit all cursors onscreen
|
|
pub fn fit() -> Self {
|
|
Self::Strategy(AutoscrollStrategy::Fit, None)
|
|
}
|
|
|
|
/// scrolls the minimal amount to fit the newest cursor
|
|
pub fn newest() -> Self {
|
|
Self::Strategy(AutoscrollStrategy::Newest, None)
|
|
}
|
|
|
|
/// scrolls so the newest cursor is vertically centered
|
|
pub fn center() -> Self {
|
|
Self::Strategy(AutoscrollStrategy::Center, None)
|
|
}
|
|
|
|
/// scrolls so the newest cursor is near the top
|
|
/// (offset by vertical_scroll_margin)
|
|
pub fn focused() -> Self {
|
|
Self::Strategy(AutoscrollStrategy::Focused, None)
|
|
}
|
|
|
|
/// Scrolls so that the newest cursor is roughly an n-th line from the top.
|
|
pub fn top_relative(n: usize) -> Self {
|
|
Self::Strategy(AutoscrollStrategy::TopRelative(n), None)
|
|
}
|
|
|
|
/// Scrolls so that the newest cursor is at the top.
|
|
pub fn top() -> Self {
|
|
Self::Strategy(AutoscrollStrategy::Top, None)
|
|
}
|
|
|
|
/// Scrolls so that the newest cursor is roughly an n-th line from the bottom.
|
|
pub fn bottom_relative(n: usize) -> Self {
|
|
Self::Strategy(AutoscrollStrategy::BottomRelative(n), None)
|
|
}
|
|
|
|
/// Scrolls so that the newest cursor is at the bottom.
|
|
pub fn bottom() -> Self {
|
|
Self::Strategy(AutoscrollStrategy::Bottom, None)
|
|
}
|
|
|
|
/// Applies a given auto-scroll strategy to a given anchor instead of a cursor.
|
|
/// E.G: Autoscroll::center().for_anchor(...) results in the anchor being at the center of the screen.
|
|
pub fn for_anchor(self, anchor: Anchor) -> Self {
|
|
match self {
|
|
Autoscroll::Next => self,
|
|
Autoscroll::Strategy(autoscroll_strategy, _) => {
|
|
Self::Strategy(autoscroll_strategy, Some(anchor))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Into<SelectionEffects> for Option<Autoscroll> {
|
|
fn into(self) -> SelectionEffects {
|
|
match self {
|
|
Some(autoscroll) => SelectionEffects::scroll(autoscroll),
|
|
None => SelectionEffects::no_scroll(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)]
|
|
pub enum AutoscrollStrategy {
|
|
Fit,
|
|
Newest,
|
|
#[default]
|
|
Center,
|
|
Focused,
|
|
Top,
|
|
Bottom,
|
|
TopRelative(usize),
|
|
BottomRelative(usize),
|
|
}
|
|
|
|
impl AutoscrollStrategy {
|
|
fn next(&self) -> Self {
|
|
match self {
|
|
AutoscrollStrategy::Center => AutoscrollStrategy::Top,
|
|
AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
|
|
_ => AutoscrollStrategy::Center,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Editor {
|
|
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
|
|
self.scroll_manager.autoscroll_request()
|
|
}
|
|
|
|
pub fn autoscroll_vertically(
|
|
&mut self,
|
|
bounds: Bounds<Pixels>,
|
|
line_height: Pixels,
|
|
max_scroll_top: f32,
|
|
window: &mut Window,
|
|
cx: &mut Context<Editor>,
|
|
) -> bool {
|
|
let viewport_height = bounds.size.height;
|
|
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 original_y = scroll_position.y;
|
|
if let Some(last_bounds) = self.expect_bounds_change.take() {
|
|
if scroll_position.y != 0. {
|
|
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
|
|
if scroll_position.y < 0. {
|
|
scroll_position.y = 0.;
|
|
}
|
|
}
|
|
}
|
|
if scroll_position.y > max_scroll_top {
|
|
scroll_position.y = max_scroll_top;
|
|
}
|
|
|
|
if original_y != scroll_position.y {
|
|
self.set_scroll_position(scroll_position, window, cx);
|
|
}
|
|
|
|
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
|
|
return false;
|
|
};
|
|
|
|
let mut target_top;
|
|
let mut target_bottom;
|
|
if let Some(first_highlighted_row) =
|
|
self.highlighted_display_row_for_autoscroll(&display_map)
|
|
{
|
|
target_top = first_highlighted_row.as_f32();
|
|
target_bottom = target_top + 1.;
|
|
} else {
|
|
let selections = self.selections.all::<Point>(cx);
|
|
|
|
target_top = selections
|
|
.first()
|
|
.unwrap()
|
|
.head()
|
|
.to_display_point(&display_map)
|
|
.row()
|
|
.as_f32();
|
|
target_bottom = selections
|
|
.last()
|
|
.unwrap()
|
|
.head()
|
|
.to_display_point(&display_map)
|
|
.row()
|
|
.next_row()
|
|
.as_f32();
|
|
|
|
let selections_fit = target_bottom - target_top <= visible_lines;
|
|
if matches!(
|
|
autoscroll,
|
|
Autoscroll::Strategy(AutoscrollStrategy::Newest, _)
|
|
) || (matches!(autoscroll, Autoscroll::Strategy(AutoscrollStrategy::Fit, _))
|
|
&& !selections_fit)
|
|
{
|
|
let newest_selection_top = selections
|
|
.iter()
|
|
.max_by_key(|s| s.id)
|
|
.unwrap()
|
|
.head()
|
|
.to_display_point(&display_map)
|
|
.row()
|
|
.as_f32();
|
|
target_top = newest_selection_top;
|
|
target_bottom = newest_selection_top + 1.;
|
|
}
|
|
}
|
|
|
|
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
|
0.
|
|
} else {
|
|
((visible_lines - (target_bottom - target_top)) / 2.0).floor()
|
|
};
|
|
|
|
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
|
|
&& target_top == last_autoscroll.1
|
|
&& target_bottom == last_autoscroll.2
|
|
{
|
|
last_autoscroll.3.next()
|
|
} else {
|
|
AutoscrollStrategy::default()
|
|
}
|
|
} else {
|
|
AutoscrollStrategy::default()
|
|
}
|
|
}
|
|
};
|
|
if let Autoscroll::Strategy(_, Some(anchor)) = autoscroll {
|
|
target_top = anchor.to_display_point(&display_map).row().as_f32();
|
|
target_bottom = target_top + 1.;
|
|
}
|
|
|
|
match strategy {
|
|
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
|
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
|
let target_top = (target_top - margin).max(0.0);
|
|
let target_bottom = target_bottom + margin;
|
|
let start_row = scroll_position.y;
|
|
let end_row = start_row + visible_lines;
|
|
|
|
let needs_scroll_up = target_top < start_row;
|
|
let needs_scroll_down = target_bottom >= end_row;
|
|
|
|
if needs_scroll_up && !needs_scroll_down {
|
|
scroll_position.y = target_top;
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
if !needs_scroll_up && needs_scroll_down {
|
|
scroll_position.y = target_bottom - visible_lines;
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
}
|
|
AutoscrollStrategy::Center => {
|
|
scroll_position.y = (target_top - margin).max(0.0);
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
AutoscrollStrategy::Focused => {
|
|
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
|
scroll_position.y = (target_top - margin).max(0.0);
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
AutoscrollStrategy::Top => {
|
|
scroll_position.y = (target_top).max(0.0);
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
AutoscrollStrategy::Bottom => {
|
|
scroll_position.y = (target_bottom - visible_lines).max(0.0);
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
AutoscrollStrategy::TopRelative(lines) => {
|
|
scroll_position.y = target_top - lines as f32;
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
AutoscrollStrategy::BottomRelative(lines) => {
|
|
scroll_position.y = target_bottom + lines as f32;
|
|
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
|
}
|
|
}
|
|
|
|
self.scroll_manager.last_autoscroll = Some((
|
|
self.scroll_manager.anchor.offset,
|
|
target_top,
|
|
target_bottom,
|
|
strategy,
|
|
));
|
|
|
|
true
|
|
}
|
|
|
|
pub(crate) fn autoscroll_horizontally(
|
|
&mut self,
|
|
start_row: DisplayRow,
|
|
viewport_width: Pixels,
|
|
scroll_width: Pixels,
|
|
em_advance: Pixels,
|
|
layouts: &[LineWithInvisibles],
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> bool {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let selections = self.selections.all::<Point>(cx);
|
|
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
|
|
|
let mut target_left;
|
|
let mut target_right;
|
|
|
|
if self
|
|
.highlighted_display_row_for_autoscroll(&display_map)
|
|
.is_none()
|
|
{
|
|
target_left = px(f32::INFINITY);
|
|
target_right = px(0.);
|
|
for selection in selections {
|
|
let head = selection.head().to_display_point(&display_map);
|
|
if head.row() >= start_row
|
|
&& head.row() < DisplayRow(start_row.0 + layouts.len() as u32)
|
|
{
|
|
let start_column = head.column();
|
|
let end_column = cmp::min(display_map.line_len(head.row()), head.column());
|
|
target_left = target_left.min(
|
|
layouts[head.row().minus(start_row) as usize]
|
|
.x_for_index(start_column as usize)
|
|
+ self.gutter_dimensions.margin,
|
|
);
|
|
target_right = target_right.max(
|
|
layouts[head.row().minus(start_row) as usize]
|
|
.x_for_index(end_column as usize)
|
|
+ em_advance,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
target_left = px(0.);
|
|
target_right = px(0.);
|
|
}
|
|
|
|
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 * em_advance;
|
|
let scroll_right = scroll_left + viewport_width;
|
|
|
|
if target_left < scroll_left {
|
|
scroll_position.x = target_left / em_advance;
|
|
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
|
true
|
|
} else if target_right > scroll_right {
|
|
scroll_position.x = (target_right - viewport_width) / em_advance;
|
|
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut Context<Self>) {
|
|
self.scroll_manager.autoscroll_request = Some((autoscroll, true));
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn request_autoscroll_remotely(
|
|
&mut self,
|
|
autoscroll: Autoscroll,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.scroll_manager.autoscroll_request = Some((autoscroll, false));
|
|
cx.notify();
|
|
}
|
|
}
|