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