1694 lines · 65.5 KB · Rust
RawDownload
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.