2516 lines · 94.8 KB · Rust
RawDownload
use std::collections::HashMap;use std::path::{Path, PathBuf}; use lsp::LspManagerModel;use pathfinder_color::ColorU;use pathfinder_geometry::rect::RectF;use pathfinder_geometry::vector::vec2f;use warp_core::channel::{Channel, ChannelState};use warp_core::features::FeatureFlag;use warp_core::ui::appearance::Appearance;use warp_core::ui::icons::ICON_DIMENSIONS;use warp_editor::render::element::VerticalExpansionBehavior;use warp_util::path::LineAndColumnArg;#[cfg(feature = "local_fs")]use warpui::clipboard::ClipboardContent;use warpui::elements::{    AcceptedByDropTarget, Align, Border, ChildAnchor, ChildView, Clipped, ConstrainedBox,    Container, CornerRadius, CrossAxisAlignment, Draggable, DraggableState, DropTarget, Empty,    Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,    OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Rect,    SavePosition, Shrinkable, Stack, Text,};use warpui::fonts::{Properties, Style, Weight};use warpui::keymap::EditableBinding;use warpui::text::point::Point;use warpui::text_layout::ClipConfig;use warpui::ui_components::button::ButtonVariant;use warpui::ui_components::components::UiComponent;use warpui::{    AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,    ViewHandle, WindowId, id,}; use super::buffer_location::LocalOrRemotePath;use super::diff_viewer::DiffViewer;use super::editor::view::{CodeEditorEvent, CodeEditorView};use super::editor_management::{CodeManager, CodeSource};use super::local_code_editor::{LocalCodeEditorEvent, LocalCodeEditorView};use crate::code::editor::scroll::ScrollPosition;use crate::code::editor::view::CodeEditorRenderOptions;use crate::code::editor_management::CodeEditorStatus;use crate::code::global_buffer_model::GlobalBufferModel;use crate::code::local_code_editor::ShowFindReferencesCard;use crate::code::{EditorTabBarDropTargetData, ImmediateSaveError, SaveOutcome, SaveStatus};use crate::editor::InteractionState;use crate::input::Vector2F;use crate::menu::{MenuItem, MenuItemFields};use crate::notebooks::file::{MarkdownDisplayMode, renders_in_warp_notebook_viewer};use crate::pane_group::focus_state::PaneFocusHandle;use crate::pane_group::pane::view::header::components::{    CenteredHeaderEdgeWidth, render_pane_header_buttons, render_pane_header_title_text,    render_three_column_header,};use crate::pane_group::pane::view::header::render_pane_header_draggable;use crate::pane_group::pane::{ActionOrigin, PaneHeaderAction, view};use crate::pane_group::{    BackingView, CodePane, PaneConfiguration, PaneConfigurationEvent, PaneDragDropLocation,    PaneEvent, TabBarAxis,};use crate::quit_warning::UnsavedStateSummary;use crate::search::ItemHighlightState;use crate::search::files::icon::icon_from_file_path;use crate::server::telemetry::CodeContextDestination;use crate::settings::CodeSettings;use crate::tab::TAB_BAR_BORDER_HEIGHT;use crate::terminal::cli_agent::{    build_selection_line_range_prompt, build_selection_substring_prompt,};use crate::terminal::view::CliAgentRouting;use crate::ui_components::blended_colors;use crate::ui_components::buttons::icon_button;use crate::util::path::{display_name_with_host, display_path_with_host};use crate::view_components::{DismissibleToast, MarkdownToggleEvent, MarkdownToggleView};use crate::workspace::util::get_context_target_terminal_view;use crate::workspace::{ActiveSession, TabBarDropTargetData, ToastStack, WorkspaceAction};use crate::{TelemetryEvent, send_telemetry_from_ctx}; type SaveCallback =    Box<dyn FnOnce(SaveOutcome, &mut CodeView, &mut ViewContext<CodeView>) + Send + Sync + 'static>; const CLOSE_BUTTON_WIDTH: f32 = 24.;const LANGUAGE_ICON_WIDTH: f32 = 16.;const TAB_INTERNAL_MARGIN: f32 = 4.;const TAB_HORIZONTAL_MARGIN: f32 = 8.;const TAB_PADDING: f32 = 2.; // Keybinding constants - exported so AI document view can reusepub const SAVE_FILE_BINDING_NAME: &str = "code_view:save";pub const SAVE_FILE_BINDING_DESCRIPTION: &str = "Save file"; pub fn init(app: &mut AppContext) {    super::editor::view::init(app);    super::local_code_editor::init(app);     let text_entry = id!("CodeEditorView") & !id!("IMEOpen");    app.register_editable_bindings([        EditableBinding::new(            SAVE_FILE_BINDING_NAME,            SAVE_FILE_BINDING_DESCRIPTION,            CodeViewAction::SaveFile,        )        .with_context_predicate(text_entry.clone())        .with_key_binding("cmdorctrl-s"),        EditableBinding::new(            "code_view:save_as",            "Save file as",            CodeViewAction::SaveFileAs,        )        .with_context_predicate(text_entry.clone())        .with_key_binding("cmdorctrl-shift-S"),        EditableBinding::new(            "code_view:close_all_tabs",            "Close all tabs",            CodeViewAction::CloseAll,        )        .with_context_predicate(id!("CodeEditorView"))        .with_key_binding("cmdorctrl-r w"),        EditableBinding::new(            "code_view:close_saved_tabs",            "Close saved tabs",            CodeViewAction::CloseSaved,        )        .with_context_predicate(id!("CodeEditorView"))        .with_key_binding("cmdorctrl-r u"),    ]);} const PADDING: f32 = 4.; pub use crate::util::openable_file_type::is_binary_file;/// Determines the `SavePosition` ID for a draggable tab based on its index.pub fn tab_position_id(index: usize) -> String {    format!("file_tab_position_{index}")} #[derive(Debug, Clone)]enum TabBarDragPosition {    BeforeTab { index: usize },    AfterTab { index: usize },} #[derive(Debug, Clone)]pub enum CodeViewAction {    SaveFile,    SaveFileAs,    AcceptPendingDiffsAndSave,    RejectPendingDiffs,    SetCurrentTabIndex {        index: usize,    },    RemoveTabAtIndex {        index: usize,    },    CloseAll,    CloseSaved,    ToggleMaximized,    #[cfg(feature = "local_fs")]    CopyFilePath,    /// Open the active code tab's file in the platform's file manager    /// (Finder on macOS, Explorer on Windows). No-op when the active tab has    /// no resolvable local path.    #[cfg(feature = "local_fs")]    RevealInFinder,    #[cfg(feature = "local_fs")]    RenderMarkdown,    DragOverIndex {        target: usize,        drag_position: RectF,    },    DropAtIndex {        origin: usize,        target: usize,        drag_position: RectF,    },    ClearEditorTabGroupDragPositions,    ClearWorkspaceTabGroupDragPositions,} #[derive(Debug, Clone)]pub enum CodeViewEvent {    Pane(PaneEvent),    TabChanged {        location: Option<LocalOrRemotePath>,        tab_index: usize,    },    FileOpened {        location: LocalOrRemotePath,        tab_index: usize,    },    RunTabConfigSkill {        path: PathBuf,    },    OpenLspLogs {        log_path: PathBuf,    },} #[derive(Default, Clone)]struct TabDataMouseStateHandles {    tab_handle: MouseStateHandle,    close_handle: MouseStateHandle,    accept_mouse_state: MouseStateHandle,    reject_mouse_state: MouseStateHandle,    tab_draggable_state: DraggableState,} #[derive(Clone)]pub struct TabData {    location: Option<LocalOrRemotePath>,    editor_view: ViewHandle<LocalCodeEditorView>,    mouse_state_handles: TabDataMouseStateHandles,    preview: bool,} #[derive(Debug, Clone)]pub enum PendingSaveIntent {    Save,    Discard,    Cancel,} impl TabData {    /// Returns the file location (local or remote), if any.    pub fn location(&self) -> Option<&LocalOrRemotePath> {        self.location.as_ref()    }     /// Returns the local filesystem path, if this tab is backed by a local file.    /// Returns `None` for remote files and untitled tabs.    pub fn local_path(&self) -> Option<PathBuf> {        self.location            .as_ref()            .and_then(|loc| loc.to_local_path().map(Path::to_path_buf))    }} pub struct CodeView {    tab_group: Vec<TabData>,    active_tab_index: usize,    pane_configuration: ModelHandle<PaneConfiguration>,    focus_handle: Option<PaneFocusHandle>,    source: CodeSource,    window_id: WindowId,    drag_position: Option<TabBarDragPosition>,    markdown_mode_segmented_control: Option<ViewHandle<MarkdownToggleView>>,} impl CodeView {    fn new_internal(source: CodeSource, ctx: &mut ViewContext<Self>) -> Self {        let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new(""));        let window_id = ctx.window_id();         Self {            tab_group: Default::default(),            active_tab_index: 0,            pane_configuration,            focus_handle: None,            source,            window_id,            drag_position: None,            markdown_mode_segmented_control: None,        }    }     pub fn new(        source: CodeSource,        line_col: Option<LineAndColumnArg>,        ctx: &mut ViewContext<Self>,    ) -> Self {        let location = source.location();        let mut view = Self::new_internal(source, ctx);        view.open_or_focus_existing(location, line_col, ctx);        #[cfg(feature = "local_fs")]        {            view.update_markdown_mode_segmented_control(ctx);        }        view    }     #[cfg(feature = "local_fs")]    fn update_markdown_mode_segmented_control(&mut self, ctx: &mut ViewContext<Self>) {        let renders_in_notebook_viewer = self            .tab_at(self.active_tab_index)            .and_then(|t| t.location.as_ref())            .map(|loc| renders_in_warp_notebook_viewer(std::path::Path::new(&loc.display_path())))            .unwrap_or(false);         if !renders_in_notebook_viewer {            self.markdown_mode_segmented_control = None;            ctx.notify();            return;        }         if self.markdown_mode_segmented_control.is_none() {            let handle = ctx.add_typed_action_view(|ctx| {                MarkdownToggleView::new(MarkdownDisplayMode::Raw, ctx)            });             ctx.subscribe_to_view(&handle, |view, _, event, ctx| {                let MarkdownToggleEvent::ModeSelected(mode) = event;                match mode {                    MarkdownDisplayMode::Rendered => {                        view.handle_action(&CodeViewAction::RenderMarkdown, ctx);                    }                    MarkdownDisplayMode::Raw => {}                }            });             self.markdown_mode_segmented_control = Some(handle);        }         ctx.notify();    }     /// Restore a code view from a persisted multi-tab snapshot.    pub fn restore(        tabs: &[crate::app_state::CodePaneTabSnapshot],        active_tab_index: usize,        source: CodeSource,        ctx: &mut ViewContext<Self>,    ) -> Self {        let mut view = Self::new_internal(source, ctx);        for tab_snapshot in tabs {            let location = tab_snapshot.path.clone().map(LocalOrRemotePath::Local);            let tab_data = view.build_tab_data(location, false, ctx);            view.tab_group.push(tab_data);        }        let clamped_index = if view.tab_group.is_empty() {            0        } else {            active_tab_index.min(view.tab_group.len() - 1)        };        view.active_tab_index = clamped_index;        view    }     /// Create a new "preview" code view for when a user is exploring the file tree.    /// There is only one preview active at a time    pub fn new_preview(source: CodeSource, ctx: &mut ViewContext<Self>) -> Self {        let path = source.path();        let mut view = Self::new_internal(source, ctx);         if let Some(path) = path {            view.open_in_preview_or_promote(path, ctx);            #[cfg(feature = "local_fs")]            {                view.update_markdown_mode_segmented_control(ctx);            }        } else {            log::warn!("Preview CodeView constructed with no path");        }        view    }     /// If a tab is a preview, promote it and emit "FileOpened"    fn promote_if_preview(&mut self, ctx: &mut ViewContext<Self>) {        if let Some(tab) = self.tab_group.get_mut(self.active_tab_index)            && tab.preview        {            tab.preview = false;            self.set_title_after_content_update(ctx);            self.update_tab_bar_state(ctx);            self.focus_contents(ctx);            send_telemetry_from_ctx!(TelemetryEvent::PreviewPanePromoted, ctx);            ctx.notify();        }    }     /// Construct an editor backed by the global shared buffer for the given location.    ///    /// For local files, additional features are wired up (selection-as-context,    /// find-references, footer). Remote files skip these because LSP and    /// related tooling run on the local machine.    fn construct_editor_for_location(        &mut self,        location: LocalOrRemotePath,        ctx: &mut ViewContext<Self>,    ) -> ViewHandle<LocalCodeEditorView> {        let is_local = matches!(location, LocalOrRemotePath::Local(_));        ctx.add_typed_action_view(|ctx| {            let mut editor = LocalCodeEditorView::new_with_global_buffer(                location,                |buffer_state, ctx| {                    ctx.add_typed_action_view(|ctx| {                        CodeEditorView::new(                            None,                            Some(buffer_state.buffer),                            CodeEditorRenderOptions::new(VerticalExpansionBehavior::FillMaxHeight),                            ctx,                        )                        .with_horizontal_scrollbar_appearance(                            warpui::elements::new_scrollable::ScrollableAppearance::new(                                warpui::elements::ScrollbarWidth::Auto,                                true,                            ),                        )                    })                },                false,                None,                ctx,            );            if is_local {                if FeatureFlag::HoaCodeReview.is_enabled() {                    editor = editor                        .with_selection_as_context(Box::new(get_context_target_terminal_view));                }                let mut editor = editor.with_find_references_provider(                    ShowFindReferencesCard {                        editor_window_id: ctx.window_id(),                        parent_scrollable_position_id: None,                    },                    ctx,                );                editor.add_footer(ctx);                editor            } else {                editor            }        })    }     /// Construct an editor for a new (unsaved) file with no file backing.    fn construct_new_file_editor(        &mut self,        ctx: &mut ViewContext<Self>,    ) -> ViewHandle<LocalCodeEditorView> {        let editor = ctx.add_typed_action_view(|ctx| {            CodeEditorView::new(                None,                None,                CodeEditorRenderOptions::new(VerticalExpansionBehavior::FillMaxHeight),                ctx,            )            .with_horizontal_scrollbar_appearance(                warpui::elements::new_scrollable::ScrollableAppearance::new(                    warpui::elements::ScrollbarWidth::Auto,                    true,                ),            )        });         ctx.add_typed_action_view(|ctx| {            let mut local_editor = LocalCodeEditorView::new(editor, None, false, None, ctx);            if FeatureFlag::HoaCodeReview.is_enabled() {                local_editor = local_editor                    .with_selection_as_context(Box::new(get_context_target_terminal_view));            }            local_editor.with_find_references_provider(                ShowFindReferencesCard {                    editor_window_id: ctx.window_id(),                    parent_scrollable_position_id: None,                },                ctx,            )        })    }     fn build_tab_data(        &mut self,        location: Option<LocalOrRemotePath>,        preview: bool,        ctx: &mut ViewContext<Self>,    ) -> TabData {        let (code_editor, tab_location) = match location {            Some(loc) => {                let editor = self.construct_editor_for_location(loc.clone(), ctx);                (editor, Some(loc))            }            None => (self.construct_new_file_editor(ctx), None),        };         let editor = code_editor.as_ref(ctx).editor().clone();         ctx.subscribe_to_view(&editor, |me, _, event, ctx| match event {            CodeEditorEvent::Focused => {                me.promote_if_preview(ctx);                ctx.emit(CodeViewEvent::Pane(PaneEvent::FocusSelf));            }            CodeEditorEvent::ContentChanged { .. } => {                me.set_title_after_content_update(ctx);            }            _ => {}        });         // For new files (CodeSource::New), mark the editor as a new file and set default directory        if tab_location.is_none() && matches!(self.source, CodeSource::New { .. }) {            let default_directory = self.source.default_directory().cloned();            code_editor.update(ctx, |local_editor, _ctx| {                local_editor.set_new_file(true);                local_editor.set_default_directory(default_directory);            });        }         // Bundled skills cannot be edited.        if self.source.is_bundled_skill() {            editor.update(ctx, |editor, ctx| {                editor.set_interaction_state(InteractionState::Selectable, ctx);            });        }

Lines 1–500 of 2516.