#![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. Load more
#![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 lines pub 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 {