2751 lines · 108.4 KB · Rust
RawDownload
/// 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.