#![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::collections::{HashMap, HashSet};use std::fmt::Debug;use std::ops::Range;use std::path::Path; use ai::diff_validation::DiffDelta;use lazy_static::lazy_static;use num_traits::SaturatingSub;use pathfinder_geometry::vector::vec2f;use settings::Setting as _;use string_offset::CharOffset;use vec1::{Vec1, vec1};use vim::vim::{Direction, InsertPosition, VimMode, VimModel, VimState, VimSubscriber};use warp_core::platform::SessionPlatform;use warp_editor::content::buffer::{ Buffer, BufferEditAction, EditOrigin, InitialBufferState, ToBufferCharOffset as _, ToBufferPoint,};use warp_editor::content::text::IndentUnit;use warp_editor::content::version::BufferVersion;use warp_editor::model::{CoreEditorModel, PlainTextEditorModel};use warp_editor::multiline::AnyMultilineString;use warp_editor::render::element::lens_element::RichTextElementLens;use warp_editor::render::element::{ DisplayOptions, DisplayStateHandle, RichTextElement, VerticalExpansionBehavior,};use warp_editor::render::model::{ AutoScrollMode, BlockSpacing, CODE_EDITOR_HIDDEN_SECTION_EXPANSION_LINES, Decoration, ExpansionType, LineCount, ParagraphStyles, RichTextStyles,};use warp_editor::search::{MATCH_FILL, SELECTED_MATCH_FILL, SearchEvent, Searcher};use warp_util::content_version::ContentVersion;use warp_util::standardized_path::StandardizedPath;use warpui::elements::new_scrollable::{ AxisConfiguration, DualAxisConfig, NewScrollableElement, ScrollableAppearance,};use warpui::elements::{ ChildAnchor, ChildView, Dismiss, Fill, Flex, Margin, MouseStateHandle, NewScrollable, OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds, ScrollStateHandle, Shrinkable, Stack,};use warpui::event::ModifiersState;use warpui::keymap::Keystroke;use warpui::platform::Cursor;use warpui::prelude::RectF;use warpui::text::point::Point;use warpui::units::Pixels;use warpui::{ AppContext, BlurContext, CursorInfo, Element, Entity, FocusContext, ModelHandle, SingletonEntity, View, ViewContext, ViewHandle, WeakViewHandle, WindowId,}; use crate::appearance::Appearance;use crate::code::editor::EditorReviewComment;use crate::code::editor::comment_editor::{CommentEditor, CommentEditorEvent};use crate::code::editor::comments::PendingComment;use crate::code::editor::diff::DiffStatus;use crate::code::editor::element::{ AddAsContextButton, CommentButton, EditorWrapper, EditorWrapperStateHandle, GutterHoverTarget, GutterRange, InnerEditor, LineNumberConfig, RevertHunkButton,};use crate::code::editor::find::view::{CodeEditorFind as Find, Event as FindViewEvent};use crate::code::editor::goto_line::view::{Event as GoToLineEvent, GoToLineView};use crate::code::editor::line::EditorLineLocation;use crate::code::editor::model::{ CodeEditorModel, CodeEditorModelEvent, HoverableLink, LineBound, StableEditorLine,};use crate::code::editor::nav_bar::{NavBar, NavBarBehavior, NavBarEvent};use crate::code::editor::scroll::{ScrollPosition, ScrollTrigger, ScrollWheelBehavior};use crate::code::{ NoopCommentEditorProvider, NoopFindReferencesCardProvider, ShowCommentEditorProvider, ShowFindReferencesCardProvider,};use crate::code_review::comments::{CommentId, CommentOrigin};use crate::editor::InteractionState;use crate::features::FeatureFlag;use crate::notebooks::editor::rich_text_styles;use crate::settings::{AppEditorSettings, CodeEditorLineNumberMode, FontSettings};use crate::view_components::find::FindDirection; mod actions;pub(super) use actions::CodeEditorViewAction;pub use actions::init; mod vim_handler; /// Limit the keybindings that conflict with the Agent Mode embedded editor.const NON_EDITABLE_KEYMAP_CONTEXT: &str = "NonEditableKeymapContext"; lazy_static! { static ref AUTOCOMPLETE_SYMBOLS: HashMap<char, char> = HashMap::from([('(', ')'), ('[', ']'), ('{', '}'), ('\'', '\''), ('"', '"'),]); static ref CLOSING_SYMBOLS: HashSet<char> = AUTOCOMPLETE_SYMBOLS.values().cloned().collect();} pub enum CodeEditorEvent { Focused, ContentChanged { origin: EditOrigin, }, /// Emitted when a unified diff computation completes. UnifiedDiffComputed, SelectionChanged, SelectionStart, SelectionEnd, CopiedEmptyText, /// Emitted when the editor is in Vim mode and a user Escape while already in Normal mode. VimEscapeInNormalMode, /// Emitted when escape key is pressed (regardless of Vim state). /// This allows parent views to handle escape for closing overlays. EscapePressed, /// Emitted when diff decorations are updated (line highlights, removed lines, etc.) DiffUpdated, /// Emitted when the plus icon is clicked to add diff hunk context DiffHunkContextAdded { #[allow(dead_code)] line_range: Range<LineCount>, }, /// Emitted when a diff hunk is reverted DiffReverted, /// Emitted when the inline comment editor is opened. CommentEditorOpened, HiddenSectionExpanded, /// Emitted when a comment is saved. This gets propagated up so that it /// can be augmented with the file and repo paths and saved to the comment model. CommentSaved { comment: EditorReviewComment, }, RequestOpenComment(CommentId), /// Emitted when the viewport is updated after layout ViewportUpdated, DelayedRenderingFlushed, /// Emitted when the render state layout has been updated. LayoutInvalidated, #[cfg(windows)] WindowsCtrlC { /// True if the `ctrl-c` action was used to copy an active selection. copied_selection: bool, }, MouseHovered { offset: CharOffset, cmd: bool, clamped: bool, /// Whether the mouse move event was covered by an element above the editor. is_covered: bool, }, DeleteComment { id: CommentId, }, VimGotoDefinition, VimFindReferences, VimShowHover,} /// Store all states related to displaying the editor content.#[derive(Default)]struct DisplayHandles { vertical_scroll_state: ScrollStateHandle, horizontal_scroll_state: ScrollStateHandle, display_state: DisplayStateHandle, wrapper_state_handle: EditorWrapperStateHandle,} #[derive(Debug, Clone, Copy)]struct CodeEditorViewDisplayOptions { vertical_expansion_behavior: VerticalExpansionBehavior, /// If `false`, we will never show the diff UI in this code editor. can_show_diff_ui: bool, collapsible_diffs: bool, show_line_numbers: bool, scroll_wheel_behavior: ScrollWheelBehavior, horizontal_scrollbar_appearance: ScrollableAppearance, vertical_scrollbar_appearance: ScrollableAppearance, show_nav_bar: bool, /// The add as context button, or `None` if it is not currently visible. diff_hunk_as_context: Option<AddAsContextButton>, /// The revert diff button, or `None` if it is not currently visible. revert_diff_hunk: Option<RevertHunkButton>, /// The add comment button, or `None` if it is not currently visible. comment_button: Option<CommentButton>, /// Whether to expand the width of the diff indicator in the gutter on hover. expand_diff_indicator_width_on_hover: bool, // Provides a starting line number for markdown code blocks. starting_line_number: Option<usize>, gutter_hover_target: GutterHoverTarget, line_height_override: Option<f32>,} #[derive(Clone, Debug)]pub(super) struct SavedComment { uuid: CommentId, location: EditorLineLocation, mouse_state: MouseStateHandle,} impl SavedComment { pub fn location(&self) -> &EditorLineLocation { &self.location } pub fn mouse_state(&self) -> &MouseStateHandle { &self.mouse_state } pub fn uuid(&self) -> CommentId { self.uuid }} #[derive(Debug)]pub struct CodeEditorRenderOptions { vertical_expansion_behavior: VerticalExpansionBehavior, line_height_override: Option<f32>, lazy_layout: bool, show_comment_editor_provider: Box<dyn ShowCommentEditorProvider>, show_find_references_provider: Box<dyn ShowFindReferencesCardProvider>,} impl CodeEditorRenderOptions { pub fn new(vertical_expansion_behavior: VerticalExpansionBehavior) -> Self { Self { vertical_expansion_behavior, line_height_override: None, lazy_layout: false, show_comment_editor_provider: Box::new(NoopCommentEditorProvider), show_find_references_provider: Box::new(NoopFindReferencesCardProvider), } } pub fn lazy_layout(mut self) -> Self { self.lazy_layout = true; self } pub fn line_height_override(mut self, line_height: f32) -> Self { self.line_height_override = Some(line_height); self } pub fn with_show_comment_editor_provider( mut self, comment_editor_provider: impl ShowCommentEditorProvider, ) -> Self { self.show_comment_editor_provider = Box::new(comment_editor_provider); self } pub fn with_show_find_references_provider( mut self, find_references_provider: impl ShowFindReferencesCardProvider, ) -> Self { self.show_find_references_provider = Box::new(find_references_provider); self }} pub struct CodeEditorView { pub model: ModelHandle<CodeEditorModel>, pub searcher: ModelHandle<Searcher>, find_bar: Option<ViewHandle<Find>>, goto_line_dialog: ViewHandle<GoToLineView>, nav_bar: ViewHandle<NavBar>, display_states: DisplayHandles, is_selecting: bool, self_handle: WeakViewHandle<Self>, display_options: CodeEditorViewDisplayOptions, pending_scroll: Option<ScrollTrigger>, supports_vim_mode: bool, vim_model: ModelHandle<VimModel>, // Track the most recent Vim search direction to determine how to cycle (n/N) thereafter. last_search_direction: Direction, active_comment_editor: ViewHandle<CommentEditor>, /// TODO: maybe turn into a map for fast UUID or range lookup comment_locations: Vec<SavedComment>, /// Save position of the comment button rendered within this code editor view. comment_save_position_id: String, show_comment_editor_provider: Box<dyn ShowCommentEditorProvider>, /// Save position of the anchor point for find references card. find_references_save_position_id: String, show_find_references_provider: Box<dyn ShowFindReferencesCardProvider>, /// The offset where find references card is anchored (if showing). find_references_anchor_offset: Option<CharOffset>, window_id: WindowId,} impl CodeEditorView { /// A [`SessionPlatform`] is used to determine the default line ending for /// the code editor. It should be provided if the code editor can modify /// files on the user's system. pub fn new( session_platform: Option<SessionPlatform>, buffer: Option<ModelHandle<Buffer>>, render_options: CodeEditorRenderOptions, ctx: &mut ViewContext<Self>, ) -> Self { let appearance_handle = Appearance::handle(ctx); let font_settings_handle = FontSettings::handle(ctx); let initial_styles = code_text_styles( appearance_handle.as_ref(ctx), font_settings_handle.as_ref(ctx), render_options.line_height_override, ); ctx.subscribe_to_model(&appearance_handle, |me, _, _, ctx| { me.handle_appearance_or_font_change(ctx); }); ctx.subscribe_to_model(&font_settings_handle, |me, _, _, ctx| { me.handle_appearance_or_font_change(ctx); }); let app_editor_settings_handle = AppEditorSettings::handle(ctx); ctx.subscribe_to_model(&app_editor_settings_handle, |_, _, _, ctx| { ctx.notify(); }); let model = ctx.add_model(|ctx| { CodeEditorModel::new( initial_styles, session_platform, render_options.lazy_layout, buffer, ctx, ) }); ctx.subscribe_to_model(&model, |me, _, event, ctx| { me.handle_editor_model_event(event, ctx); }); // Creates a new model for searching the editor let buffer = model.as_ref(ctx).buffer().clone(); let selection_model = model.as_ref(ctx).buffer_selection_model().clone(); let searcher = ctx.add_model(|ctx| Searcher::new(buffer, selection_model, ctx)); // Subscribes to events from Searcher model so that we can handle ctx.subscribe_to_model(&searcher, |me, _, event, ctx| { me.handle_searcher_event(event, ctx); }); let find_bar = ctx.add_typed_action_view(|ctx| Find::new(searcher.clone(), ctx)); ctx.subscribe_to_view(&find_bar, move |me, _, event, ctx| { me.handle_find_event(event, ctx); }); let goto_line_dialog = ctx.add_typed_action_view(GoToLineView::new); ctx.subscribe_to_view(&goto_line_dialog, |me, _, event, ctx| { me.handle_goto_line_event(event, ctx); }); let nav_bar = ctx.add_typed_action_view(|ctx| NavBar::new(model.clone(), ctx)); ctx.subscribe_to_view(&nav_bar, |me, _, event, ctx| match event { NavBarEvent::Close => { me.toggle_diff_nav(None, ctx); ctx.notify(); } }); // If feature flag is enabled, enable vim mode. let supports_vim_mode = FeatureFlag::VimCodeEditor.is_enabled(); let vim_model = ctx.add_model(|_| VimModel::new()); ctx.subscribe_to_model(&vim_model, Self::handle_vim_event); // Ensure CodeEditorView starts in Normal mode when Vim keybindings are enabled. if supports_vim_mode && AppEditorSettings::as_ref(ctx).vim_mode_enabled() { vim_model.update(ctx, |vim_model, ctx| { if let Ok(escape) = Keystroke::parse("escape") { vim_model.keypress(&escape, ctx); } }); } // Ensure that we re-render when the rendering model changes. ctx.observe(&model.as_ref(ctx).render_state().clone(), |_, _, ctx| { ctx.notify(); }); let comment_model = model.as_ref(ctx).comments().clone(); let comment_editor = ctx.add_typed_action_view(|ctx| CommentEditor::new(ctx, comment_model)); ctx.subscribe_to_view(&comment_editor, |me, _, event, ctx| { me.handle_comment_editor_event(event, ctx); }); Self { searcher, find_bar: Some(find_bar), goto_line_dialog, model, display_states: Default::default(), is_selecting: false, self_handle: ctx.handle(), nav_bar, comment_locations: Vec::new(), display_options: CodeEditorViewDisplayOptions { vertical_expansion_behavior: render_options.vertical_expansion_behavior, can_show_diff_ui: true, collapsible_diffs: true, show_line_numbers: true, starting_line_number: None, show_nav_bar: true, diff_hunk_as_context: Default::default(), revert_diff_hunk: Default::default(), comment_button: Default::default(), // By default expand diff indicators on hover. expand_diff_indicator_width_on_hover: true, scroll_wheel_behavior: ScrollWheelBehavior::AlwaysHandle, // By default, we should render the horizontal scrollbar as overlay to prevent it // from truncating space for the code editor. We should not render it as an overlay // for small code editors. horizontal_scrollbar_appearance: ScrollableAppearance::new( warpui::elements::ScrollbarWidth::Auto, false, ), vertical_scrollbar_appearance: ScrollableAppearance::new( warpui::elements::ScrollbarWidth::Auto, false, ), gutter_hover_target: GutterHoverTarget::GutterElement, line_height_override: render_options.line_height_override, }, pending_scroll: None, supports_vim_mode, vim_model, last_search_direction: Direction::Forward, active_comment_editor: comment_editor, comment_save_position_id: format!("code_editor_comment_{}", ctx.view_id()), show_comment_editor_provider: render_options.show_comment_editor_provider, find_references_save_position_id: format!( "code_editor_find_references_{}", ctx.view_id() ), show_find_references_provider: render_options.show_find_references_provider, find_references_anchor_offset: None, window_id: ctx.window_id(), } } pub fn set_find_references_anchor_offset(&mut self, offset: Option<CharOffset>) { self.find_references_anchor_offset = offset; } pub fn find_references_save_position_id(&self) -> &str { &self.find_references_save_position_id } pub fn show_find_references_provider(&self) -> &dyn ShowFindReferencesCardProvider { self.show_find_references_provider.as_ref() } pub fn window_id(&self) -> WindowId { self.window_id } pub fn set_add_diff_hunk_as_context_button( &mut self, enabled: bool, ctx: &mut ViewContext<Self>, ) { self.display_options.diff_hunk_as_context = Some(AddAsContextButton::new(enabled)); ctx.notify(); } /// Enables the add context button (plus icon) in diff hunks. Only enable this for code review views. pub fn with_add_context_button(mut self) -> Self { self.display_options.diff_hunk_as_context = Some(AddAsContextButton::new(true /* enabled */)); self } /// Enables the "revert" button on diff hunks. Only enable this for code review views. pub fn with_revert_diff_hunk_button(mut self) -> Self { self.display_options.revert_diff_hunk = Some(RevertHunkButton::new(true /* is_enabled */)); self } /// Enables the "comment" button on diff hunks. Only enable this for code review views. pub fn with_comment_button(mut self) -> Self { self.display_options.comment_button = Some(CommentButton::default()); self } /// Disables the diff indicator expanding on hover. pub fn disable_diff_indicator_expansion_on_hover(mut self) -> Self { self.display_options.expand_diff_indicator_width_on_hover = false; self } pub fn with_gutter_hover_target(mut self, target: GutterHoverTarget) -> Self { self.display_options.gutter_hover_target = target; self } /// Enables clicking on diff hunk gutter elements to collapse changed sections. pub fn with_collapsible_diffs(mut self, enabled: bool) -> Self { self.display_options.collapsible_diffs = enabled; self } pub(crate) fn disable_find_and_replace(mut self) -> Self { self.find_bar = None; selfLines 1–500 of 2517. 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::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::ops::Range; use std::path::Path; use ai::diff_validation::DiffDelta; use lazy_static::lazy_static; use num_traits::SaturatingSub; use pathfinder_geometry::vector::vec2f; use settings::Setting as _; use string_offset::CharOffset; use vec1::{Vec1, vec1}; use vim::vim::{Direction, InsertPosition, VimMode, VimModel, VimState, VimSubscriber}; use warp_core::platform::SessionPlatform; use warp_editor::content::buffer::{ Buffer, BufferEditAction, EditOrigin, InitialBufferState, ToBufferCharOffset as _, ToBufferPoint, }; use warp_editor::content::text::IndentUnit; use warp_editor::content::version::BufferVersion; use warp_editor::model::{CoreEditorModel, PlainTextEditorModel}; use warp_editor::multiline::AnyMultilineString; use warp_editor::render::element::lens_element::RichTextElementLens; use warp_editor::render::element::{ DisplayOptions, DisplayStateHandle, RichTextElement, VerticalExpansionBehavior, }; use warp_editor::render::model::{ AutoScrollMode, BlockSpacing, CODE_EDITOR_HIDDEN_SECTION_EXPANSION_LINES, Decoration, ExpansionType, LineCount, ParagraphStyles, RichTextStyles, }; use warp_editor::search::{MATCH_FILL, SELECTED_MATCH_FILL, SearchEvent, Searcher}; use warp_util::content_version::ContentVersion; use warp_util::standardized_path::StandardizedPath; use warpui::elements::new_scrollable::{ AxisConfiguration, DualAxisConfig, NewScrollableElement, ScrollableAppearance, }; use warpui::elements::{ ChildAnchor, ChildView, Dismiss, Fill, Flex, Margin, MouseStateHandle, NewScrollable, OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds, ScrollStateHandle, Shrinkable, Stack, }; use warpui::event::ModifiersState; use warpui::keymap::Keystroke; use warpui::platform::Cursor; use warpui::prelude::RectF; use warpui::text::point::Point; use warpui::units::Pixels; use warpui::{ AppContext, BlurContext, CursorInfo, Element, Entity, FocusContext, ModelHandle, SingletonEntity, View, ViewContext, ViewHandle, WeakViewHandle, WindowId, }; use crate::appearance::Appearance; use crate::code::editor::EditorReviewComment; use crate::code::editor::comment_editor::{CommentEditor, CommentEditorEvent}; use crate::code::editor::comments::PendingComment; use crate::code::editor::diff::DiffStatus; use crate::code::editor::element::{ AddAsContextButton, CommentButton, EditorWrapper, EditorWrapperStateHandle, GutterHoverTarget, GutterRange, InnerEditor, LineNumberConfig, RevertHunkButton, }; use crate::code::editor::find::view::{CodeEditorFind as Find, Event as FindViewEvent}; use crate::code::editor::goto_line::view::{Event as GoToLineEvent, GoToLineView}; use crate::code::editor::line::EditorLineLocation; use crate::code::editor::model::{ CodeEditorModel, CodeEditorModelEvent, HoverableLink, LineBound, StableEditorLine, }; use crate::code::editor::nav_bar::{NavBar, NavBarBehavior, NavBarEvent}; use crate::code::editor::scroll::{ScrollPosition, ScrollTrigger, ScrollWheelBehavior}; use crate::code::{ NoopCommentEditorProvider, NoopFindReferencesCardProvider, ShowCommentEditorProvider, ShowFindReferencesCardProvider, }; use crate::code_review::comments::{CommentId, CommentOrigin}; use crate::editor::InteractionState; use crate::features::FeatureFlag; use crate::notebooks::editor::rich_text_styles; use crate::settings::{AppEditorSettings, CodeEditorLineNumberMode, FontSettings}; use crate::view_components::find::FindDirection; mod actions; pub(super) use actions::CodeEditorViewAction; pub use actions::init; mod vim_handler; /// Limit the keybindings that conflict with the Agent Mode embedded editor. const NON_EDITABLE_KEYMAP_CONTEXT: &str = "NonEditableKeymapContext"; lazy_static! { static ref AUTOCOMPLETE_SYMBOLS: HashMap<char, char> = HashMap::from([('(', ')'), ('[', ']'), ('{', '}'), ('\'', '\''), ('"', '"'),]); static ref CLOSING_SYMBOLS: HashSet<char> = AUTOCOMPLETE_SYMBOLS.values().cloned().collect(); } pub enum CodeEditorEvent { Focused, ContentChanged { origin: EditOrigin, }, /// Emitted when a unified diff computation completes. UnifiedDiffComputed, SelectionChanged, SelectionStart, SelectionEnd, CopiedEmptyText, /// Emitted when the editor is in Vim mode and a user Escape while already in Normal mode. VimEscapeInNormalMode, /// Emitted when escape key is pressed (regardless of Vim state). /// This allows parent views to handle escape for closing overlays. EscapePressed, /// Emitted when diff decorations are updated (line highlights, removed lines, etc.) DiffUpdated, /// Emitted when the plus icon is clicked to add diff hunk context DiffHunkContextAdded { #[allow(dead_code)] line_range: Range<LineCount>, }, /// Emitted when a diff hunk is reverted DiffReverted, /// Emitted when the inline comment editor is opened. CommentEditorOpened, HiddenSectionExpanded, /// Emitted when a comment is saved. This gets propagated up so that it /// can be augmented with the file and repo paths and saved to the comment model. CommentSaved { comment: EditorReviewComment, }, RequestOpenComment(CommentId), /// Emitted when the viewport is updated after layout ViewportUpdated, DelayedRenderingFlushed, /// Emitted when the render state layout has been updated. LayoutInvalidated, #[cfg(windows)] WindowsCtrlC { /// True if the `ctrl-c` action was used to copy an active selection. copied_selection: bool, }, MouseHovered { offset: CharOffset, cmd: bool, clamped: bool, /// Whether the mouse move event was covered by an element above the editor. is_covered: bool, }, DeleteComment { id: CommentId, }, VimGotoDefinition, VimFindReferences, VimShowHover, } /// Store all states related to displaying the editor content. #[derive(Default)] struct DisplayHandles { vertical_scroll_state: ScrollStateHandle, horizontal_scroll_state: ScrollStateHandle, display_state: DisplayStateHandle, wrapper_state_handle: EditorWrapperStateHandle, } #[derive(Debug, Clone, Copy)] struct CodeEditorViewDisplayOptions { vertical_expansion_behavior: VerticalExpansionBehavior, /// If `false`, we will never show the diff UI in this code editor. can_show_diff_ui: bool, collapsible_diffs: bool, show_line_numbers: bool, scroll_wheel_behavior: ScrollWheelBehavior, horizontal_scrollbar_appearance: ScrollableAppearance, vertical_scrollbar_appearance: ScrollableAppearance, show_nav_bar: bool, /// The add as context button, or `None` if it is not currently visible. diff_hunk_as_context: Option<AddAsContextButton>, /// The revert diff button, or `None` if it is not currently visible. revert_diff_hunk: Option<RevertHunkButton>, /// The add comment button, or `None` if it is not currently visible. comment_button: Option<CommentButton>, /// Whether to expand the width of the diff indicator in the gutter on hover. expand_diff_indicator_width_on_hover: bool, // Provides a starting line number for markdown code blocks. starting_line_number: Option<usize>, gutter_hover_target: GutterHoverTarget, line_height_override: Option<f32>, } #[derive(Clone, Debug)] pub(super) struct SavedComment { uuid: CommentId, location: EditorLineLocation, mouse_state: MouseStateHandle, } impl SavedComment { pub fn location(&self) -> &EditorLineLocation { &self.location } pub fn mouse_state(&self) -> &MouseStateHandle { &self.mouse_state } pub fn uuid(&self) -> CommentId { self.uuid } } #[derive(Debug)] pub struct CodeEditorRenderOptions { vertical_expansion_behavior: VerticalExpansionBehavior, line_height_override: Option<f32>, lazy_layout: bool, show_comment_editor_provider: Box<dyn ShowCommentEditorProvider>, show_find_references_provider: Box<dyn ShowFindReferencesCardProvider>, } impl CodeEditorRenderOptions { pub fn new(vertical_expansion_behavior: VerticalExpansionBehavior) -> Self { Self { vertical_expansion_behavior, line_height_override: None, lazy_layout: false, show_comment_editor_provider: Box::new(NoopCommentEditorProvider), show_find_references_provider: Box::new(NoopFindReferencesCardProvider), } } pub fn lazy_layout(mut self) -> Self { self.lazy_layout = true; self } pub fn line_height_override(mut self, line_height: f32) -> Self { self.line_height_override = Some(line_height); self } pub fn with_show_comment_editor_provider( mut self, comment_editor_provider: impl ShowCommentEditorProvider, ) -> Self { self.show_comment_editor_provider = Box::new(comment_editor_provider); self } pub fn with_show_find_references_provider( mut self, find_references_provider: impl ShowFindReferencesCardProvider, ) -> Self { self.show_find_references_provider = Box::new(find_references_provider); self } } pub struct CodeEditorView { pub model: ModelHandle<CodeEditorModel>, pub searcher: ModelHandle<Searcher>, find_bar: Option<ViewHandle<Find>>, goto_line_dialog: ViewHandle<GoToLineView>, nav_bar: ViewHandle<NavBar>, display_states: DisplayHandles, is_selecting: bool, self_handle: WeakViewHandle<Self>, display_options: CodeEditorViewDisplayOptions, pending_scroll: Option<ScrollTrigger>, supports_vim_mode: bool, vim_model: ModelHandle<VimModel>, // Track the most recent Vim search direction to determine how to cycle (n/N) thereafter. last_search_direction: Direction, active_comment_editor: ViewHandle<CommentEditor>, /// TODO: maybe turn into a map for fast UUID or range lookup comment_locations: Vec<SavedComment>, /// Save position of the comment button rendered within this code editor view. comment_save_position_id: String, show_comment_editor_provider: Box<dyn ShowCommentEditorProvider>, /// Save position of the anchor point for find references card. find_references_save_position_id: String, show_find_references_provider: Box<dyn ShowFindReferencesCardProvider>, /// The offset where find references card is anchored (if showing). find_references_anchor_offset: Option<CharOffset>, window_id: WindowId, } impl CodeEditorView { /// A [`SessionPlatform`] is used to determine the default line ending for /// the code editor. It should be provided if the code editor can modify /// files on the user's system. pub fn new( session_platform: Option<SessionPlatform>, buffer: Option<ModelHandle<Buffer>>, render_options: CodeEditorRenderOptions, ctx: &mut ViewContext<Self>, ) -> Self { let appearance_handle = Appearance::handle(ctx); let font_settings_handle = FontSettings::handle(ctx); let initial_styles = code_text_styles( appearance_handle.as_ref(ctx), font_settings_handle.as_ref(ctx), render_options.line_height_override, ); ctx.subscribe_to_model(&appearance_handle, |me, _, _, ctx| { me.handle_appearance_or_font_change(ctx); }); ctx.subscribe_to_model(&font_settings_handle, |me, _, _, ctx| { me.handle_appearance_or_font_change(ctx); }); let app_editor_settings_handle = AppEditorSettings::handle(ctx); ctx.subscribe_to_model(&app_editor_settings_handle, |_, _, _, ctx| { ctx.notify(); }); let model = ctx.add_model(|ctx| { CodeEditorModel::new( initial_styles, session_platform, render_options.lazy_layout, buffer, ctx, ) }); ctx.subscribe_to_model(&model, |me, _, event, ctx| { me.handle_editor_model_event(event, ctx); }); // Creates a new model for searching the editor let buffer = model.as_ref(ctx).buffer().clone(); let selection_model = model.as_ref(ctx).buffer_selection_model().clone(); let searcher = ctx.add_model(|ctx| Searcher::new(buffer, selection_model, ctx)); // Subscribes to events from Searcher model so that we can handle ctx.subscribe_to_model(&searcher, |me, _, event, ctx| { me.handle_searcher_event(event, ctx); }); let find_bar = ctx.add_typed_action_view(|ctx| Find::new(searcher.clone(), ctx)); ctx.subscribe_to_view(&find_bar, move |me, _, event, ctx| { me.handle_find_event(event, ctx); }); let goto_line_dialog = ctx.add_typed_action_view(GoToLineView::new); ctx.subscribe_to_view(&goto_line_dialog, |me, _, event, ctx| { me.handle_goto_line_event(event, ctx); }); let nav_bar = ctx.add_typed_action_view(|ctx| NavBar::new(model.clone(), ctx)); ctx.subscribe_to_view(&nav_bar, |me, _, event, ctx| match event { NavBarEvent::Close => { me.toggle_diff_nav(None, ctx); ctx.notify(); } }); // If feature flag is enabled, enable vim mode. let supports_vim_mode = FeatureFlag::VimCodeEditor.is_enabled(); let vim_model = ctx.add_model(|_| VimModel::new()); ctx.subscribe_to_model(&vim_model, Self::handle_vim_event); // Ensure CodeEditorView starts in Normal mode when Vim keybindings are enabled. if supports_vim_mode && AppEditorSettings::as_ref(ctx).vim_mode_enabled() { vim_model.update(ctx, |vim_model, ctx| { if let Ok(escape) = Keystroke::parse("escape") { vim_model.keypress(&escape, ctx); } }); } // Ensure that we re-render when the rendering model changes. ctx.observe(&model.as_ref(ctx).render_state().clone(), |_, _, ctx| { ctx.notify(); }); let comment_model = model.as_ref(ctx).comments().clone(); let comment_editor = ctx.add_typed_action_view(|ctx| CommentEditor::new(ctx, comment_model)); ctx.subscribe_to_view(&comment_editor, |me, _, event, ctx| { me.handle_comment_editor_event(event, ctx); }); Self { searcher, find_bar: Some(find_bar), goto_line_dialog, model, display_states: Default::default(), is_selecting: false, self_handle: ctx.handle(), nav_bar, comment_locations: Vec::new(), display_options: CodeEditorViewDisplayOptions { vertical_expansion_behavior: render_options.vertical_expansion_behavior, can_show_diff_ui: true, collapsible_diffs: true, show_line_numbers: true, starting_line_number: None, show_nav_bar: true, diff_hunk_as_context: Default::default(), revert_diff_hunk: Default::default(), comment_button: Default::default(), // By default expand diff indicators on hover. expand_diff_indicator_width_on_hover: true, scroll_wheel_behavior: ScrollWheelBehavior::AlwaysHandle, // By default, we should render the horizontal scrollbar as overlay to prevent it // from truncating space for the code editor. We should not render it as an overlay // for small code editors. horizontal_scrollbar_appearance: ScrollableAppearance::new( warpui::elements::ScrollbarWidth::Auto, false, ), vertical_scrollbar_appearance: ScrollableAppearance::new( warpui::elements::ScrollbarWidth::Auto, false, ), gutter_hover_target: GutterHoverTarget::GutterElement, line_height_override: render_options.line_height_override, }, pending_scroll: None, supports_vim_mode, vim_model, last_search_direction: Direction::Forward, active_comment_editor: comment_editor, comment_save_position_id: format!("code_editor_comment_{}", ctx.view_id()), show_comment_editor_provider: render_options.show_comment_editor_provider, find_references_save_position_id: format!( "code_editor_find_references_{}", ctx.view_id() ), show_find_references_provider: render_options.show_find_references_provider, find_references_anchor_offset: None, window_id: ctx.window_id(), } } pub fn set_find_references_anchor_offset(&mut self, offset: Option<CharOffset>) { self.find_references_anchor_offset = offset; } pub fn find_references_save_position_id(&self) -> &str { &self.find_references_save_position_id } pub fn show_find_references_provider(&self) -> &dyn ShowFindReferencesCardProvider { self.show_find_references_provider.as_ref() } pub fn window_id(&self) -> WindowId { self.window_id } pub fn set_add_diff_hunk_as_context_button( &mut self, enabled: bool, ctx: &mut ViewContext<Self>, ) { self.display_options.diff_hunk_as_context = Some(AddAsContextButton::new(enabled)); ctx.notify(); } /// Enables the add context button (plus icon) in diff hunks. Only enable this for code review views. pub fn with_add_context_button(mut self) -> Self { self.display_options.diff_hunk_as_context = Some(AddAsContextButton::new(true /* enabled */)); self } /// Enables the "revert" button on diff hunks. Only enable this for code review views. pub fn with_revert_diff_hunk_button(mut self) -> Self { self.display_options.revert_diff_hunk = Some(RevertHunkButton::new(true /* is_enabled */)); self } /// Enables the "comment" button on diff hunks. Only enable this for code review views. pub fn with_comment_button(mut self) -> Self { self.display_options.comment_button = Some(CommentButton::default()); self } /// Disables the diff indicator expanding on hover. pub fn disable_diff_indicator_expansion_on_hover(mut self) -> Self { self.display_options.expand_diff_indicator_width_on_hover = false; self } pub fn with_gutter_hover_target(mut self, target: GutterHoverTarget) -> Self { self.display_options.gutter_hover_target = target; self } /// Enables clicking on diff hunk gutter elements to collapse changed sections. pub fn with_collapsible_diffs(mut self, enabled: bool) -> Self { self.display_options.collapsible_diffs = enabled; self } pub(crate) fn disable_find_and_replace(mut self) -> Self { self.find_bar = None; self