4365 lines · 171.4 KB · Rust
RawDownload
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]// Adding this file level gate as some of the code around editability is not used in WASM yet. use std::future::Future;use std::ops::Range;use std::path::Path;use std::rc::Rc;use std::sync::Arc;use std::{cmp, mem}; use ai::diff_validation::DiffDelta;use itertools::Itertools;use languages::{Language, language_by_filename, language_by_local_filename, language_by_name};use line_ending::LineEnding;use num_traits::SaturatingSub;use rangemap::{RangeMap, RangeSet};use string_offset::CharOffset;use syntax_tree::{ColorMap, DecorationStateEvent, SyntaxTreeState};use vec1::{Vec1, vec1};use vim::vim::{    BracketChar, CharacterMotion, Direction, FindCharMotion, FirstNonWhitespaceMotion,    InsertPosition, LineMotion, MotionType, TextObjectInclusion, TextObjectType, VimOperator,    VimTextObject, WordBound, WordMotion, WordType,};use vim::{    find_next_paragraph_end, find_previous_paragraph_start, vim_a_block, vim_a_paragraph,    vim_a_quote, vim_a_word, vim_find_char_on_line, vim_find_matching_bracket, vim_inner_block,    vim_inner_paragraph, vim_inner_quote, vim_inner_word, vim_word_iterator_from_offset,};use warp_core::platform::SessionPlatform;use warp_core::semantic_selection::SemanticSelection;use warp_core::ui::theme::Fill;use warp_editor::content::anchor::Anchor;use warp_editor::content::buffer::{    AutoScrollBehavior, Buffer, BufferEditAction, BufferEvent, BufferSelectAction, EditOrigin,    InitialBufferState, SelectionOffsets, ShouldAutoscroll, ToBufferCharOffset, ToBufferPoint,    VimInsertPoint,};use warp_editor::content::edit::EditDelta;use warp_editor::content::find::{SearchConfig, SearchResults};use warp_editor::content::hidden_lines_model::HiddenLinesModel;use warp_editor::content::selection_model::BufferSelectionModel;use warp_editor::content::text::{BufferBlockStyle, IndentBehavior, IndentUnit};use warp_editor::content::version::BufferVersion;use warp_editor::decoration::DecorationLayer;use warp_editor::editor::TextDecoration;use warp_editor::model::{CoreEditorModel, PlainTextEditorModel};use warp_editor::multiline::{AnyMultilineString, LF, MultilineString};use warp_editor::render::model::{    AutoScrollMode, BlockItem, BlockSpacings, BrokenLinkStyle, CheckBoxStyle, ColumnUnit,    Decoration, HorizontalRuleStyle, InlineCodeStyle, LineCount, LineDecoration, ParagraphStyles,    RenderEvent, RenderLineLocation, RenderState, RichTextStyles, StyleUpdateAction, TableStyle,    UpdateDecorationAfterLayout, WidthSetting,};use warp_editor::selection::{SelectionMode, SelectionModel, TextDirection, TextUnit};use warp_util::standardized_path::StandardizedPath;use warpui::elements::{    AnchorPair, OffsetPositioning, OffsetType, PositionedElementOffsetBounds, PositioningAxis,    XAxisAnchor, YAxisAnchor,};use warpui::text::TextBuffer;use warpui::text::point::Point;use warpui::units::{IntoPixels, Pixels};use warpui::{AppContext, Entity, ModelAsRef, ModelContext, ModelHandle, SingletonEntity}; use super::super::DiffResult;use super::comments::{EditorCommentsModel, PendingComment, PendingCommentEvent};use super::diff::{    DiffModel, DiffModelEvent, DiffStatus, RenderableDiffHunk, add_inline_overlay_color,};use super::line::EditorLineLocation;use crate::appearance::Appearance;use crate::code::editor::line_iterator::LineIterator;use crate::code_review::comments::{CommentId, CommentOrigin, LineDiffContent};use crate::editor::InteractionState;use crate::notebooks::editor::model::word_unit;use crate::themes::theme::AnsiColorIdentifier;use crate::util::link_detection::get_word_range_at_offset; /// An opaque handle to a stable line in the editor content, suitable for scroll/// position preservation. Contains an internal anchor that tracks through/// buffer edits so the line position remains accurate even after insertions/// or deletions elsewhere in the file.////// Obtain via [`CodeEditorModel::line_at_vertical_offset`]; resolve back to a/// pixel offset via [`CodeEditorModel::line_top`].#[derive(Debug, Clone)]pub struct StableEditorLine(StableEditorLineInner); #[derive(Debug, Clone)]enum StableEditorLineInner {    /// Scroll position is on a line in the current buffer.    CurrentLine {        /// Anchored at the start of the line; tracks through buffer edits.        anchor: Anchor,    },    /// Scroll position is on a removed/deleted line shown in the diff.    RemovedLine {        /// Anchored at the start of the attachment line; tracks through        /// buffer edits.        anchor: Anchor,        temp_index: usize,    },} /// Enum used for vim movements to the start and end of linespub enum LineBound {    Start,    End,} #[derive(Debug, Clone, PartialEq)]pub enum DiffNavigationState {    /// Diff hunks are collapsed and not visible    Collapsed,    /// All diff hunks are expanded but none is specifically focused    Expanded,    /// All diff hunks are expanded and one specific hunk is focused    Focused(usize),} enum IndentMode {    NewlineAbove,    NewlineBelow,    LinewiseChange,    Enter,} struct IndentResult {    /// The indentation to insert before the cursor    insert_before_cursor: String,    /// Optionally, text to insert after the cursor (e.g. in the case    /// where we hit enter between brackets)    insert_after_cursor: Option<String>,} /// Case transformation kinds used by vim toggle case.pub enum CaseTransform {    Toggle,    Uppercase,    Lowercase,} impl CaseTransform {    fn apply_to(&self, input: String) -> String {        match self {            CaseTransform::Toggle => input                .chars()                .map(|c| {                    if c.is_lowercase() {                        c.to_uppercase().next().unwrap_or(c)                    } else if c.is_uppercase() {                        c.to_lowercase().next().unwrap_or(c)                    } else {                        c                    }                })                .collect(),            CaseTransform::Uppercase => input.to_uppercase(),            CaseTransform::Lowercase => input.to_lowercase(),        }    }} /// A hoverable link with an optional on-click handler.pub struct HoverableLink {    range: Range<CharOffset>,    #[allow(clippy::type_complexity)]    on_click: Option<Box<dyn Fn(&mut AppContext)>>,} impl HoverableLink {    pub fn new(range: Range<CharOffset>) -> Self {        Self {            range,            on_click: None,        }    }     pub fn with_on_click(mut self, on_click: Box<dyn Fn(&mut AppContext)>) -> Self {        self.on_click = Some(on_click);        self    }     pub fn range(&self) -> &Range<CharOffset> {        &self.range    }     pub fn trigger_on_click(&self, ctx: &mut AppContext) {        if let Some(on_click) = &self.on_click {            on_click(ctx);        }    }} pub enum CodeEditorModelEvent {    /// Emitted when diff decorations are updated (line highlights, removed lines, etc.)    DiffUpdated,    /// Emitted when syntax highlighting decorations are updated    SyntaxHighlightingUpdated,    ContentChanged {        origin: EditOrigin,    },    SelectionChanged,    UnifiedDiffComputed(Rc<DiffResult>),    ViewportUpdated(BufferVersion),    InteractionStateChanged,    DelayedRenderingFlushed,    /// Emitted when the render state layout has been updated.    /// Consumers can use this to invalidate cached heights.    LayoutInvalidated,    #[cfg(windows)]    WindowsCtrlC {        /// True if the `ctrl-c` action was used to copy an active selection.        copied_selection: bool,    },} /// Triggers to delay rendering until a certain event.#[derive(Clone, Copy)]enum DelayRenderingTrigger {    SyntaxHighlighting(BufferVersion),    DiffUpdate(BufferVersion),} struct DelayRendering {    edits: Vec<(EditDelta, BufferVersion)>,    should_autoscroll: ShouldAutoscroll,    block_until: DelayRenderingTrigger,} impl DelayRendering {    fn new(trigger: DelayRenderingTrigger) -> Self {        Self {            edits: Vec::new(),            should_autoscroll: ShouldAutoscroll::No,            block_until: trigger,        }    }     fn should_render_for_syntax_highlight(&self, buffer_version: BufferVersion) -> bool {        match self.block_until {            DelayRenderingTrigger::DiffUpdate(_) => false,            DelayRenderingTrigger::SyntaxHighlighting(version) => buffer_version >= version,        }    }     fn should_render_for_diff_update(&self, buffer_version: BufferVersion) -> bool {        match self.block_until {            DelayRenderingTrigger::DiffUpdate(version) => buffer_version >= version,            DelayRenderingTrigger::SyntaxHighlighting(_) => false,        }    }     fn flush_render(self, model: &CodeEditorModel, ctx: &mut ModelContext<CodeEditorModel>) {        model.render_state.update(ctx, move |render_state, _| {            let should_autoscroll = self.should_autoscroll;            for (delta, content_version) in self.edits {                render_state.add_pending_edit(delta, content_version);            }            match should_autoscroll {                ShouldAutoscroll::Yes => render_state.request_autoscroll(),                ShouldAutoscroll::VerticalOnly => render_state.request_vertical_autoscroll(),                ShouldAutoscroll::No => (),            }        });         // Refresh the diff state now that the pending render state has been flushed.        if model.diff_nav_is_active() {            model.refresh_diff_state(ctx);        }         ctx.emit(CodeEditorModelEvent::DelayedRenderingFlushed);    }     /// Consume the delay rendering state without flushing edits to the render state.    /// Use when a full layout rebuild will follow that supersedes the pending edits.    /// Still emits `DelayedRenderingFlushed` so downstream listeners (e.g. CodeReviewView)    /// are notified.    fn skip(self, ctx: &mut ModelContext<CodeEditorModel>) {        ctx.emit(CodeEditorModelEvent::DelayedRenderingFlushed);    }} pub struct CodeEditorModel {    render_state: ModelHandle<RenderState>,    content: ModelHandle<Buffer>,    selection_model: ModelHandle<BufferSelectionModel>,    diff: ModelHandle<DiffModel>,    selection: ModelHandle<SelectionModel>,    syntax_tree: ModelHandle<SyntaxTreeState>,    comments: ModelHandle<EditorCommentsModel>,    hidden_lines: ModelHandle<HiddenLinesModel>,    /// The current state of diff navigation (collapsed, expanded, or focused on a specific hunk)    diff_navigation_state: DiffNavigationState,    interaction_state: InteractionState,    /// Only applies to scenarios where current line highlighting is possible.    /// For example, current line highlighting will always be disabled during diff navigation.    show_current_line_highlights: bool,    /// Delay rendering of content updates until a certain trigger.    delay_rendering: Option<DelayRendering>,    /// Stores the selection "tails" when entering Vim visual mode so we can derive    /// visual selections that may differ from the cursor positions.    vim_visual_tails: Vec<CharOffset>,    hovered_symbol_range: Option<HoverableLink>,    /// Automatically hide lines outside of the active diff with X context lines.    hide_lines_outside_of_active_diff: Option<usize>,    /// Recalculate hidden lines after a diff for this buffer version or later completes.    recalculate_hidden_lines_after_diff: Option<BufferVersion>,    /// Whether this editor was configured to use lazy layout.    lazy_layout_enabled: bool,    /// Whether the editor has completed at least one layout cycle.    lazy_layout_initialized: bool,    /// Whether syntax parsing should be bootstrapped from the latest full buffer content.    pending_syntax_tree_bootstrap: bool,} impl CodeEditorModel {    pub fn new(        text_styles: RichTextStyles,        session_platform: Option<SessionPlatform>,        lazy_layout: bool,        buffer: Option<ModelHandle<Buffer>>, // Whether the editor is using an underlying shared buffer.        ctx: &mut ModelContext<Self>,    ) -> Self {        let content = buffer.unwrap_or_else(|| {            ctx.add_model(|_| {                Buffer::new(Box::new(|block_style, _| match block_style {                    // Use 4 spaces as the default indentation unit.                    BufferBlockStyle::PlainText => IndentBehavior::TabIndent(IndentUnit::Space(4)),                    _ => IndentBehavior::Ignore,                }))            })        });        content.update(ctx, |buffer, _| {            buffer.set_session_platform(session_platform);        });         Self::from_content(            content,            true,        // show_current_line_highlights            lazy_layout, // lazy_layout_enabled            false,       // lazy_layout_initialized            ctx,            |hidden_lines, ctx| {                ctx.add_model(|ctx| {                    RenderState::new(text_styles, lazy_layout, Some(hidden_lines.clone()), ctx)                        .with_width_setting(WidthSetting::InfiniteWidth)                })            },        )    }     /// Constructs a `CodeEditorModel` in TUI char-cell mode.    ///    /// Identical to `new` but creates the `RenderState` with    /// [`LayoutMode::CharCell`] so all soft-wrap positions use monospace    /// character-count arithmetic rather than font-aware pixel layout.    /// `TuiEditorModel` (in `warp_tui`) is a type alias for this type;    /// constructing via this method is what gives the TUI editor all of    /// `CodeEditorModel`'s features (vim, syntax, diff, hidden lines) for free    /// while sharing no GUI-rendering infrastructure.    ///    /// Like `new`, this reads syntax-highlight colors from the `Appearance`    /// singleton, so callers must register `Appearance` (a real one for the    /// runtime, `Appearance::mock()` for tests) before constructing the model.    pub fn new_tui(terminal_width: u16, ctx: &mut ModelContext<Self>) -> Self {        let content = ctx.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore)));         Self::from_content(            content,            false, // show_current_line_highlights: no GPU rendering in TUI            false, // lazy_layout_enabled: no lazy layout in TUI            true,  // lazy_layout_initialized: no lazy layout in TUI            ctx,            |hidden_lines, ctx| {                let hidden_lines = hidden_lines.clone();                // CharCell layout consumes the configured tab size; RenderState                // retains the remaining styles for API compatibility with pixel layout.                ctx.add_model(|ctx| {                    RenderState::new_tui(                        terminal_width,                        Self::tui_stub_text_styles(),                        hidden_lines,                        ctx,                    )                })            },        )    }     /// Shared construction for [`Self::new`] and [`Self::new_tui`]. The two modes    /// differ only in how the backing `content` buffer and the `RenderState` are    /// built (GUI pixel layout vs. TUI char-cell layout) plus a few flags; all    /// other sub-models (selection, syntax tree, diff, hidden lines, comments)    /// and event subscriptions are identical and wired up here.    ///    /// `build_render_state` receives the freshly-created `hidden_lines` handle so    /// both paths can attach it to their `RenderState`.    fn from_content(        content: ModelHandle<Buffer>,        show_current_line_highlights: bool,        lazy_layout_enabled: bool,        lazy_layout_initialized: bool,        ctx: &mut ModelContext<Self>,        build_render_state: impl FnOnce(            &ModelHandle<HiddenLinesModel>,            &mut ModelContext<Self>,        ) -> ModelHandle<RenderState>,    ) -> Self {        ctx.subscribe_to_model(&content, |me, _, event, ctx| {            me.handle_content_model_event(event, ctx);        });         let selection_model = ctx.add_model(|_ctx| BufferSelectionModel::new(content.clone()));         let color_map = Self::syntax_highlighting_color_map(ctx);        let buffer_version = content.as_ref(ctx).buffer_version();        let buffer_handle = content.downgrade();        let syntax_tree =            ctx.add_model(|_ctx| SyntaxTreeState::new(buffer_handle, buffer_version, color_map));        ctx.subscribe_to_model(&syntax_tree, |me, _, event, ctx| {            me.handle_syntax_tree_model_event(event, ctx);        });         let diff = ctx.add_model(|_ctx| DiffModel::new());        ctx.subscribe_to_model(&diff, |me, _, event, ctx| {            me.handle_diff_model_event(event, ctx);        });         let hidden_lines =            ctx.add_model(|_| HiddenLinesModel::new(content.clone(), selection_model.clone()));         let render_state = build_render_state(&hidden_lines, ctx);        ctx.subscribe_to_model(&render_state, |me, _, event, ctx| {            me.handle_render_state_model_event(event, ctx);        });        let selection = ctx.add_model(|ctx| {            SelectionModel::new(                content.clone(),                render_state.clone(),                selection_model.clone(),                Some(hidden_lines.clone()),                ctx,            )            .with_disable_hidden_navigation()        });         let comments = ctx.add_model(|_| EditorCommentsModel {            pending_comment: PendingComment::Closed,        });         Self {            render_state,            diff,            content,            selection_model,            selection,            syntax_tree,            comments,            hidden_lines,            diff_navigation_state: DiffNavigationState::Collapsed,            interaction_state: InteractionState::Editable,            show_current_line_highlights,            delay_rendering: None,            vim_visual_tails: vec![],            hovered_symbol_range: None,            hide_lines_outside_of_active_diff: None,            recalculate_hidden_lines_after_diff: None,            lazy_layout_enabled,            lazy_layout_initialized,            pending_syntax_tree_bootstrap: false,        }    }     /// A minimal [`RichTextStyles`] for the TUI char-cell editor.    ///    /// [`RenderState::new_tui`] consumes the base paragraph's tab size and    /// retains the remaining styles for API compatibility. This stub lives here    /// so the core editor crate doesn't carry a TUI-specific dependency.    fn tui_stub_text_styles() -> RichTextStyles {        use warpui::elements::{Border, Fill};        use warpui::fonts::{FamilyId, Weight};         const TRANSPARENT: warpui::color::ColorU = warpui::color::ColorU {            r: 0,            g: 0,            b: 0,            a: 0,        };        let paragraph = |fixed_width_tab_size| ParagraphStyles {            font_family: FamilyId(0),            font_size: 10.,            font_weight: Weight::Normal,            line_height_ratio: 1.,            text_color: TRANSPARENT,            baseline_ratio: 0.7,            fixed_width_tab_size,        };        RichTextStyles {

Lines 1–500 of 4365.