/// This module contains a model that can be used for loading and saving text files/// and displaying them in a code editor./// It also handles applying an optional diff to the file content that will be applied/// when the file is loaded.use std::{ ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, time::Duration,}; use ai::diff_validation::DiffType;use futures::stream::AbortHandle;use lsp::types::FileLocation;use lsp::{ LanguageId, LanguageServerId, LspEvent, LspManagerModel, LspManagerModelEvent, LspServerModel, ReferenceLocation,};use lsp_types::FormattingOptions;use markdown_parser::FormattedText;use num_traits::SaturatingSub;use pathfinder_color::ColorU;use pathfinder_geometry::rect::RectF;use pathfinder_geometry::vector::Vector2F;use remote_server::manager::RemoteServerManager;#[cfg(feature = "local_fs")]use repo_metadata::repositories::DetectedRepositories;use string_offset::CharOffset;use vec1::Vec1;use vim::vim::{MotionType, VimMode};use warp_core::r#async::debounce;use warp_core::features::FeatureFlag;use warp_core::ui::appearance::Appearance;use warp_core::ui::icons::Icon;use warp_editor::content::buffer::InitialBufferState;use warp_editor::content::text::IndentUnit;use warp_editor::render::model::{Decoration, LineCount};use warp_util::content_version::ContentVersion;use warp_util::file::{FileId, FileLoadError, FileSaveError};#[cfg(feature = "local_fs")]use warp_util::local_or_remote_path::LocalOrRemotePath;use warp_util::path::to_relative_path;use warp_util::sync::Condition;use warpui::elements::{ Border, ChildAnchor, ChildView, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Rect, Shrinkable, Stack, Text,};use warpui::keymap::FixedBinding;use warpui::keymap::macros::*;use warpui::platform::SaveFilePickerConfiguration;use warpui::text::point::Point;use warpui::ui_components::button::ButtonVariant;use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};use warpui::{ AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, WindowId,}; use crate::ai::persisted_workspace::{PersistedWorkspace, PersistedWorkspaceEvent};use crate::code::buffer_location::LocalOrRemotePath as BufferFileLocation;use crate::code::editor::EditorReviewComment;use crate::code::editor::model::HoverableLink;use crate::code::footer::{CodeFooterView, CodeFooterViewEvent};use crate::code::global_buffer_model::{BufferState, GlobalBufferModel, GlobalBufferModelEvent};use crate::code::{SaveOutcome, ShowFindReferencesCardProvider};use crate::code_review::comments::CommentId;use crate::menu::{Event, Menu, MenuItem, MenuItemFields};use crate::settings::{AISettings, CodeSettings};use crate::terminal::TerminalView;use crate::workspace::WorkspaceAction; const DROP_SHADOW_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 48,}; const HOVER_DEBOUNCE_PERIOD: Duration = Duration::from_millis(500); /// How long to wait after the user stops typing before triggering a debounced/// auto-save. Mirrors VS Code's default `files.autoSaveDelay` of 1000ms.const AUTO_SAVE_DEBOUNCE_PERIOD: Duration = Duration::from_millis(1000); use warp_core::send_telemetry_from_ctx; use super::ImmediateSaveError;use super::diff_viewer::DiffViewer;use super::editor::scroll::{ScrollPosition, ScrollTrigger};use super::editor::view::{CodeEditorEvent, CodeEditorView};use super::find_references_view::{FindReferencesView, FindReferencesViewEvent};use super::language_server_extension::ProcessedDiagnostic;use super::lsp_telemetry::LspTelemetryEvent; type SaveCallback = Box<dyn FnOnce(SaveOutcome, &mut ViewContext<LocalCodeEditorView>) + Send + Sync + 'static>; pub fn init(app: &mut AppContext) { app.register_fixed_bindings([FixedBinding::new( "cmdorctrl-l", LocalCodeEditorAction::InsertSelectedTextToInput, id!("LocalCodeEditorView") & !id!("IMEOpen"), )]);} pub enum LocalCodeEditorEvent { FileLoaded, FailedToLoad { error: Rc<FileLoadError>, }, FileSaved { /// Whether the save was triggered by auto-save. Used to suppress the /// "File saved." toast, which should only appear for manual (cmd-s) saves. auto_saved: bool, }, FailedToSave { error: Arc<FileSaveError>, }, DiffAccepted, DiffRejected, /// Emitted when a user presses Escape in Vim Normal mode inside the embedded editor. VimMinimizeRequested, /// Emitted when a user edits the file. UserEdited, /// Emitted when the diff status changes (e.g., line counts update). DiffStatusUpdated, SelectionAddedAsContext { relative_file_path: String, /// 1-indexed line range of the selection: `[start, end]` both inclusive. line_range: Range<LineCount>, /// Literal text content of the selection. selected_text: String, }, DiscardUnsavedChanges { path: PathBuf, }, GotoDefinition { path: PathBuf, line: usize, column: usize, /// The ID of the LSP server that produced this definition. /// Used to register external files with the correct server. source_server_id: LanguageServerId, }, /// Emitted when a comment is saved. This propagates the comment content /// changes to the CodeReviewView, which will update the comment model. CommentSaved { comment: EditorReviewComment, }, RequestOpenComment(CommentId), DeleteComment { id: CommentId, }, /// Emitted when the viewport is updated after layout ViewportUpdated, /// Emitted when the render state layout has been updated. LayoutInvalidated, /// Request to open LSP logs for the given file path. /// The workspace will handle opening a terminal with `tail -f` on the log file. OpenLspLogs { log_path: PathBuf, }, RunTabConfigSkill { path: PathBuf, }, DelayedRenderingFlushed,} /// Metadata about a file that is opened in the code view.#[derive(Debug, Clone)]struct LoadedFileMetadata { id: FileId, location: BufferFileLocation,} use warp_errors::report_error; pub use super::diff_viewer::DisplayMode; type TerminalTargetFn = dyn Fn(WindowId, &AppContext) -> Option<ViewHandle<TerminalView>>; struct SelectionAsContextTooltip { mouse_state: MouseStateHandle, terminal_target_fn: Box<TerminalTargetFn>,} #[derive(Debug, Clone)]pub enum LocalCodeEditorAction { InsertSelectedTextToInput, SaveFile, DiscardUnsavedChanges, NavigateToTarget(FileLocation), GotoDefinition, FindReferences, OpenContextMenu, /// Lazily fetch find-references and show the card (triggered on cmd-click when at definition). /// This is the fallback when go-to-definition has no different location to navigate to. FetchAndShowFindReferences { lsp_position: lsp::types::Location, anchor_offset: CharOffset, },} #[derive(Default)]struct ConflictResolutionBannerMouseStates { discard_mouse_state: MouseStateHandle, overwrite_mouse_state: MouseStateHandle,} #[derive(Default)]struct ContextMenuState { mouse_state: MouseStateHandle, is_open: bool,} /// A hover content segment - either plain text/markdown or a code block.pub(super) enum HoverContentSegment { /// Plain text/markdown content (rendered with FormattedTextElement). Text(FormattedText), /// Code block with syntax highlighting (rendered with CodeEditorView). CodeBlock { view: ViewHandle<CodeEditorView> },} /// State for the LSP hover tooltip.pub(super) enum LspHoverState { None, Loading(Option<AbortHandle>), Loaded { /// The content segments to display in the hover tooltip (from LSP hover info). segments: Vec<HoverContentSegment>, /// Diagnostics at the hovered location (displayed first, before hover info). diagnostics: Vec<ProcessedDiagnostic>, /// The offset range where the hover was triggered (used for positioning). hovered_offset_range: Range<CharOffset>, /// Scroll state for the tooltip content. scroll_state: ClippedScrollStateHandle, mouse_state: MouseStateHandle, },} impl LspHoverState { pub(super) fn clear(&mut self) -> bool { if matches!(self, LspHoverState::None) { return false; } if let Self::Loading(Some(handle)) = self { handle.abort(); } *self = LspHoverState::None; true } pub(super) fn contains_offset(&self, offset: CharOffset) -> bool { if let LspHoverState::Loaded { hovered_offset_range, .. } = self { return hovered_offset_range.contains(&offset); } false }} pub(super) const HOVER_TOOLTIP_MAX_WIDTH: f32 = 400.;pub(super) const HOVER_TOOLTIP_MAX_HEIGHT: f32 = 100.; pub struct LocalCodeEditorView { pub(super) editor: ViewHandle<CodeEditorView>, metadata: Option<LoadedFileMetadata>, enable_diff_nav_by_default: bool, is_new_file: bool, diff_type: Option<DiffType>, selection_as_context_tooltip: Option<SelectionAsContextTooltip>, /// A marker for when the backing file has first been loaded. This is used to prevent applying /// a diff before it can be properly calculated. file_loaded: Condition, /// Whether content was changed from its base. was_edited: bool, /// Content version of the base file state. base_content_version: Option<ContentVersion>, /// Set to `true` when a `RemoteBufferConflict` event fires for this /// editor's buffer. Cleared when the user discards or overwrites. has_remote_conflict: bool, conflict_banner_mouse_states: ConflictResolutionBannerMouseStates, /// Default directory to use for save dialogs when creating new files default_directory: Option<PathBuf>, pub(super) lsp_server: Option<ModelHandle<LspServerModel>>, /// Footer for displaying LSP status. Only created for normal editing contexts, not for diff/review views. footer: Option<ViewHandle<CodeFooterView>>, /// Context menu for right-click actions. context_menu: ViewHandle<Menu<LocalCodeEditorAction>>, context_menu_state: ContextMenuState, /// Channel for debouncing hover requests. hover_debounce_tx: async_channel::Sender<CharOffset>, /// Channel for debouncing auto-save requests while the user types. auto_save_debounce_tx: async_channel::Sender<()>, /// Set while an auto-save (debounced or focus-change) is in flight so the /// resulting `FileSaved` event can be marked as an auto-save and suppress /// the success toast. auto_save_in_flight: bool, /// State for the LSP hover tooltip. pub(super) lsp_hover_state: LspHoverState, /// Pending scroll position to apply after the file is loaded. This is used when /// `set_pending_scroll` is called before the file content has finished loading /// (e.g., in the GlobalBuffer path where content loads asynchronously). pending_scroll_on_load: Option<ScrollPosition>, /// Cached processed diagnostics. Updated when diagnostics change. /// Used as source of truth for both decorations and hover display. pub(super) processed_diagnostics: Vec<ProcessedDiagnostic>, /// Decorations for LSP diagnostics (errors and warnings). pub(super) diagnostic_decorations: Vec<Decoration>, /// View for the find references feature. find_references_view: Option<ViewHandle<FindReferencesView>>,} impl LocalCodeEditorView { pub fn new( editor: ViewHandle<CodeEditorView>, diff_type: Option<DiffType>, enable_diff_nav_by_default: bool, display_mode: Option<DisplayMode>, ctx: &mut ViewContext<Self>, ) -> Self { let context_menu = ctx.add_typed_action_view(|_| { Menu::new() .prevent_interaction_with_other_elements() .with_drop_shadow() }); ctx.subscribe_to_view(&context_menu, |me, _, event, ctx| { me.handle_menu_event(event, ctx); }); ctx.subscribe_to_view(&editor, |me, _, event, ctx| match event { CodeEditorEvent::UnifiedDiffComputed => { ctx.emit(LocalCodeEditorEvent::DiffAccepted); } CodeEditorEvent::ContentChanged { origin, .. } => { me.update_diff_hunk_gutter_buttons(ctx); // Clear cached diagnostics/decorations on any content change. This is to avoid showing stale diagnostics while we are waiting for new diagnostics. if !me.processed_diagnostics.is_empty() || !me.diagnostic_decorations.is_empty() { me.clear_diagnostics(ctx); } if origin.from_user() { me.was_edited = true; ctx.emit(LocalCodeEditorEvent::UserEdited); // Queue a debounced auto-save while the user types. The // debounced path saves without running the LSP formatter, // matching VS Code's `files.autoSave: afterDelay` behavior. // A `Some` `diff_type` means this editor is showing a // pending accept/reject diff (e.g. an agent "edit-file" // proposal), which must never auto-save. Editable // code-review diffs use `diff_type = None` and stay eligible. if me.diff_type.is_none() && *CodeSettings::as_ref(ctx).auto_save { let _ = me.auto_save_debounce_tx.try_send(()); } } } CodeEditorEvent::VimEscapeInNormalMode => { if me.dismiss_lsp_overlays(ctx) { ctx.notify(); } else { ctx.emit(LocalCodeEditorEvent::VimMinimizeRequested); } } CodeEditorEvent::EscapePressed => { if me.dismiss_lsp_overlays(ctx) { ctx.notify(); } } CodeEditorEvent::DiffUpdated => { ctx.emit(LocalCodeEditorEvent::DiffStatusUpdated); } CodeEditorEvent::SelectionEnd => { ctx.notify(); } CodeEditorEvent::MouseHovered { offset, cmd, clamped, is_covered, } => { // If the mouse location is clamped (meaning it's not hovering on an actual buffer text), // or if the event is covered by an element above the editor, // we should clear the hovered range and symbol. // // However, if the mouse is over the LSP hover card itself, we should not clear // the hover state - the "covered" event is expected when hovering over the tooltip. if *clamped || *is_covered { // Check if the mouse is currently over the hover tooltip. let is_over_hover_card = if let LspHoverState::Loaded { mouse_state, .. } = &me.lsp_hover_state { mouse_state .lock() .ok() .is_some_and(|state| state.is_mouse_over_element()) } else { false }; // If the mouse is over the hover card, don't clear the hover state. if is_over_hover_card { return; } let mut updated = me .editor .update(ctx, |editor, ctx| editor.clear_hovered_symbol_range(ctx)); updated = updated || me.lsp_hover_state.clear(); if updated { ctx.notify(); } return; } // If hovering with cmd pressed, trigger goto definition search. if *cmd { if me.is_lsp_server_available(ctx) { me.definition_for_hovered_range(*offset, ctx); } } else { me.editor.update(ctx, |editor, ctx| { if editor.clear_hovered_symbol_range(ctx) { ctx.notify(); } }); // Queue hover request for documentation/type info (debounced). if me.is_lsp_server_available(ctx) { // Two conditions where we should early return: // 1. The active lsp hover state already contains the hovered offset. // 2. The lsp hover state is None. Meaning a later movement event cancelled the in progress hover. if me.lsp_hover_state.contains_offset(*offset) { return; } if me.lsp_hover_state.clear() { ctx.notify(); } let _ = me.hover_debounce_tx.try_send(*offset); me.lsp_hover_state = LspHoverState::Loading(None); } } } CodeEditorEvent::CommentSaved { comment } => { ctx.emit(LocalCodeEditorEvent::CommentSaved { comment: comment.clone(), }); } CodeEditorEvent::DeleteComment { id } => { ctx.emit(LocalCodeEditorEvent::DeleteComment { id: *id }); } CodeEditorEvent::RequestOpenComment(uuid) => { ctx.emit(LocalCodeEditorEvent::RequestOpenComment(*uuid)); } CodeEditorEvent::ViewportUpdated => { ctx.emit(LocalCodeEditorEvent::ViewportUpdated); } CodeEditorEvent::LayoutInvalidated => { ctx.emit(LocalCodeEditorEvent::LayoutInvalidated); } CodeEditorEvent::DelayedRenderingFlushed => { ctx.emit(LocalCodeEditorEvent::DelayedRenderingFlushed); } CodeEditorEvent::VimGotoDefinition | CodeEditorEvent::VimFindReferences | CodeEditorEvent::VimShowHover => { if me.dismiss_lsp_overlays(ctx) { ctx.notify(); } match event { CodeEditorEvent::VimGotoDefinition => me.goto_definition_at_cursor(ctx), CodeEditorEvent::VimFindReferences => me.find_references_at_cursor(ctx), CodeEditorEvent::VimShowHover => me.show_hover_at_cursor(ctx), _ => unreachable!(), } } _ => {} }); let is_new_file = matches!(diff_type, Some(DiffType::Create { .. })); // Set up debounce for hover requests let (hover_debounce_tx, hover_debounce_rx) = async_channel::unbounded(); ctx.spawn_stream_local( debounce(HOVER_DEBOUNCE_PERIOD, hover_debounce_rx), |me, offset, ctx| me.hover_for_offset(offset, ctx), |_, _| {}, ); Lines 1–500 of 2751. Load more
/// This module contains a model that can be used for loading and saving text files /// and displaying them in a code editor. /// It also handles applying an optional diff to the file content that will be applied /// when the file is loaded. use std::{ ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, time::Duration, }; use ai::diff_validation::DiffType; use futures::stream::AbortHandle; use lsp::types::FileLocation; use lsp::{ LanguageId, LanguageServerId, LspEvent, LspManagerModel, LspManagerModelEvent, LspServerModel, ReferenceLocation, }; use lsp_types::FormattingOptions; use markdown_parser::FormattedText; use num_traits::SaturatingSub; use pathfinder_color::ColorU; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::Vector2F; use remote_server::manager::RemoteServerManager; #[cfg(feature = "local_fs")] use repo_metadata::repositories::DetectedRepositories; use string_offset::CharOffset; use vec1::Vec1; use vim::vim::{MotionType, VimMode}; use warp_core::r#async::debounce; use warp_core::features::FeatureFlag; use warp_core::ui::appearance::Appearance; use warp_core::ui::icons::Icon; use warp_editor::content::buffer::InitialBufferState; use warp_editor::content::text::IndentUnit; use warp_editor::render::model::{Decoration, LineCount}; use warp_util::content_version::ContentVersion; use warp_util::file::{FileId, FileLoadError, FileSaveError}; #[cfg(feature = "local_fs")] use warp_util::local_or_remote_path::LocalOrRemotePath; use warp_util::path::to_relative_path; use warp_util::sync::Condition; use warpui::elements::{ Border, ChildAnchor, ChildView, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Rect, Shrinkable, Stack, Text, }; use warpui::keymap::FixedBinding; use warpui::keymap::macros::*; use warpui::platform::SaveFilePickerConfiguration; use warpui::text::point::Point; use warpui::ui_components::button::ButtonVariant; use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use warpui::{ AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, WindowId, }; use crate::ai::persisted_workspace::{PersistedWorkspace, PersistedWorkspaceEvent}; use crate::code::buffer_location::LocalOrRemotePath as BufferFileLocation; use crate::code::editor::EditorReviewComment; use crate::code::editor::model::HoverableLink; use crate::code::footer::{CodeFooterView, CodeFooterViewEvent}; use crate::code::global_buffer_model::{BufferState, GlobalBufferModel, GlobalBufferModelEvent}; use crate::code::{SaveOutcome, ShowFindReferencesCardProvider}; use crate::code_review::comments::CommentId; use crate::menu::{Event, Menu, MenuItem, MenuItemFields}; use crate::settings::{AISettings, CodeSettings}; use crate::terminal::TerminalView; use crate::workspace::WorkspaceAction; const DROP_SHADOW_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 48, }; const HOVER_DEBOUNCE_PERIOD: Duration = Duration::from_millis(500); /// How long to wait after the user stops typing before triggering a debounced /// auto-save. Mirrors VS Code's default `files.autoSaveDelay` of 1000ms. const AUTO_SAVE_DEBOUNCE_PERIOD: Duration = Duration::from_millis(1000); use warp_core::send_telemetry_from_ctx; use super::ImmediateSaveError; use super::diff_viewer::DiffViewer; use super::editor::scroll::{ScrollPosition, ScrollTrigger}; use super::editor::view::{CodeEditorEvent, CodeEditorView}; use super::find_references_view::{FindReferencesView, FindReferencesViewEvent}; use super::language_server_extension::ProcessedDiagnostic; use super::lsp_telemetry::LspTelemetryEvent; type SaveCallback = Box<dyn FnOnce(SaveOutcome, &mut ViewContext<LocalCodeEditorView>) + Send + Sync + 'static>; pub fn init(app: &mut AppContext) { app.register_fixed_bindings([FixedBinding::new( "cmdorctrl-l", LocalCodeEditorAction::InsertSelectedTextToInput, id!("LocalCodeEditorView") & !id!("IMEOpen"), )]); } pub enum LocalCodeEditorEvent { FileLoaded, FailedToLoad { error: Rc<FileLoadError>, }, FileSaved { /// Whether the save was triggered by auto-save. Used to suppress the /// "File saved." toast, which should only appear for manual (cmd-s) saves. auto_saved: bool, }, FailedToSave { error: Arc<FileSaveError>, }, DiffAccepted, DiffRejected, /// Emitted when a user presses Escape in Vim Normal mode inside the embedded editor. VimMinimizeRequested, /// Emitted when a user edits the file. UserEdited, /// Emitted when the diff status changes (e.g., line counts update). DiffStatusUpdated, SelectionAddedAsContext { relative_file_path: String, /// 1-indexed line range of the selection: `[start, end]` both inclusive. line_range: Range<LineCount>, /// Literal text content of the selection. selected_text: String, }, DiscardUnsavedChanges { path: PathBuf, }, GotoDefinition { path: PathBuf, line: usize, column: usize, /// The ID of the LSP server that produced this definition. /// Used to register external files with the correct server. source_server_id: LanguageServerId, }, /// Emitted when a comment is saved. This propagates the comment content /// changes to the CodeReviewView, which will update the comment model. CommentSaved { comment: EditorReviewComment, }, RequestOpenComment(CommentId), DeleteComment { id: CommentId, }, /// Emitted when the viewport is updated after layout ViewportUpdated, /// Emitted when the render state layout has been updated. LayoutInvalidated, /// Request to open LSP logs for the given file path. /// The workspace will handle opening a terminal with `tail -f` on the log file. OpenLspLogs { log_path: PathBuf, }, RunTabConfigSkill { path: PathBuf, }, DelayedRenderingFlushed, } /// Metadata about a file that is opened in the code view. #[derive(Debug, Clone)] struct LoadedFileMetadata { id: FileId, location: BufferFileLocation, } use warp_errors::report_error; pub use super::diff_viewer::DisplayMode; type TerminalTargetFn = dyn Fn(WindowId, &AppContext) -> Option<ViewHandle<TerminalView>>; struct SelectionAsContextTooltip { mouse_state: MouseStateHandle, terminal_target_fn: Box<TerminalTargetFn>, } #[derive(Debug, Clone)] pub enum LocalCodeEditorAction { InsertSelectedTextToInput, SaveFile, DiscardUnsavedChanges, NavigateToTarget(FileLocation), GotoDefinition, FindReferences, OpenContextMenu, /// Lazily fetch find-references and show the card (triggered on cmd-click when at definition). /// This is the fallback when go-to-definition has no different location to navigate to. FetchAndShowFindReferences { lsp_position: lsp::types::Location, anchor_offset: CharOffset, }, } #[derive(Default)] struct ConflictResolutionBannerMouseStates { discard_mouse_state: MouseStateHandle, overwrite_mouse_state: MouseStateHandle, } #[derive(Default)] struct ContextMenuState { mouse_state: MouseStateHandle, is_open: bool, } /// A hover content segment - either plain text/markdown or a code block. pub(super) enum HoverContentSegment { /// Plain text/markdown content (rendered with FormattedTextElement). Text(FormattedText), /// Code block with syntax highlighting (rendered with CodeEditorView). CodeBlock { view: ViewHandle<CodeEditorView> }, } /// State for the LSP hover tooltip. pub(super) enum LspHoverState { None, Loading(Option<AbortHandle>), Loaded { /// The content segments to display in the hover tooltip (from LSP hover info). segments: Vec<HoverContentSegment>, /// Diagnostics at the hovered location (displayed first, before hover info). diagnostics: Vec<ProcessedDiagnostic>, /// The offset range where the hover was triggered (used for positioning). hovered_offset_range: Range<CharOffset>, /// Scroll state for the tooltip content. scroll_state: ClippedScrollStateHandle, mouse_state: MouseStateHandle, }, } impl LspHoverState { pub(super) fn clear(&mut self) -> bool { if matches!(self, LspHoverState::None) { return false; } if let Self::Loading(Some(handle)) = self { handle.abort(); } *self = LspHoverState::None; true } pub(super) fn contains_offset(&self, offset: CharOffset) -> bool { if let LspHoverState::Loaded { hovered_offset_range, .. } = self { return hovered_offset_range.contains(&offset); } false } } pub(super) const HOVER_TOOLTIP_MAX_WIDTH: f32 = 400.; pub(super) const HOVER_TOOLTIP_MAX_HEIGHT: f32 = 100.; pub struct LocalCodeEditorView { pub(super) editor: ViewHandle<CodeEditorView>, metadata: Option<LoadedFileMetadata>, enable_diff_nav_by_default: bool, is_new_file: bool, diff_type: Option<DiffType>, selection_as_context_tooltip: Option<SelectionAsContextTooltip>, /// A marker for when the backing file has first been loaded. This is used to prevent applying /// a diff before it can be properly calculated. file_loaded: Condition, /// Whether content was changed from its base. was_edited: bool, /// Content version of the base file state. base_content_version: Option<ContentVersion>, /// Set to `true` when a `RemoteBufferConflict` event fires for this /// editor's buffer. Cleared when the user discards or overwrites. has_remote_conflict: bool, conflict_banner_mouse_states: ConflictResolutionBannerMouseStates, /// Default directory to use for save dialogs when creating new files default_directory: Option<PathBuf>, pub(super) lsp_server: Option<ModelHandle<LspServerModel>>, /// Footer for displaying LSP status. Only created for normal editing contexts, not for diff/review views. footer: Option<ViewHandle<CodeFooterView>>, /// Context menu for right-click actions. context_menu: ViewHandle<Menu<LocalCodeEditorAction>>, context_menu_state: ContextMenuState, /// Channel for debouncing hover requests. hover_debounce_tx: async_channel::Sender<CharOffset>, /// Channel for debouncing auto-save requests while the user types. auto_save_debounce_tx: async_channel::Sender<()>, /// Set while an auto-save (debounced or focus-change) is in flight so the /// resulting `FileSaved` event can be marked as an auto-save and suppress /// the success toast. auto_save_in_flight: bool, /// State for the LSP hover tooltip. pub(super) lsp_hover_state: LspHoverState, /// Pending scroll position to apply after the file is loaded. This is used when /// `set_pending_scroll` is called before the file content has finished loading /// (e.g., in the GlobalBuffer path where content loads asynchronously). pending_scroll_on_load: Option<ScrollPosition>, /// Cached processed diagnostics. Updated when diagnostics change. /// Used as source of truth for both decorations and hover display. pub(super) processed_diagnostics: Vec<ProcessedDiagnostic>, /// Decorations for LSP diagnostics (errors and warnings). pub(super) diagnostic_decorations: Vec<Decoration>, /// View for the find references feature. find_references_view: Option<ViewHandle<FindReferencesView>>, } impl LocalCodeEditorView { pub fn new( editor: ViewHandle<CodeEditorView>, diff_type: Option<DiffType>, enable_diff_nav_by_default: bool, display_mode: Option<DisplayMode>, ctx: &mut ViewContext<Self>, ) -> Self { let context_menu = ctx.add_typed_action_view(|_| { Menu::new() .prevent_interaction_with_other_elements() .with_drop_shadow() }); ctx.subscribe_to_view(&context_menu, |me, _, event, ctx| { me.handle_menu_event(event, ctx); }); ctx.subscribe_to_view(&editor, |me, _, event, ctx| match event { CodeEditorEvent::UnifiedDiffComputed => { ctx.emit(LocalCodeEditorEvent::DiffAccepted); } CodeEditorEvent::ContentChanged { origin, .. } => { me.update_diff_hunk_gutter_buttons(ctx); // Clear cached diagnostics/decorations on any content change. This is to avoid showing stale diagnostics while we are waiting for new diagnostics. if !me.processed_diagnostics.is_empty() || !me.diagnostic_decorations.is_empty() { me.clear_diagnostics(ctx); } if origin.from_user() { me.was_edited = true; ctx.emit(LocalCodeEditorEvent::UserEdited); // Queue a debounced auto-save while the user types. The // debounced path saves without running the LSP formatter, // matching VS Code's `files.autoSave: afterDelay` behavior. // A `Some` `diff_type` means this editor is showing a // pending accept/reject diff (e.g. an agent "edit-file" // proposal), which must never auto-save. Editable // code-review diffs use `diff_type = None` and stay eligible. if me.diff_type.is_none() && *CodeSettings::as_ref(ctx).auto_save { let _ = me.auto_save_debounce_tx.try_send(()); } } } CodeEditorEvent::VimEscapeInNormalMode => { if me.dismiss_lsp_overlays(ctx) { ctx.notify(); } else { ctx.emit(LocalCodeEditorEvent::VimMinimizeRequested); } } CodeEditorEvent::EscapePressed => { if me.dismiss_lsp_overlays(ctx) { ctx.notify(); } } CodeEditorEvent::DiffUpdated => { ctx.emit(LocalCodeEditorEvent::DiffStatusUpdated); } CodeEditorEvent::SelectionEnd => { ctx.notify(); } CodeEditorEvent::MouseHovered { offset, cmd, clamped, is_covered, } => { // If the mouse location is clamped (meaning it's not hovering on an actual buffer text), // or if the event is covered by an element above the editor, // we should clear the hovered range and symbol. // // However, if the mouse is over the LSP hover card itself, we should not clear // the hover state - the "covered" event is expected when hovering over the tooltip. if *clamped || *is_covered { // Check if the mouse is currently over the hover tooltip. let is_over_hover_card = if let LspHoverState::Loaded { mouse_state, .. } = &me.lsp_hover_state { mouse_state .lock() .ok() .is_some_and(|state| state.is_mouse_over_element()) } else { false }; // If the mouse is over the hover card, don't clear the hover state. if is_over_hover_card { return; } let mut updated = me .editor .update(ctx, |editor, ctx| editor.clear_hovered_symbol_range(ctx)); updated = updated || me.lsp_hover_state.clear(); if updated { ctx.notify(); } return; } // If hovering with cmd pressed, trigger goto definition search. if *cmd { if me.is_lsp_server_available(ctx) { me.definition_for_hovered_range(*offset, ctx); } } else { me.editor.update(ctx, |editor, ctx| { if editor.clear_hovered_symbol_range(ctx) { ctx.notify(); } }); // Queue hover request for documentation/type info (debounced). if me.is_lsp_server_available(ctx) { // Two conditions where we should early return: // 1. The active lsp hover state already contains the hovered offset. // 2. The lsp hover state is None. Meaning a later movement event cancelled the in progress hover. if me.lsp_hover_state.contains_offset(*offset) { return; } if me.lsp_hover_state.clear() { ctx.notify(); } let _ = me.hover_debounce_tx.try_send(*offset); me.lsp_hover_state = LspHoverState::Loading(None); } } } CodeEditorEvent::CommentSaved { comment } => { ctx.emit(LocalCodeEditorEvent::CommentSaved { comment: comment.clone(), }); } CodeEditorEvent::DeleteComment { id } => { ctx.emit(LocalCodeEditorEvent::DeleteComment { id: *id }); } CodeEditorEvent::RequestOpenComment(uuid) => { ctx.emit(LocalCodeEditorEvent::RequestOpenComment(*uuid)); } CodeEditorEvent::ViewportUpdated => { ctx.emit(LocalCodeEditorEvent::ViewportUpdated); } CodeEditorEvent::LayoutInvalidated => { ctx.emit(LocalCodeEditorEvent::LayoutInvalidated); } CodeEditorEvent::DelayedRenderingFlushed => { ctx.emit(LocalCodeEditorEvent::DelayedRenderingFlushed); } CodeEditorEvent::VimGotoDefinition | CodeEditorEvent::VimFindReferences | CodeEditorEvent::VimShowHover => { if me.dismiss_lsp_overlays(ctx) { ctx.notify(); } match event { CodeEditorEvent::VimGotoDefinition => me.goto_definition_at_cursor(ctx), CodeEditorEvent::VimFindReferences => me.find_references_at_cursor(ctx), CodeEditorEvent::VimShowHover => me.show_hover_at_cursor(ctx), _ => unreachable!(), } } _ => {} }); let is_new_file = matches!(diff_type, Some(DiffType::Create { .. })); // Set up debounce for hover requests let (hover_debounce_tx, hover_debounce_rx) = async_channel::unbounded(); ctx.spawn_stream_local( debounce(HOVER_DEBOUNCE_PERIOD, hover_debounce_rx), |me, offset, ctx| me.hover_for_offset(offset, ctx), |_, _| {}, );