editor: Improve scroll to support position at the edge of the editor on click. (#1672)
But if we use `Up`, `Down` to move the cursor, it still will keep 3 lines. https://github.com/user-attachments/assets/4310a4cc-3829-4190-9be9-e7159c688884
This commit is contained in:
parent
8144085077
commit
dae5fcd8aa
5 changed files with 78 additions and 39 deletions
|
|
@ -1,13 +1,13 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, App, Context, HighlightStyle, Hitbox, MouseDownEvent, Task, UnderlineStyle, Window,
|
App, Context, HighlightStyle, Hitbox, MouseDownEvent, Task, UnderlineStyle, Window, px,
|
||||||
};
|
};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
use std::{ops::Range, rc::Rc};
|
use std::{ops::Range, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
input::{element::TextElement, GoToDefinition, InputState, RopeExt},
|
|
||||||
ActiveTheme,
|
ActiveTheme,
|
||||||
|
input::{GoToDefinition, InputState, RopeExt, element::TextElement},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Definition provider
|
/// Definition provider
|
||||||
|
|
@ -172,7 +172,7 @@ impl InputState {
|
||||||
let start = self.text.position_to_offset(&target_range.start);
|
let start = self.text.position_to_offset(&target_range.start);
|
||||||
let end = self.text.position_to_offset(&target_range.end);
|
let end = self.text.position_to_offset(&target_range.end);
|
||||||
|
|
||||||
self.move_to(start, cx);
|
self.move_to(start, None, cx);
|
||||||
self.select_to(end, cx);
|
self.select_to(end, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@ use crate::input::{
|
||||||
MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _,
|
MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum MoveDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
impl InputState {
|
impl InputState {
|
||||||
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
|
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
|
||||||
pub(super) fn update_preferred_column(&mut self) {
|
pub(super) fn update_preferred_column(&mut self) {
|
||||||
|
|
@ -33,10 +39,15 @@ impl InputState {
|
||||||
/// The offset is the UTF-8 offset.
|
/// The offset is the UTF-8 offset.
|
||||||
///
|
///
|
||||||
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
||||||
pub(crate) fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
pub(crate) fn move_to(
|
||||||
|
&mut self,
|
||||||
|
offset: usize,
|
||||||
|
direction: Option<MoveDirection>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let offset = offset.clamp(0, self.text.len());
|
let offset = offset.clamp(0, self.text.len());
|
||||||
self.selected_range = (offset..offset).into();
|
self.selected_range = (offset..offset).into();
|
||||||
self.scroll_to(offset, cx);
|
self.scroll_to(offset, direction, cx);
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
self.update_preferred_column();
|
self.update_preferred_column();
|
||||||
self.hide_context_menu(cx);
|
self.hide_context_menu(cx);
|
||||||
|
|
@ -93,7 +104,12 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
self.move_to(new_offset, cx);
|
let direction = if move_lines < 0 {
|
||||||
|
MoveDirection::Up
|
||||||
|
} else {
|
||||||
|
MoveDirection::Down
|
||||||
|
};
|
||||||
|
self.move_to(new_offset, Some(direction), cx);
|
||||||
// Set back the preferred_column
|
// Set back the preferred_column
|
||||||
self.preferred_column = was_preferred_column;
|
self.preferred_column = was_preferred_column;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
@ -102,18 +118,18 @@ impl InputState {
|
||||||
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
if self.selected_range.is_empty() {
|
if self.selected_range.is_empty() {
|
||||||
self.move_to(self.previous_boundary(self.cursor()), cx);
|
self.move_to(self.previous_boundary(self.cursor()), None, cx);
|
||||||
} else {
|
} else {
|
||||||
self.move_to(self.selected_range.start, cx)
|
self.move_to(self.selected_range.start, None, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
if self.selected_range.is_empty() {
|
if self.selected_range.is_empty() {
|
||||||
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
self.move_to(self.next_boundary(self.selected_range.end), None, cx);
|
||||||
} else {
|
} else {
|
||||||
self.move_to(self.selected_range.end, cx)
|
self.move_to(self.selected_range.end, None, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,6 +145,7 @@ impl InputState {
|
||||||
if !self.selected_range.is_empty() {
|
if !self.selected_range.is_empty() {
|
||||||
self.move_to(
|
self.move_to(
|
||||||
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
|
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
|
||||||
|
Some(MoveDirection::Up),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +165,7 @@ impl InputState {
|
||||||
if !self.selected_range.is_empty() {
|
if !self.selected_range.is_empty() {
|
||||||
self.move_to(
|
self.move_to(
|
||||||
self.next_boundary(self.selected_range.end.saturating_sub(1)),
|
self.next_boundary(self.selected_range.end.saturating_sub(1)),
|
||||||
|
Some(MoveDirection::Down),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -190,13 +208,13 @@ impl InputState {
|
||||||
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
let offset = self.start_of_line();
|
let offset = self.start_of_line();
|
||||||
self.move_to(offset, cx);
|
self.move_to(offset, Some(MoveDirection::Up), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
let offset = self.end_of_line();
|
let offset = self.end_of_line();
|
||||||
self.move_to(offset, cx);
|
self.move_to(offset, Some(MoveDirection::Down), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn move_to_start(
|
pub(super) fn move_to_start(
|
||||||
|
|
@ -205,11 +223,11 @@ impl InputState {
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.move_to(0, cx);
|
self.move_to(0, None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.move_to(self.text.len(), cx);
|
self.move_to(self.text.len(), None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn move_to_previous_word(
|
pub(super) fn move_to_previous_word(
|
||||||
|
|
@ -219,7 +237,7 @@ impl InputState {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let offset = self.previous_start_of_word();
|
let offset = self.previous_start_of_word();
|
||||||
self.move_to(offset, cx);
|
self.move_to(offset, None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn move_to_next_word(
|
pub(super) fn move_to_next_word(
|
||||||
|
|
@ -229,6 +247,6 @@ impl InputState {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let offset = self.next_end_of_word();
|
let offset = self.next_end_of_word();
|
||||||
self.move_to(offset, cx);
|
self.move_to(offset, None, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, div, prelude::FluentBuilder as _, px, App, AppContext as _, Context,
|
App, AppContext as _, Context, Corner, DismissEvent, Entity, IntoElement, MouseDownEvent,
|
||||||
Corner, DismissEvent, Entity, IntoElement, MouseDownEvent, ParentElement as _, Pixels, Point,
|
ParentElement as _, Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred,
|
||||||
Render, Styled, Subscription, Window,
|
div, prelude::FluentBuilder as _, px,
|
||||||
};
|
};
|
||||||
use rust_i18n::t;
|
use rust_i18n::t;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
input::{self, popovers::ContextMenu, InputState},
|
|
||||||
menu::PopupMenu,
|
|
||||||
ActiveTheme as _,
|
ActiveTheme as _,
|
||||||
|
input::{self, InputState, popovers::ContextMenu},
|
||||||
|
menu::PopupMenu,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Context menu for mouse right clicks.
|
/// Context menu for mouse right clicks.
|
||||||
|
|
@ -31,7 +31,7 @@ impl InputState {
|
||||||
) {
|
) {
|
||||||
// Show Mouse context menu
|
// Show Mouse context menu
|
||||||
if !self.selected_range.contains(offset) {
|
if !self.selected_range.contains(offset) {
|
||||||
self.move_to(offset, cx);
|
self.move_to(offset, None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.context_menu = Some(ContextMenu::MouseContext(self.mouse_context_menu.clone()));
|
self.context_menu = Some(ContextMenu::MouseContext(self.mouse_context_menu.clone()));
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,23 @@ use rust_i18n::t;
|
||||||
use std::{ops::Range, rc::Rc};
|
use std::{ops::Range, rc::Rc};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, canvas, div, prelude::FluentBuilder as _, App, AppContext as _, Context, Empty,
|
App, AppContext as _, Context, Empty, Entity, FocusHandle, Focusable, Half,
|
||||||
Entity, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
|
InteractiveElement as _, IntoElement, KeyBinding, ParentElement as _, Pixels, Render, Styled,
|
||||||
ParentElement as _, Pixels, Render, Styled, Subscription, Window,
|
Subscription, Window, actions, canvas, div, prelude::FluentBuilder as _,
|
||||||
};
|
};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
ActiveTheme, Disableable, IconName, Selectable, Sizable,
|
||||||
actions::SelectUp,
|
actions::SelectUp,
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
h_flex,
|
h_flex,
|
||||||
input::{Enter, Escape, IndentInline, Input, InputEvent, InputState, RopeExt as _, Search},
|
input::{
|
||||||
|
Enter, Escape, IndentInline, Input, InputEvent, InputState, RopeExt as _, Search,
|
||||||
|
movement::MoveDirection,
|
||||||
|
},
|
||||||
label::Label,
|
label::Label,
|
||||||
v_flex, ActiveTheme, Disableable, IconName, Selectable, Sizable,
|
v_flex,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTEXT: &'static str = "SearchPanel";
|
const CONTEXT: &'static str = "SearchPanel";
|
||||||
|
|
@ -309,7 +313,7 @@ impl SearchPanel {
|
||||||
fn prev(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
fn prev(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(range) = self.matcher.next_back() {
|
if let Some(range) = self.matcher.next_back() {
|
||||||
self.editor.update(cx, |state, cx| {
|
self.editor.update(cx, |state, cx| {
|
||||||
state.scroll_to(range.start, cx);
|
state.scroll_to(range.start, Some(MoveDirection::Up), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,7 +321,7 @@ impl SearchPanel {
|
||||||
fn next(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
fn next(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(range) = self.matcher.next() {
|
if let Some(range) = self.matcher.next() {
|
||||||
self.editor.update(cx, |state, cx| {
|
self.editor.update(cx, |state, cx| {
|
||||||
state.scroll_to(range.end, cx);
|
state.scroll_to(range.end, Some(MoveDirection::Down), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -346,7 +350,7 @@ impl SearchPanel {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
text_state.update(cx, |state, cx| {
|
text_state.update(cx, |state, cx| {
|
||||||
let range_utf16 = state.range_to_utf16(&range);
|
let range_utf16 = state.range_to_utf16(&range);
|
||||||
state.scroll_to(next_range.end, cx);
|
state.scroll_to(next_range.end, Some(MoveDirection::Down), cx);
|
||||||
state.replace_text_in_range_silent(
|
state.replace_text_in_range_silent(
|
||||||
Some(range_utf16),
|
Some(range_utf16),
|
||||||
new_text.as_str(),
|
new_text.as_str(),
|
||||||
|
|
@ -383,7 +387,7 @@ impl SearchPanel {
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
state.scroll_to(0, cx);
|
state.scroll_to(0, Some(MoveDirection::Down), cx);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use super::{
|
||||||
mask_pattern::MaskPattern, mode::InputMode, number_input, text_wrapper::TextWrapper,
|
mask_pattern::MaskPattern, mode::InputMode, number_input, text_wrapper::TextWrapper,
|
||||||
};
|
};
|
||||||
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
|
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||||
|
use crate::input::movement::MoveDirection;
|
||||||
use crate::input::{
|
use crate::input::{
|
||||||
HoverDefinition, Lsp, Position,
|
HoverDefinition, Lsp, Position,
|
||||||
element::RIGHT_MARGIN,
|
element::RIGHT_MARGIN,
|
||||||
|
|
@ -825,7 +826,7 @@ impl InputState {
|
||||||
let position: Position = position.into();
|
let position: Position = position.into();
|
||||||
let offset = self.text.position_to_offset(&position);
|
let offset = self.text.position_to_offset(&position);
|
||||||
|
|
||||||
self.move_to(offset, cx);
|
self.move_to(offset, None, cx);
|
||||||
self.update_preferred_column();
|
self.update_preferred_column();
|
||||||
self.focus(window, cx);
|
self.focus(window, cx);
|
||||||
}
|
}
|
||||||
|
|
@ -1178,7 +1179,7 @@ impl InputState {
|
||||||
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.replace_text("", window, cx);
|
self.replace_text("", window, cx);
|
||||||
self.selected_range = (0..0).into();
|
self.selected_range = (0..0).into();
|
||||||
self.scroll_to(0, cx);
|
self.scroll_to(0, None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
|
@ -1233,7 +1234,7 @@ impl InputState {
|
||||||
if event.modifiers.shift {
|
if event.modifiers.shift {
|
||||||
self.select_to(offset, cx);
|
self.select_to(offset, cx);
|
||||||
} else {
|
} else {
|
||||||
self.move_to(offset, cx)
|
self.move_to(offset, None, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1331,7 +1332,15 @@ impl InputState {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn scroll_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
/// Scroll to make the given offset visible.
|
||||||
|
///
|
||||||
|
/// If `direction` is Some, will keep edges at the same side.
|
||||||
|
pub(crate) fn scroll_to(
|
||||||
|
&mut self,
|
||||||
|
offset: usize,
|
||||||
|
direction: Option<MoveDirection>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let Some(last_layout) = self.last_layout.as_ref() else {
|
let Some(last_layout) = self.last_layout.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -1340,6 +1349,7 @@ impl InputState {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut scroll_offset = self.scroll_handle.offset();
|
let mut scroll_offset = self.scroll_handle.offset();
|
||||||
|
let was_offset = scroll_offset;
|
||||||
let line_height = last_layout.line_height;
|
let line_height = last_layout.line_height;
|
||||||
|
|
||||||
let point = self.text.offset_to_point(offset);
|
let point = self.text.offset_to_point(offset);
|
||||||
|
|
@ -1375,19 +1385,26 @@ impl InputState {
|
||||||
|
|
||||||
// Check if row_offset_y is out of the viewport
|
// Check if row_offset_y is out of the viewport
|
||||||
// If row offset is not in the viewport, scroll to make it visible
|
// If row offset is not in the viewport, scroll to make it visible
|
||||||
let edge_height = if self.mode.is_code_editor() {
|
let edge_height = if direction.is_some() && self.mode.is_code_editor() {
|
||||||
3 * line_height
|
3 * line_height
|
||||||
} else {
|
} else {
|
||||||
line_height
|
line_height
|
||||||
};
|
};
|
||||||
if row_offset_y - edge_height < -scroll_offset.y {
|
if row_offset_y - edge_height + line_height < -scroll_offset.y {
|
||||||
// Scroll up
|
// Scroll up
|
||||||
scroll_offset.y = -row_offset_y + edge_height;
|
scroll_offset.y = -row_offset_y + edge_height - line_height;
|
||||||
} else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
|
} else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
|
||||||
// Scroll down
|
// Scroll down
|
||||||
scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
|
scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid necessary scroll, when it was already in the correct position.
|
||||||
|
if direction == Some(MoveDirection::Up) {
|
||||||
|
scroll_offset.y = scroll_offset.y.max(was_offset.y);
|
||||||
|
} else if direction == Some(MoveDirection::Down) {
|
||||||
|
scroll_offset.y = scroll_offset.y.min(was_offset.y);
|
||||||
|
}
|
||||||
|
|
||||||
scroll_offset.x = scroll_offset.x.min(px(0.));
|
scroll_offset.x = scroll_offset.x.min(px(0.));
|
||||||
scroll_offset.y = scroll_offset.y.min(px(0.));
|
scroll_offset.y = scroll_offset.y.min(px(0.));
|
||||||
self.deferred_scroll_offset = Some(scroll_offset);
|
self.deferred_scroll_offset = Some(scroll_offset);
|
||||||
|
|
@ -1431,7 +1448,7 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.replace_text_in_range_silent(None, &new_text, window, cx);
|
self.replace_text_in_range_silent(None, &new_text, window, cx);
|
||||||
self.scroll_to(self.cursor(), cx);
|
self.scroll_to(self.cursor(), None, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue