editor: Add sticky scroll (#42242)
Closes #5344 https://github.com/user-attachments/assets/37ec58b0-7cf6-4eea-9b34-dccf03d3526b Release Notes: - Added a setting to stick scopes to the top of the editor --------- Co-authored-by: KyleBarton <kjb@initialcapacity.io> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
co-authored by
KyleBarton
Conrad Irwin
parent
d896af2f15
commit
aa6270e658
@@ -605,6 +605,10 @@
|
||||
// to both the horizontal and vertical delta values while scrolling. Fast scrolling
|
||||
// happens when a user holds the alt or option key while scrolling.
|
||||
"fast_scroll_sensitivity": 4.0,
|
||||
"sticky_scroll": {
|
||||
// Whether to stick scopes to the top of the editor.
|
||||
"enabled": false
|
||||
},
|
||||
"relative_line_numbers": "disabled",
|
||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||
"search_wrap": true,
|
||||
|
||||
@@ -117,8 +117,8 @@ use language::{
|
||||
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
|
||||
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
|
||||
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
|
||||
IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal,
|
||||
TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
IndentSize, Language, OffsetRangeExt, OutlineItem, Point, Runnable, RunnableRange, Selection,
|
||||
SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
language_settings::{
|
||||
self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
|
||||
language_settings,
|
||||
@@ -1183,6 +1183,7 @@ pub struct Editor {
|
||||
hide_mouse_mode: HideMouseMode,
|
||||
pub change_list: ChangeList,
|
||||
inline_value_cache: InlineValueCache,
|
||||
|
||||
selection_drag_state: SelectionDragState,
|
||||
colors: Option<LspColorData>,
|
||||
post_scroll_update: Task<()>,
|
||||
@@ -1764,6 +1765,51 @@ impl Editor {
|
||||
Editor::new_internal(mode, buffer, project, None, window, cx)
|
||||
}
|
||||
|
||||
pub fn sticky_headers(&self, cx: &App) -> Option<Vec<OutlineItem<Anchor>>> {
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
let multi_buffer_visible_start = self
|
||||
.scroll_manager
|
||||
.anchor()
|
||||
.anchor
|
||||
.to_point(&multi_buffer_snapshot);
|
||||
let max_row = multi_buffer_snapshot.max_point().row;
|
||||
|
||||
let start_row = (multi_buffer_visible_start.row).min(max_row);
|
||||
let end_row = (multi_buffer_visible_start.row + 10).min(max_row);
|
||||
|
||||
if let Some((excerpt_id, buffer_id, buffer)) = multi_buffer.read(cx).as_singleton() {
|
||||
let outline_items = buffer
|
||||
.outline_items_containing(
|
||||
Point::new(start_row, 0)..Point::new(end_row, 0),
|
||||
true,
|
||||
self.style().map(|style| style.syntax.as_ref()),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|outline_item| OutlineItem {
|
||||
depth: outline_item.depth,
|
||||
range: Anchor::range_in_buffer(*excerpt_id, buffer_id, outline_item.range),
|
||||
source_range_for_text: Anchor::range_in_buffer(
|
||||
*excerpt_id,
|
||||
buffer_id,
|
||||
outline_item.source_range_for_text,
|
||||
),
|
||||
text: outline_item.text,
|
||||
highlight_ranges: outline_item.highlight_ranges,
|
||||
name_ranges: outline_item.name_ranges,
|
||||
body_range: outline_item
|
||||
.body_range
|
||||
.map(|range| Anchor::range_in_buffer(*excerpt_id, buffer_id, range)),
|
||||
annotation_range: outline_item
|
||||
.annotation_range
|
||||
.map(|range| Anchor::range_in_buffer(*excerpt_id, buffer_id, range)),
|
||||
});
|
||||
return Some(outline_items.collect());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn new_internal(
|
||||
mode: EditorMode,
|
||||
multi_buffer: Entity<MultiBuffer>,
|
||||
|
||||
@@ -33,6 +33,7 @@ pub struct EditorSettings {
|
||||
pub horizontal_scroll_margin: f32,
|
||||
pub scroll_sensitivity: f32,
|
||||
pub fast_scroll_sensitivity: f32,
|
||||
pub sticky_scroll: StickyScroll,
|
||||
pub relative_line_numbers: RelativeLineNumbers,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
pub use_smartcase_search: bool,
|
||||
@@ -65,6 +66,11 @@ pub struct Jupyter {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StickyScroll {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Toolbar {
|
||||
pub breadcrumbs: bool,
|
||||
@@ -185,6 +191,7 @@ impl Settings for EditorSettings {
|
||||
let toolbar = editor.toolbar.unwrap();
|
||||
let search = editor.search.unwrap();
|
||||
let drag_and_drop_selection = editor.drag_and_drop_selection.unwrap();
|
||||
let sticky_scroll = editor.sticky_scroll.unwrap();
|
||||
Self {
|
||||
cursor_blink: editor.cursor_blink.unwrap(),
|
||||
cursor_shape: editor.cursor_shape.map(Into::into),
|
||||
@@ -235,6 +242,9 @@ impl Settings for EditorSettings {
|
||||
horizontal_scroll_margin: editor.horizontal_scroll_margin.unwrap(),
|
||||
scroll_sensitivity: editor.scroll_sensitivity.unwrap(),
|
||||
fast_scroll_sensitivity: editor.fast_scroll_sensitivity.unwrap(),
|
||||
sticky_scroll: StickyScroll {
|
||||
enabled: sticky_scroll.enabled.unwrap(),
|
||||
},
|
||||
relative_line_numbers: editor.relative_line_numbers.unwrap(),
|
||||
seed_search_query_from_cursor: editor.seed_search_query_from_cursor.unwrap(),
|
||||
use_smartcase_search: editor.use_smartcase_search.unwrap(),
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
JoinLines,
|
||||
code_context_menus::CodeContextMenu,
|
||||
edit_prediction_tests::FakeEditPredictionProvider,
|
||||
element::StickyHeader,
|
||||
linked_editing_ranges::LinkedEditingRanges,
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
test::{
|
||||
@@ -27003,6 +27004,215 @@ async fn test_end_of_editor_context(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_sticky_scroll(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let buffer = indoc! {"
|
||||
ˇfn foo() {
|
||||
let abc = 123;
|
||||
}
|
||||
struct Bar;
|
||||
impl Bar {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
fn baz() {
|
||||
}
|
||||
"};
|
||||
cx.set_state(&buffer);
|
||||
|
||||
cx.update_editor(|e, _, cx| {
|
||||
e.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(rust_lang()), cx);
|
||||
})
|
||||
});
|
||||
|
||||
let mut sticky_headers = |offset: ScrollOffset| {
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
|
||||
EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
|
||||
.into_iter()
|
||||
.map(
|
||||
|StickyHeader {
|
||||
start_point,
|
||||
offset,
|
||||
..
|
||||
}| { (start_point, offset) },
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
let fn_foo = Point { row: 0, column: 0 };
|
||||
let impl_bar = Point { row: 4, column: 0 };
|
||||
let fn_new = Point { row: 5, column: 4 };
|
||||
|
||||
assert_eq!(sticky_headers(0.0), vec![]);
|
||||
assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
|
||||
assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
|
||||
assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
|
||||
assert_eq!(sticky_headers(2.0), vec![]);
|
||||
assert_eq!(sticky_headers(2.5), vec![]);
|
||||
assert_eq!(sticky_headers(3.0), vec![]);
|
||||
assert_eq!(sticky_headers(3.5), vec![]);
|
||||
assert_eq!(sticky_headers(4.0), vec![]);
|
||||
assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
|
||||
assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
|
||||
assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
|
||||
assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
|
||||
assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
|
||||
assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
|
||||
assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
|
||||
assert_eq!(sticky_headers(8.0), vec![]);
|
||||
assert_eq!(sticky_headers(8.5), vec![]);
|
||||
assert_eq!(sticky_headers(9.0), vec![]);
|
||||
assert_eq!(sticky_headers(9.5), vec![]);
|
||||
assert_eq!(sticky_headers(10.0), vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
|
||||
enabled: Some(true),
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let line_height = cx.editor(|editor, window, _cx| {
|
||||
editor
|
||||
.style()
|
||||
.unwrap()
|
||||
.text
|
||||
.line_height_in_pixels(window.rem_size())
|
||||
});
|
||||
|
||||
let buffer = indoc! {"
|
||||
ˇfn foo() {
|
||||
let abc = 123;
|
||||
}
|
||||
struct Bar;
|
||||
impl Bar {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
fn baz() {
|
||||
}
|
||||
"};
|
||||
cx.set_state(&buffer);
|
||||
|
||||
cx.update_editor(|e, _, cx| {
|
||||
e.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(rust_lang()), cx);
|
||||
})
|
||||
});
|
||||
|
||||
let fn_foo = || empty_range(0, 0);
|
||||
let impl_bar = || empty_range(4, 0);
|
||||
let fn_new = || empty_range(5, 4);
|
||||
|
||||
let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.scroll(
|
||||
gpui::Point {
|
||||
x: 0.,
|
||||
y: scroll_offset,
|
||||
},
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.simulate_click(
|
||||
gpui::Point {
|
||||
x: px(0.),
|
||||
y: click_offset as f32 * line_height,
|
||||
},
|
||||
Modifiers::none(),
|
||||
);
|
||||
cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
scroll_and_click(
|
||||
4.5, // impl Bar is halfway off the screen
|
||||
0.0 // click top of screen
|
||||
),
|
||||
// scrolled to impl Bar
|
||||
(gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scroll_and_click(
|
||||
4.5, // impl Bar is halfway off the screen
|
||||
0.25 // click middle of impl Bar
|
||||
),
|
||||
// scrolled to impl Bar
|
||||
(gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scroll_and_click(
|
||||
4.5, // impl Bar is halfway off the screen
|
||||
1.5 // click below impl Bar (e.g. fn new())
|
||||
),
|
||||
// scrolled to fn new() - this is below the impl Bar header which has persisted
|
||||
(gpui::Point { x: 0., y: 4. }, vec![fn_new()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scroll_and_click(
|
||||
5.5, // fn new is halfway underneath impl Bar
|
||||
0.75 // click on the overlap of impl Bar and fn new()
|
||||
),
|
||||
(gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scroll_and_click(
|
||||
5.5, // fn new is halfway underneath impl Bar
|
||||
1.25 // click on the visible part of fn new()
|
||||
),
|
||||
(gpui::Point { x: 0., y: 4. }, vec![fn_new()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scroll_and_click(
|
||||
1.5, // fn foo is halfway off the screen
|
||||
0.0 // click top of screen
|
||||
),
|
||||
(gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scroll_and_click(
|
||||
1.5, // fn foo is halfway off the screen
|
||||
0.75 // click visible part of let abc...
|
||||
)
|
||||
.0,
|
||||
// no change in scroll
|
||||
// we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
|
||||
(gpui::Point { x: 0., y: 1.5 })
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_prev_reference(cx: &mut TestAppContext) {
|
||||
const CYCLE_POSITIONS: &[&'static str] = &[
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::{
|
||||
HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp,
|
||||
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
|
||||
OpenExcerptsSplit, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
|
||||
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SizingBehavior, SoftWrap,
|
||||
StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
|
||||
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SelectionEffects,
|
||||
SizingBehavior, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
|
||||
@@ -29,7 +29,7 @@ use crate::{
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition},
|
||||
scroll::{
|
||||
ActiveScrollbarState, ScrollOffset, ScrollPixelOffset, ScrollbarThumbState,
|
||||
ActiveScrollbarState, Autoscroll, ScrollOffset, ScrollPixelOffset, ScrollbarThumbState,
|
||||
scroll_amount::ScrollAmount,
|
||||
},
|
||||
};
|
||||
@@ -4555,6 +4555,138 @@ impl EditorElement {
|
||||
header
|
||||
}
|
||||
|
||||
fn layout_sticky_headers(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
editor_width: Pixels,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
text_hitbox: &Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<StickyHeaders> {
|
||||
let show_line_numbers = snapshot
|
||||
.show_line_numbers
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
|
||||
|
||||
let rows = Self::sticky_headers(self.editor.read(cx), snapshot, cx);
|
||||
|
||||
let mut lines = Vec::<StickyHeaderLine>::new();
|
||||
|
||||
for StickyHeader {
|
||||
item,
|
||||
sticky_row,
|
||||
start_point,
|
||||
offset,
|
||||
} in rows.into_iter().rev()
|
||||
{
|
||||
let line = layout_line(
|
||||
sticky_row,
|
||||
snapshot,
|
||||
&self.style,
|
||||
editor_width,
|
||||
is_row_soft_wrapped,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let line_number = show_line_numbers.then(|| {
|
||||
let number = (start_point.row + 1).to_string();
|
||||
let color = cx.theme().colors().editor_line_number;
|
||||
self.shape_line_number(SharedString::from(number), color, window)
|
||||
});
|
||||
|
||||
lines.push(StickyHeaderLine::new(
|
||||
sticky_row,
|
||||
line_height * offset as f32,
|
||||
line,
|
||||
line_number,
|
||||
item.range.start,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
content_origin,
|
||||
gutter_hitbox,
|
||||
text_hitbox,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
|
||||
lines.reverse();
|
||||
if lines.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(StickyHeaders {
|
||||
lines,
|
||||
gutter_background: cx.theme().colors().editor_gutter_background,
|
||||
content_background: self.style.background,
|
||||
gutter_right_padding: gutter_dimensions.right_padding,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn sticky_headers(
|
||||
editor: &Editor,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<StickyHeader> {
|
||||
let scroll_top = snapshot.scroll_position().y;
|
||||
|
||||
let mut end_rows = Vec::<DisplayRow>::new();
|
||||
let mut rows = Vec::<StickyHeader>::new();
|
||||
|
||||
let items = editor.sticky_headers(cx).unwrap_or_default();
|
||||
|
||||
for item in items {
|
||||
let start_point = item.range.start.to_point(snapshot.buffer_snapshot());
|
||||
let end_point = item.range.end.to_point(snapshot.buffer_snapshot());
|
||||
|
||||
let sticky_row = snapshot
|
||||
.display_snapshot
|
||||
.point_to_display_point(start_point, Bias::Left)
|
||||
.row();
|
||||
let end_row = snapshot
|
||||
.display_snapshot
|
||||
.point_to_display_point(end_point, Bias::Left)
|
||||
.row();
|
||||
let max_sticky_row = end_row.previous_row();
|
||||
if max_sticky_row <= sticky_row {
|
||||
continue;
|
||||
}
|
||||
|
||||
while end_rows
|
||||
.last()
|
||||
.is_some_and(|&last_end| last_end < sticky_row)
|
||||
{
|
||||
end_rows.pop();
|
||||
}
|
||||
let depth = end_rows.len();
|
||||
let adjusted_scroll_top = scroll_top + depth as f64;
|
||||
|
||||
if sticky_row.as_f64() >= adjusted_scroll_top || end_row.as_f64() <= adjusted_scroll_top
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let max_scroll_offset = max_sticky_row.as_f64() - scroll_top;
|
||||
let offset = (depth as f64).min(max_scroll_offset);
|
||||
|
||||
end_rows.push(end_row);
|
||||
rows.push(StickyHeader {
|
||||
item,
|
||||
sticky_row,
|
||||
start_point,
|
||||
offset,
|
||||
});
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
fn layout_cursor_popovers(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -6407,6 +6539,89 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_sticky_headers(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(mut sticky_headers) = layout.sticky_headers.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if sticky_headers.lines.is_empty() {
|
||||
layout.sticky_headers = Some(sticky_headers);
|
||||
return;
|
||||
}
|
||||
|
||||
let whitespace_setting = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer
|
||||
.read(cx)
|
||||
.language_settings(cx)
|
||||
.show_whitespaces;
|
||||
sticky_headers.paint(layout, whitespace_setting, window, cx);
|
||||
|
||||
let sticky_header_hitboxes: Vec<Hitbox> = sticky_headers
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| line.hitbox.clone())
|
||||
.collect();
|
||||
let hovered_hitbox = sticky_header_hitboxes
|
||||
.iter()
|
||||
.find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
|
||||
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, _cx| {
|
||||
if !phase.bubble() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_hover = sticky_header_hitboxes
|
||||
.iter()
|
||||
.find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
|
||||
if hovered_hitbox != current_hover {
|
||||
window.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
for (line_index, line) in sticky_headers.lines.iter().enumerate() {
|
||||
let editor = self.editor.clone();
|
||||
let hitbox = line.hitbox.clone();
|
||||
let target_anchor = line.target_anchor;
|
||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if !phase.bubble() {
|
||||
return;
|
||||
}
|
||||
|
||||
if event.button == MouseButton::Left && hitbox.is_hovered(window) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::top_relative(line_index)),
|
||||
window,
|
||||
cx,
|
||||
|selections| selections.select_ranges([target_anchor..target_anchor]),
|
||||
);
|
||||
cx.stop_propagation();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let text_bounds = layout.position_map.text_hitbox.bounds;
|
||||
let border_top = text_bounds.top()
|
||||
+ sticky_headers.lines.last().unwrap().offset
|
||||
+ layout.position_map.line_height;
|
||||
let separator_height = px(1.);
|
||||
let border_bounds = Bounds::from_corners(
|
||||
point(layout.gutter_hitbox.bounds.left(), border_top),
|
||||
point(text_bounds.right(), border_top + separator_height),
|
||||
);
|
||||
window.paint_quad(fill(border_bounds, cx.theme().colors().border_variant));
|
||||
|
||||
layout.sticky_headers = Some(sticky_headers);
|
||||
}
|
||||
|
||||
fn paint_lines_background(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
@@ -8107,6 +8322,27 @@ impl LineWithInvisibles {
|
||||
cx: &mut App,
|
||||
) {
|
||||
let line_y = f32::from(line_height) * Pixels::from(row.as_f64() - scroll_position.y);
|
||||
self.prepaint_with_custom_offset(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
content_origin,
|
||||
line_y,
|
||||
line_elements,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn prepaint_with_custom_offset(
|
||||
&mut self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
line_y: Pixels,
|
||||
line_elements: &mut SmallVec<[AnyElement; 1]>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let mut fragment_origin =
|
||||
content_origin + gpui::point(Pixels::from(-scroll_pixel_position.x), line_y);
|
||||
for fragment in &mut self.fragments {
|
||||
@@ -8141,9 +8377,31 @@ impl LineWithInvisibles {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
|
||||
self.draw_with_custom_offset(
|
||||
layout,
|
||||
row,
|
||||
content_origin,
|
||||
layout.position_map.line_height
|
||||
* (row.as_f64() - layout.position_map.scroll_position.y) as f32,
|
||||
whitespace_setting,
|
||||
selection_ranges,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_with_custom_offset(
|
||||
&self,
|
||||
layout: &EditorLayout,
|
||||
row: DisplayRow,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
line_y: Pixels,
|
||||
whitespace_setting: ShowWhitespaceSetting,
|
||||
selection_ranges: &[Range<DisplayPoint>],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
let mut fragment_origin = content_origin
|
||||
+ gpui::point(
|
||||
Pixels::from(-layout.position_map.scroll_pixel_position.x),
|
||||
@@ -8582,6 +8840,7 @@ impl Element for EditorElement {
|
||||
};
|
||||
|
||||
let is_minimap = self.editor.read(cx).mode.is_minimap();
|
||||
let is_singleton = self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
|
||||
|
||||
if !is_minimap {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
@@ -9228,6 +9487,26 @@ impl Element for EditorElement {
|
||||
scroll_position.x * f64::from(em_advance),
|
||||
scroll_position.y * f64::from(line_height),
|
||||
);
|
||||
let sticky_headers = if !is_minimap
|
||||
&& is_singleton
|
||||
&& EditorSettings::get_global(cx).sticky_scroll.enabled
|
||||
{
|
||||
self.layout_sticky_headers(
|
||||
&snapshot,
|
||||
editor_width,
|
||||
is_row_soft_wrapped,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
content_origin,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&text_hitbox,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let indent_guides = self.layout_indent_guides(
|
||||
content_origin,
|
||||
text_hitbox.origin,
|
||||
@@ -9697,6 +9976,7 @@ impl Element for EditorElement {
|
||||
tab_invisible,
|
||||
space_invisible,
|
||||
sticky_buffer_header,
|
||||
sticky_headers,
|
||||
expand_toggles,
|
||||
}
|
||||
})
|
||||
@@ -9767,6 +10047,7 @@ impl Element for EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
self.paint_sticky_headers(layout, window, cx);
|
||||
self.paint_minimap(layout, window, cx);
|
||||
self.paint_scrollbars(layout, window, cx);
|
||||
self.paint_edit_prediction_popover(layout, window, cx);
|
||||
@@ -9875,15 +10156,180 @@ pub struct EditorLayout {
|
||||
tab_invisible: ShapedLine,
|
||||
space_invisible: ShapedLine,
|
||||
sticky_buffer_header: Option<AnyElement>,
|
||||
sticky_headers: Option<StickyHeaders>,
|
||||
document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
|
||||
}
|
||||
|
||||
struct StickyHeaders {
|
||||
lines: Vec<StickyHeaderLine>,
|
||||
gutter_background: Hsla,
|
||||
content_background: Hsla,
|
||||
gutter_right_padding: Pixels,
|
||||
}
|
||||
|
||||
struct StickyHeaderLine {
|
||||
row: DisplayRow,
|
||||
offset: Pixels,
|
||||
line: LineWithInvisibles,
|
||||
line_number: Option<ShapedLine>,
|
||||
elements: SmallVec<[AnyElement; 1]>,
|
||||
available_text_width: Pixels,
|
||||
target_anchor: Anchor,
|
||||
hitbox: Hitbox,
|
||||
}
|
||||
|
||||
impl EditorLayout {
|
||||
fn line_end_overshoot(&self) -> Pixels {
|
||||
0.15 * self.position_map.line_height
|
||||
}
|
||||
}
|
||||
|
||||
impl StickyHeaders {
|
||||
fn paint(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
whitespace_setting: ShowWhitespaceSetting,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
|
||||
for line in self.lines.iter_mut().rev() {
|
||||
window.paint_layer(
|
||||
Bounds::new(
|
||||
layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
|
||||
size(line.hitbox.size.width, line_height),
|
||||
),
|
||||
|window| {
|
||||
let gutter_bounds = Bounds::new(
|
||||
layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
|
||||
size(layout.gutter_hitbox.size.width, line_height),
|
||||
);
|
||||
window.paint_quad(fill(gutter_bounds, self.gutter_background));
|
||||
|
||||
let text_bounds = Bounds::new(
|
||||
layout.position_map.text_hitbox.origin + point(Pixels::ZERO, line.offset),
|
||||
size(line.available_text_width, line_height),
|
||||
);
|
||||
window.paint_quad(fill(text_bounds, self.content_background));
|
||||
|
||||
if line.hitbox.is_hovered(window) {
|
||||
let hover_overlay = cx.theme().colors().panel_overlay_hover;
|
||||
window.paint_quad(fill(gutter_bounds, hover_overlay));
|
||||
window.paint_quad(fill(text_bounds, hover_overlay));
|
||||
}
|
||||
|
||||
line.paint(
|
||||
layout,
|
||||
self.gutter_right_padding,
|
||||
line.available_text_width,
|
||||
layout.content_origin,
|
||||
line_height,
|
||||
whitespace_setting,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &line.hitbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StickyHeaderLine {
|
||||
fn new(
|
||||
row: DisplayRow,
|
||||
offset: Pixels,
|
||||
mut line: LineWithInvisibles,
|
||||
line_number: Option<ShapedLine>,
|
||||
target_anchor: Anchor,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
text_hitbox: &Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let mut elements = SmallVec::<[AnyElement; 1]>::new();
|
||||
line.prepaint_with_custom_offset(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
content_origin,
|
||||
offset,
|
||||
&mut elements,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let hitbox_bounds = Bounds::new(
|
||||
gutter_hitbox.origin + point(Pixels::ZERO, offset),
|
||||
size(text_hitbox.right() - gutter_hitbox.left(), line_height),
|
||||
);
|
||||
let available_text_width =
|
||||
(hitbox_bounds.size.width - gutter_hitbox.size.width).max(Pixels::ZERO);
|
||||
|
||||
Self {
|
||||
row,
|
||||
offset,
|
||||
line,
|
||||
line_number,
|
||||
elements,
|
||||
available_text_width,
|
||||
target_anchor,
|
||||
hitbox: window.insert_hitbox(hitbox_bounds, HitboxBehavior::BlockMouseExceptScroll),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
layout: &EditorLayout,
|
||||
gutter_right_padding: Pixels,
|
||||
available_text_width: Pixels,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
whitespace_setting: ShowWhitespaceSetting,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: Bounds::new(
|
||||
layout.position_map.text_hitbox.bounds.origin
|
||||
+ point(Pixels::ZERO, self.offset),
|
||||
size(available_text_width, line_height),
|
||||
),
|
||||
}),
|
||||
|window| {
|
||||
self.line.draw_with_custom_offset(
|
||||
layout,
|
||||
self.row,
|
||||
content_origin,
|
||||
self.offset,
|
||||
whitespace_setting,
|
||||
&[],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
for element in &mut self.elements {
|
||||
element.paint(window, cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(line_number) = &self.line_number {
|
||||
let gutter_origin = layout.gutter_hitbox.origin + point(Pixels::ZERO, self.offset);
|
||||
let gutter_width = layout.gutter_hitbox.size.width;
|
||||
let origin = point(
|
||||
gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
|
||||
gutter_origin.y,
|
||||
);
|
||||
line_number.paint(origin, line_height, window, cx).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LineNumberSegment {
|
||||
shaped_line: ShapedLine,
|
||||
@@ -10730,6 +11176,13 @@ impl HighlightedRange {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StickyHeader {
|
||||
pub item: language::OutlineItem<Anchor>,
|
||||
pub sticky_row: DisplayRow,
|
||||
pub start_point: Point,
|
||||
pub offset: ScrollOffset,
|
||||
}
|
||||
|
||||
enum CursorPopoverType {
|
||||
CodeContextMenu,
|
||||
EditPrediction,
|
||||
|
||||
@@ -2613,6 +2613,9 @@ pub fn rust_lang() -> Arc<Language> {
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_queries(LanguageQueries {
|
||||
outline: Some(Cow::from(include_str!(
|
||||
"../../languages/src/rust/outline.scm"
|
||||
))),
|
||||
indents: Some(Cow::from(
|
||||
r#"
|
||||
[
|
||||
|
||||
@@ -96,6 +96,10 @@ pub struct EditorSettingsContent {
|
||||
/// Default: 4.0
|
||||
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
||||
pub fast_scroll_sensitivity: Option<f32>,
|
||||
/// Settings for sticking scopes to the top of the editor.
|
||||
///
|
||||
/// Default: sticky scroll is disabled
|
||||
pub sticky_scroll: Option<StickyScrollContent>,
|
||||
/// Whether the line numbers on editors gutter are relative or not.
|
||||
/// When "enabled" shows relative number of buffer lines, when "wrapped" shows
|
||||
/// relative number of display lines.
|
||||
@@ -312,6 +316,16 @@ pub struct ScrollbarContent {
|
||||
pub axes: Option<ScrollbarAxesContent>,
|
||||
}
|
||||
|
||||
/// Sticky scroll related settings
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
|
||||
pub struct StickyScrollContent {
|
||||
/// Whether sticky scroll is enabled.
|
||||
///
|
||||
/// Default: false
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Minimap related settings
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
|
||||
|
||||
@@ -255,6 +255,7 @@ impl VsCodeSettings {
|
||||
excerpt_context_lines: None,
|
||||
expand_excerpt_lines: None,
|
||||
fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"),
|
||||
sticky_scroll: self.sticky_scroll_content(),
|
||||
go_to_definition_fallback: None,
|
||||
gutter: self.gutter_content(),
|
||||
hide_mouse: None,
|
||||
@@ -303,6 +304,12 @@ impl VsCodeSettings {
|
||||
}
|
||||
}
|
||||
|
||||
fn sticky_scroll_content(&self) -> Option<StickyScrollContent> {
|
||||
skip_default(StickyScrollContent {
|
||||
enabled: self.read_bool("editor.stickyScroll.enabled"),
|
||||
})
|
||||
}
|
||||
|
||||
fn gutter_content(&self) -> Option<GutterContent> {
|
||||
skip_default(GutterContent {
|
||||
line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
|
||||
|
||||
@@ -1352,6 +1352,21 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Sticky Scroll",
|
||||
description: "Whether to stick scopes to the top of the editor",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("sticky_scroll.enabled"),
|
||||
pick: |settings_content| {
|
||||
settings_content.editor.sticky_scroll.as_ref().and_then(|sticky_scroll| sticky_scroll.enabled.as_ref())
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content.editor.sticky_scroll.get_or_insert_default().enabled = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Signature Help"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Auto Signature Help",
|
||||
|
||||
@@ -218,6 +218,10 @@ TBD: Centered layout related settings
|
||||
"active_line_width": 1, // Width of active guide in pixels [1-10]
|
||||
"coloring": "fixed", // disabled, fixed, indent_aware
|
||||
"background_coloring": "disabled" // disabled, indent_aware
|
||||
},
|
||||
|
||||
"sticky_scroll": {
|
||||
"enabled": false // Whether to stick scopes to the top of the editor. Disabled by default.
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user