mod gutter_button;use std::ops::Range;use std::sync::Arc;use std::sync::atomic::{AtomicBool, Ordering}; pub use gutter_button::{AddAsContextButton, CommentButton, RevertHunkButton};use parking_lot::Mutex;use pathfinder_color::ColorU;use pathfinder_geometry::rect::RectF;use pathfinder_geometry::vector::{Vector2F, vec2f};use warp_core::features::FeatureFlag;use warp_core::ui::appearance::Appearance;use warp_core::ui::theme::Fill;use warp_core::ui::theme::color::internal_colors;use warp_editor::editor::EditorView;use warp_editor::render::element::lens_element::RichTextElementLens;use warp_editor::render::element::{RenderableBlock, RichTextElement, VerticalExpansionBehavior};use warp_editor::render::model::{ BlockLocation, ExpansionType, LineCount, RenderState, gutter_expansion_button_types,};use warpui::elements::new_scrollable::{NewScrollableElement, ScrollableAxis};use warpui::elements::{ Align, Axis, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, Empty, F32Ext, Flex, Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Point, Radius, ScrollData, Stack, Text, ZIndex,};use warpui::event::DispatchedEvent;use warpui::fonts::FamilyId;use warpui::ui_components::components::UiComponent;use warpui::units::{IntoPixels, Pixels};use warpui::{ AfterLayoutContext, AppContext, ClipBounds, Element, Event, EventContext, LayoutContext, ModelHandle, PaintContext, SingletonEntity, SizeConstraint,}; use super::diff::{DiffHunkDisplay, DiffStatus};use super::model::DiffNavigationState;use crate::code::editor::element::gutter_button::GutterButton;use crate::code::editor::line::EditorLineLocation;use crate::code::editor::view::{CodeEditorViewAction, SavedComment};use crate::settings::CodeEditorLineNumberMode;use crate::view_components::action_button::{ActionButtonTheme, SecondaryTheme}; pub const GUTTER_WIDTH: f32 = 94.;const VERTICAL_DIFF_HUNK_INDICATOR_WIDTH: f32 = 3.;const VERTICAL_DIFF_HUNK_INDICATOR_HOVERED_WIDTH: f32 = 8.; fn highlight_element(appearance: &Appearance) -> Box<dyn Element> { let border_color = appearance.theme().accent(); Container::new(Empty::new().finish()) .with_border(Border::all(2.).with_border_fill(border_color)) .finish()} #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum ChangeType { Add, Remove,} #[derive(Debug, Clone, Copy)]pub enum GutterElementType { DiffHunk { hunk: Option<DiffHunkDisplay>, change_type: ChangeType, }, HiddenSection { expansion_type: ExpansionType, },} /// The inner editor element wrapped by the EditorWrapper. Currently we support a full scrollable/// editor or a lens element into a section of the buffer.pub enum InnerEditor<V: EditorView> { FullEditor(RichTextElement<V>), Lens(RichTextElementLens<V>),} impl<V: EditorView> InnerEditor<V> { fn blocks(&self) -> Option<&[Box<dyn RenderableBlock>]> { match self { InnerEditor::FullEditor(element) => element.blocks(), InnerEditor::Lens(element) => element.blocks(), } } fn model(&self) -> &ModelHandle<RenderState> { match self { InnerEditor::FullEditor(element) => &element.model, InnerEditor::Lens(element) => &element.model, } }} impl<V: EditorView> Element for InnerEditor<V> { fn layout( &mut self, constraint: SizeConstraint, ctx: &mut LayoutContext, app: &AppContext, ) -> Vector2F { match self { InnerEditor::FullEditor(element) => element.layout(constraint, ctx, app), InnerEditor::Lens(element) => element.layout(constraint, ctx, app), } } fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) { match self { InnerEditor::FullEditor(element) => element.after_layout(ctx, app), InnerEditor::Lens(element) => element.after_layout(ctx, app), } } fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) { match self { InnerEditor::FullEditor(element) => element.paint(origin, ctx, app), InnerEditor::Lens(element) => element.paint(origin, ctx, app), }; } fn size(&self) -> Option<Vector2F> { None } fn origin(&self) -> Option<Point> { None } fn dispatch_event( &mut self, event: &DispatchedEvent, ctx: &mut EventContext, app: &AppContext, ) -> bool { match self { InnerEditor::FullEditor(element) => element.dispatch_event(event, ctx, app), InnerEditor::Lens(element) => element.dispatch_event(event, ctx, app), } }} struct GutterElement { element: Box<dyn Element>, offset: Pixels, height: f32, hovered: bool, line: EditorLineLocation, element_type: GutterElementType, /// Optional background fill for removed-line (temporary) blocks. overlay: Option<Fill>,} impl GutterElement { /// Checks if the given position falls within this gutter element's bounds. /// Returns a GutterRange if the position is contained, None otherwise. /// When `check_y_axis_only` is true, a position is considered within the element /// if it's anywhere within the `EditorWrapper`'s full line width. fn contains_position( &self, position: Vector2F, wrapper_origin: Vector2F, wrapper_size: Vector2F, check_y_axis_only: bool, ) -> Option<GutterRange> { let gutter_origin = wrapper_origin + vec2f(0., self.offset.as_f32()); match self.element_type { GutterElementType::HiddenSection { expansion_type } => { let does_contain = if check_y_axis_only { // We can count the position if it's within the editor wrapper's whole line width let line_origin = Vector2F::new(wrapper_origin.x(), gutter_origin.y()); let line_size = Vector2F::new(wrapper_size.x(), self.height); RectF::new(line_origin, line_size).contains_point(position) } else { // Hidden sections use the full gutter width let size = vec2f(GUTTER_WIDTH, self.height); RectF::new(gutter_origin, size).contains_point(position) }; if does_contain { Some(GutterRange::HiddenSection { line: self.line.clone(), expansion_type, }) } else { None } } GutterElementType::DiffHunk { .. } => { let does_contain = if check_y_axis_only { // We can count the position if it's within the editor wrapper's whole line width let line_origin = Vector2F::new(wrapper_origin.x(), gutter_origin.y()); let line_size = Vector2F::new(wrapper_size.x(), self.height); RectF::new(line_origin, line_size).contains_point(position) } else { // For diff hunks, check if position is in the sliver or the full gutter // Get the sliver size (always use expanded=true for hit testing) let sliver_size = self.diff_hunk_size(true)?; let sliver_rect = RectF::new(gutter_origin, sliver_size); sliver_rect.contains_point(position) }; if does_contain { // Position is in the sliver Some(GutterRange::DiffHunk { line: self.line.clone(), in_sliver: true, }) } else { // Check if position is in the full gutter width (but not in sliver) let full_gutter_rect = RectF::new(gutter_origin, vec2f(GUTTER_WIDTH, self.height)); if full_gutter_rect.contains_point(position) { Some(GutterRange::DiffHunk { line: self.line.clone(), in_sliver: false, }) } else { None } } } } } /// The size of the diff hunk element in gutter (if exists). fn diff_hunk_size(&self, gutter_element_is_hovered: bool) -> Option<Vector2F> { match self.element_type { GutterElementType::HiddenSection { .. } => { // Hidden section elements are always horizontal. Some(vec2f(GUTTER_WIDTH, self.height)) } GutterElementType::DiffHunk { hunk: ref diff, .. } => { let vertical_indicator_width = if gutter_element_is_hovered { VERTICAL_DIFF_HUNK_INDICATOR_HOVERED_WIDTH } else { VERTICAL_DIFF_HUNK_INDICATOR_WIDTH }; diff.as_ref() .map(|_| vec2f(vertical_indicator_width, self.height)) } } } // The color of the diff hunk indicator element in gutter (if it exists). fn diff_indicator_color(&self, diff_hunks_are_expanded: bool) -> Option<ColorU> { if let GutterElementType::DiffHunk { hunk: Some(diff_hunk), change_type, } = self.element_type { match &diff_hunk { DiffHunkDisplay::Add(color) | DiffHunkDisplay::Remove(color) => Some(*color), DiffHunkDisplay::Replacement { collapsed_color: change_color, add_color, remove_color, } => { if diff_hunks_are_expanded { match change_type { ChangeType::Add => Some(*add_color), ChangeType::Remove => Some(*remove_color), } } else { Some(*change_color) } } } } else { None } }} /// States that need to live in between frames.#[derive(Default)]pub struct EditorWrapperState { /// The line range of the hovered diff hunk. hovered_diff_hunk: Mutex<Option<EditorLineLocation>>, /// Whether there is an active click. in_click: AtomicBool, /// Mouse state handle for the plus button. add_as_context_mouse_state: MouseStateHandle, /// Mouse state handle for the revert button. revert_mouse_state: MouseStateHandle, /// Mouse state handle for the comment button. comment_mouse_state: MouseStateHandle, /// Tracks the line range where the add context button was last clicked, /// so we don't show the button again until a different range is hovered. last_clicked_range: Mutex<Option<Range<LineCount>>>,} impl EditorWrapperState { /// Record that a range has been clicked (add context button was used) pub fn record_clicked_range(&self, range: Range<LineCount>) { *self.last_clicked_range.lock() = Some(range); } /// Clear the clicked range (when hovering over a new range) pub fn clear_clicked_range(&self) { *self.last_clicked_range.lock() = None; } /// Check if a range has been clicked pub fn is_range_clicked(&self, range: &Range<LineCount>) -> bool { self.last_clicked_range.lock().as_ref() == Some(range) }} pub type EditorWrapperStateHandle = Arc<EditorWrapperState>; pub enum GutterRange { DiffHunk { line: EditorLineLocation, // Whether the position is in the sliver (the colored part) of the diff hunk element. in_sliver: bool, }, HiddenSection { line: EditorLineLocation, expansion_type: ExpansionType, },} impl GutterRange { pub fn line(&self) -> &EditorLineLocation { match self { GutterRange::DiffHunk { line, .. } => line, GutterRange::HiddenSection { line, .. } => line, } }} #[derive(Debug, Clone, Copy)]pub enum GutterHoverTarget { // The entire line covered by the gutter is considered the hover target. Line, // Only the gutter element itself is considered the hover target. GutterElement,} /// The caller defined handler for a click event.type EditorWrapperClickHandler = Box<dyn FnMut(GutterRange, &mut EventContext)>; /// UI Config for rendering line number.pub struct LineNumberConfig { pub font_family: FamilyId, pub font_size: f32, pub text_color: ColorU, pub highlight_text_color: ColorU, pub starting_line_number: Option<usize>, pub mode: CodeEditorLineNumberMode, pub active_line_number: Option<LineCount>, pub active_cursor_is_visible: bool,}impl LineNumberConfig { pub fn absolute_line_number(&self, line_count: LineCount) -> usize { line_count.as_usize() + self.starting_line_number.unwrap_or(1) } pub fn display_line_number(&self, line_count: LineCount) -> usize { if self.mode == CodeEditorLineNumberMode::Relative && let Some(active_line_number) = self.active_line_number && active_line_number != line_count { return active_line_number .as_usize() .abs_diff(line_count.as_usize()); } self.absolute_line_number(line_count) }} struct CommentBox { line_highlight_element: Box<dyn Element>, line: EditorLineLocation,} pub struct EditorWrapper<V: EditorView> { editor: InnerEditor<V>, element_size: Option<Vector2F>, element_origin: Option<Point>, /// Whether the editor should expand vertically to fill the available space. vertical_expansion_behavior: VerticalExpansionBehavior, /// If there is no [`LineNumberConfig`], the entire left gutter won't be rendered. line_number_config: Option<LineNumberConfig>, gutter_elements: Option<Vec<GutterElement>>, diff_status: DiffStatus, state_handle: EditorWrapperStateHandle, click_handler: EditorWrapperClickHandler, /// The current state of diff navigation in the editor diff_navigation_state: DiffNavigationState, /// The line range of the focused diff hunk (if any) focused_diff_line_range: Option<Range<LineCount>>, should_handle_scroll_wheel: bool, /// This helps us handle events properly on stacks. A stack will always /// put its children on higher z-indexes than its origin, so a hit test using the standard /// `z_index` method would always result in the event being covered (by the children of the /// stack). Instead, we track the upper-bound of z-indexes _contained by_ the child element. /// Then we use that upper bound to do the hit testing, which means a parent will always get /// events from its children, regardless of whether they are stacks or not. child_max_z_index: Option<ZIndex>, /// Display state of the "add as agent context" button shown next to diff hunks. add_hunk_as_context_button: Option<AddAsContextButton>, /// Display state of the "revert" button shown next to diff hunks. revert_hunk_button: Option<RevertHunkButton>, /// Display state of the "comment" button shown next to diff hunks. comment_button: Option<CommentButton>, // Todo: kc combine all comment related fields into a struct. comment_box: Option<CommentBox>, /// Lines with saved comments attached. These lines always have an /// indicator in the gutter element. saved_comments: Vec<SavedComment>, gutter_element_hover_target: GutterHoverTarget, expand_diff_indicator_width_on_hover: bool, comment_save_position_id: String, find_references_save_position_id: String, /// The line where find references card is anchored (if active). find_references_anchor: Option<EditorLineLocation>,} impl<V: EditorView> EditorWrapper<V> { fn paint_removed_line_overlays( &self, origin: Vector2F, wrapper_size: Vector2F, ctx: &mut PaintContext, ) { // Removed lines: group consecutive Remove-type gutter elements by line_range and // draw one full-width rect per group. let Some(gutter_elements) = &self.gutter_elements else { return; }; struct Group { start_y: f32, end_y: f32, line_range: Range<LineCount>, overlay: Fill, } let mut group: Option<Group> = None; let mut flush = |group: &mut Option<Group>| { let Some(group) = group.take() else { return; }; ctx.scene .draw_rect_without_hit_recording(RectF::new( origin + vec2f(0., group.start_y), vec2f(wrapper_size.x(), group.end_y - group.start_y), )) .with_background(group.overlay); }; for element in gutter_elements.iter() { let is_remove = matches!( element.element_type, GutterElementType::DiffHunk { change_type: ChangeType::Remove, .. } ); if !is_remove { flush(&mut group); continue; } let Some(overlay) = element.overlay else { flush(&mut group); continue; }; let current_range = element.line.line_range().clone(); let start_y = element.offset.as_f32(); let end_y = start_y + element.height; match &mut group { Some(group) if group.line_range == current_range && group.overlay == overlay => { group.end_y = end_y; } _ => { flush(&mut group); group = Some(Group { start_y, end_y, line_range: current_range, overlay, }); } } } flush(&mut group); } #[allow(clippy::too_many_arguments)] pub fn new(Lines 1–500 of 1694. Load more
mod gutter_button; use std::ops::Range; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; pub use gutter_button::{AddAsContextButton, CommentButton, RevertHunkButton}; use parking_lot::Mutex; use pathfinder_color::ColorU; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::{Vector2F, vec2f}; use warp_core::features::FeatureFlag; use warp_core::ui::appearance::Appearance; use warp_core::ui::theme::Fill; use warp_core::ui::theme::color::internal_colors; use warp_editor::editor::EditorView; use warp_editor::render::element::lens_element::RichTextElementLens; use warp_editor::render::element::{RenderableBlock, RichTextElement, VerticalExpansionBehavior}; use warp_editor::render::model::{ BlockLocation, ExpansionType, LineCount, RenderState, gutter_expansion_button_types, }; use warpui::elements::new_scrollable::{NewScrollableElement, ScrollableAxis}; use warpui::elements::{ Align, Axis, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, Empty, F32Ext, Flex, Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Point, Radius, ScrollData, Stack, Text, ZIndex, }; use warpui::event::DispatchedEvent; use warpui::fonts::FamilyId; use warpui::ui_components::components::UiComponent; use warpui::units::{IntoPixels, Pixels}; use warpui::{ AfterLayoutContext, AppContext, ClipBounds, Element, Event, EventContext, LayoutContext, ModelHandle, PaintContext, SingletonEntity, SizeConstraint, }; use super::diff::{DiffHunkDisplay, DiffStatus}; use super::model::DiffNavigationState; use crate::code::editor::element::gutter_button::GutterButton; use crate::code::editor::line::EditorLineLocation; use crate::code::editor::view::{CodeEditorViewAction, SavedComment}; use crate::settings::CodeEditorLineNumberMode; use crate::view_components::action_button::{ActionButtonTheme, SecondaryTheme}; pub const GUTTER_WIDTH: f32 = 94.; const VERTICAL_DIFF_HUNK_INDICATOR_WIDTH: f32 = 3.; const VERTICAL_DIFF_HUNK_INDICATOR_HOVERED_WIDTH: f32 = 8.; fn highlight_element(appearance: &Appearance) -> Box<dyn Element> { let border_color = appearance.theme().accent(); Container::new(Empty::new().finish()) .with_border(Border::all(2.).with_border_fill(border_color)) .finish() } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ChangeType { Add, Remove, } #[derive(Debug, Clone, Copy)] pub enum GutterElementType { DiffHunk { hunk: Option<DiffHunkDisplay>, change_type: ChangeType, }, HiddenSection { expansion_type: ExpansionType, }, } /// The inner editor element wrapped by the EditorWrapper. Currently we support a full scrollable /// editor or a lens element into a section of the buffer. pub enum InnerEditor<V: EditorView> { FullEditor(RichTextElement<V>), Lens(RichTextElementLens<V>), } impl<V: EditorView> InnerEditor<V> { fn blocks(&self) -> Option<&[Box<dyn RenderableBlock>]> { match self { InnerEditor::FullEditor(element) => element.blocks(), InnerEditor::Lens(element) => element.blocks(), } } fn model(&self) -> &ModelHandle<RenderState> { match self { InnerEditor::FullEditor(element) => &element.model, InnerEditor::Lens(element) => &element.model, } } } impl<V: EditorView> Element for InnerEditor<V> { fn layout( &mut self, constraint: SizeConstraint, ctx: &mut LayoutContext, app: &AppContext, ) -> Vector2F { match self { InnerEditor::FullEditor(element) => element.layout(constraint, ctx, app), InnerEditor::Lens(element) => element.layout(constraint, ctx, app), } } fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) { match self { InnerEditor::FullEditor(element) => element.after_layout(ctx, app), InnerEditor::Lens(element) => element.after_layout(ctx, app), } } fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) { match self { InnerEditor::FullEditor(element) => element.paint(origin, ctx, app), InnerEditor::Lens(element) => element.paint(origin, ctx, app), }; } fn size(&self) -> Option<Vector2F> { None } fn origin(&self) -> Option<Point> { None } fn dispatch_event( &mut self, event: &DispatchedEvent, ctx: &mut EventContext, app: &AppContext, ) -> bool { match self { InnerEditor::FullEditor(element) => element.dispatch_event(event, ctx, app), InnerEditor::Lens(element) => element.dispatch_event(event, ctx, app), } } } struct GutterElement { element: Box<dyn Element>, offset: Pixels, height: f32, hovered: bool, line: EditorLineLocation, element_type: GutterElementType, /// Optional background fill for removed-line (temporary) blocks. overlay: Option<Fill>, } impl GutterElement { /// Checks if the given position falls within this gutter element's bounds. /// Returns a GutterRange if the position is contained, None otherwise. /// When `check_y_axis_only` is true, a position is considered within the element /// if it's anywhere within the `EditorWrapper`'s full line width. fn contains_position( &self, position: Vector2F, wrapper_origin: Vector2F, wrapper_size: Vector2F, check_y_axis_only: bool, ) -> Option<GutterRange> { let gutter_origin = wrapper_origin + vec2f(0., self.offset.as_f32()); match self.element_type { GutterElementType::HiddenSection { expansion_type } => { let does_contain = if check_y_axis_only { // We can count the position if it's within the editor wrapper's whole line width let line_origin = Vector2F::new(wrapper_origin.x(), gutter_origin.y()); let line_size = Vector2F::new(wrapper_size.x(), self.height); RectF::new(line_origin, line_size).contains_point(position) } else { // Hidden sections use the full gutter width let size = vec2f(GUTTER_WIDTH, self.height); RectF::new(gutter_origin, size).contains_point(position) }; if does_contain { Some(GutterRange::HiddenSection { line: self.line.clone(), expansion_type, }) } else { None } } GutterElementType::DiffHunk { .. } => { let does_contain = if check_y_axis_only { // We can count the position if it's within the editor wrapper's whole line width let line_origin = Vector2F::new(wrapper_origin.x(), gutter_origin.y()); let line_size = Vector2F::new(wrapper_size.x(), self.height); RectF::new(line_origin, line_size).contains_point(position) } else { // For diff hunks, check if position is in the sliver or the full gutter // Get the sliver size (always use expanded=true for hit testing) let sliver_size = self.diff_hunk_size(true)?; let sliver_rect = RectF::new(gutter_origin, sliver_size); sliver_rect.contains_point(position) }; if does_contain { // Position is in the sliver Some(GutterRange::DiffHunk { line: self.line.clone(), in_sliver: true, }) } else { // Check if position is in the full gutter width (but not in sliver) let full_gutter_rect = RectF::new(gutter_origin, vec2f(GUTTER_WIDTH, self.height)); if full_gutter_rect.contains_point(position) { Some(GutterRange::DiffHunk { line: self.line.clone(), in_sliver: false, }) } else { None } } } } } /// The size of the diff hunk element in gutter (if exists). fn diff_hunk_size(&self, gutter_element_is_hovered: bool) -> Option<Vector2F> { match self.element_type { GutterElementType::HiddenSection { .. } => { // Hidden section elements are always horizontal. Some(vec2f(GUTTER_WIDTH, self.height)) } GutterElementType::DiffHunk { hunk: ref diff, .. } => { let vertical_indicator_width = if gutter_element_is_hovered { VERTICAL_DIFF_HUNK_INDICATOR_HOVERED_WIDTH } else { VERTICAL_DIFF_HUNK_INDICATOR_WIDTH }; diff.as_ref() .map(|_| vec2f(vertical_indicator_width, self.height)) } } } // The color of the diff hunk indicator element in gutter (if it exists). fn diff_indicator_color(&self, diff_hunks_are_expanded: bool) -> Option<ColorU> { if let GutterElementType::DiffHunk { hunk: Some(diff_hunk), change_type, } = self.element_type { match &diff_hunk { DiffHunkDisplay::Add(color) | DiffHunkDisplay::Remove(color) => Some(*color), DiffHunkDisplay::Replacement { collapsed_color: change_color, add_color, remove_color, } => { if diff_hunks_are_expanded { match change_type { ChangeType::Add => Some(*add_color), ChangeType::Remove => Some(*remove_color), } } else { Some(*change_color) } } } } else { None } } } /// States that need to live in between frames. #[derive(Default)] pub struct EditorWrapperState { /// The line range of the hovered diff hunk. hovered_diff_hunk: Mutex<Option<EditorLineLocation>>, /// Whether there is an active click. in_click: AtomicBool, /// Mouse state handle for the plus button. add_as_context_mouse_state: MouseStateHandle, /// Mouse state handle for the revert button. revert_mouse_state: MouseStateHandle, /// Mouse state handle for the comment button. comment_mouse_state: MouseStateHandle, /// Tracks the line range where the add context button was last clicked, /// so we don't show the button again until a different range is hovered. last_clicked_range: Mutex<Option<Range<LineCount>>>, } impl EditorWrapperState { /// Record that a range has been clicked (add context button was used) pub fn record_clicked_range(&self, range: Range<LineCount>) { *self.last_clicked_range.lock() = Some(range); } /// Clear the clicked range (when hovering over a new range) pub fn clear_clicked_range(&self) { *self.last_clicked_range.lock() = None; } /// Check if a range has been clicked pub fn is_range_clicked(&self, range: &Range<LineCount>) -> bool { self.last_clicked_range.lock().as_ref() == Some(range) } } pub type EditorWrapperStateHandle = Arc<EditorWrapperState>; pub enum GutterRange { DiffHunk { line: EditorLineLocation, // Whether the position is in the sliver (the colored part) of the diff hunk element. in_sliver: bool, }, HiddenSection { line: EditorLineLocation, expansion_type: ExpansionType, }, } impl GutterRange { pub fn line(&self) -> &EditorLineLocation { match self { GutterRange::DiffHunk { line, .. } => line, GutterRange::HiddenSection { line, .. } => line, } } } #[derive(Debug, Clone, Copy)] pub enum GutterHoverTarget { // The entire line covered by the gutter is considered the hover target. Line, // Only the gutter element itself is considered the hover target. GutterElement, } /// The caller defined handler for a click event. type EditorWrapperClickHandler = Box<dyn FnMut(GutterRange, &mut EventContext)>; /// UI Config for rendering line number. pub struct LineNumberConfig { pub font_family: FamilyId, pub font_size: f32, pub text_color: ColorU, pub highlight_text_color: ColorU, pub starting_line_number: Option<usize>, pub mode: CodeEditorLineNumberMode, pub active_line_number: Option<LineCount>, pub active_cursor_is_visible: bool, } impl LineNumberConfig { pub fn absolute_line_number(&self, line_count: LineCount) -> usize { line_count.as_usize() + self.starting_line_number.unwrap_or(1) } pub fn display_line_number(&self, line_count: LineCount) -> usize { if self.mode == CodeEditorLineNumberMode::Relative && let Some(active_line_number) = self.active_line_number && active_line_number != line_count { return active_line_number .as_usize() .abs_diff(line_count.as_usize()); } self.absolute_line_number(line_count) } } struct CommentBox { line_highlight_element: Box<dyn Element>, line: EditorLineLocation, } pub struct EditorWrapper<V: EditorView> { editor: InnerEditor<V>, element_size: Option<Vector2F>, element_origin: Option<Point>, /// Whether the editor should expand vertically to fill the available space. vertical_expansion_behavior: VerticalExpansionBehavior, /// If there is no [`LineNumberConfig`], the entire left gutter won't be rendered. line_number_config: Option<LineNumberConfig>, gutter_elements: Option<Vec<GutterElement>>, diff_status: DiffStatus, state_handle: EditorWrapperStateHandle, click_handler: EditorWrapperClickHandler, /// The current state of diff navigation in the editor diff_navigation_state: DiffNavigationState, /// The line range of the focused diff hunk (if any) focused_diff_line_range: Option<Range<LineCount>>, should_handle_scroll_wheel: bool, /// This helps us handle events properly on stacks. A stack will always /// put its children on higher z-indexes than its origin, so a hit test using the standard /// `z_index` method would always result in the event being covered (by the children of the /// stack). Instead, we track the upper-bound of z-indexes _contained by_ the child element. /// Then we use that upper bound to do the hit testing, which means a parent will always get /// events from its children, regardless of whether they are stacks or not. child_max_z_index: Option<ZIndex>, /// Display state of the "add as agent context" button shown next to diff hunks. add_hunk_as_context_button: Option<AddAsContextButton>, /// Display state of the "revert" button shown next to diff hunks. revert_hunk_button: Option<RevertHunkButton>, /// Display state of the "comment" button shown next to diff hunks. comment_button: Option<CommentButton>, // Todo: kc combine all comment related fields into a struct. comment_box: Option<CommentBox>, /// Lines with saved comments attached. These lines always have an /// indicator in the gutter element. saved_comments: Vec<SavedComment>, gutter_element_hover_target: GutterHoverTarget, expand_diff_indicator_width_on_hover: bool, comment_save_position_id: String, find_references_save_position_id: String, /// The line where find references card is anchored (if active). find_references_anchor: Option<EditorLineLocation>, } impl<V: EditorView> EditorWrapper<V> { fn paint_removed_line_overlays( &self, origin: Vector2F, wrapper_size: Vector2F, ctx: &mut PaintContext, ) { // Removed lines: group consecutive Remove-type gutter elements by line_range and // draw one full-width rect per group. let Some(gutter_elements) = &self.gutter_elements else { return; }; struct Group { start_y: f32, end_y: f32, line_range: Range<LineCount>, overlay: Fill, } let mut group: Option<Group> = None; let mut flush = |group: &mut Option<Group>| { let Some(group) = group.take() else { return; }; ctx.scene .draw_rect_without_hit_recording(RectF::new( origin + vec2f(0., group.start_y), vec2f(wrapper_size.x(), group.end_y - group.start_y), )) .with_background(group.overlay); }; for element in gutter_elements.iter() { let is_remove = matches!( element.element_type, GutterElementType::DiffHunk { change_type: ChangeType::Remove, .. } ); if !is_remove { flush(&mut group); continue; } let Some(overlay) = element.overlay else { flush(&mut group); continue; }; let current_range = element.line.line_range().clone(); let start_y = element.offset.as_f32(); let end_y = start_y + element.height; match &mut group { Some(group) if group.line_range == current_range && group.overlay == overlay => { group.end_y = end_y; } _ => { flush(&mut group); group = Some(Group { start_y, end_y, line_range: current_range, overlay, }); } } } flush(&mut group); } #[allow(clippy::too_many_arguments)] pub fn new(