1086 lines · 40.9 KB · Rust
RawDownload
#![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 pathfinder_color::ColorU;use warp_editor::editor::NavigationKey;use warp_editor::search::{SearchEvent, Searcher};pub use warpui::AppContext;pub use warpui::accessibility::{AccessibilityContent, WarpA11yRole};use warpui::elements::{    Align, Border, ChildAnchor, Clipped, ConstrainedBox, Container, CornerRadius,    CrossAxisAlignment, DropShadow, Element, Flex, Hoverable, MainAxisAlignment, MouseStateHandle,    OffsetPositioning, ParentAnchor, ParentOffsetBounds, Radius, Rect, SavePosition, Shrinkable,    Text,};pub use warpui::elements::{ParentElement as _, Stack};pub use warpui::geometry::vector::vec2f;use warpui::keymap::EditableBinding;use warpui::presenter::ChildView;use warpui::ui_components::components::UiComponent;use warpui::{    Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,    ViewHandle,}; use crate::appearance::Appearance;use crate::editor::{    EditorView, Event as EditorEvent, InteractionState, PropagateAndNoOpNavigationKeys,    SingleLineEditorOptions, TextOptions,};use crate::features::FeatureFlag;use crate::send_telemetry_from_ctx;use crate::server::telemetry::{FindOption, TelemetryEvent};use crate::settings::AppEditorSettings;use crate::themes::theme::Fill;use crate::ui_components::blended_colors;use crate::ui_components::icons::Icon;use crate::view_components::action_button::{ActionButton, DisabledSecondaryTheme, SecondaryTheme};use crate::view_components::find::FindDirection; pub const FIND_BAR_WIDTH: f32 = 500.;const ICON_PADDING: f32 = 4.;const HORIZONTAL_ICON_SPACING: f32 = 4.;const ICON_CONTAINER_CORNER_RADIUS: f32 = 4.;pub const FIND_BAR_PADDING: f32 = 4.;const FIND_EDITOR_PADDING: f32 = 6.;pub const FIND_EDITOR_BORDER_RADIUS: f32 = 6.;const FIND_EDITOR_BORDER_WIDTH: f32 = 1.;const FIND_EDITOR_FONT_SIZE: f32 = 12.;const FIND_EDITOR_ROW_SPACING: f32 = 4.; pub const REGEX_TOGGLE_TOOLTIP: &str = "Regex toggle";pub const CASE_SENSITIVE_TOOLTIP: &str = "Case sensitive search";pub const PRESERVE_CASE_TOOLTIP: &str = "Preserve case";pub const FIND_PLACEHOLDER_TEXT: &str = "Find";pub const REPLACE_PLACEHOLDER_TEXT: &str = "Replace"; #[derive(Default)]struct ButtonMouseStates {    match_up: MouseStateHandle,    match_down: MouseStateHandle,    close: MouseStateHandle,    toggle_case_sensitivity: MouseStateHandle,    toggle_regex_search: MouseStateHandle,    toggle_replace_open: MouseStateHandle,    toggle_preserve_case: MouseStateHandle,} #[derive(Debug)]pub enum Event {    CloseFindBar,    Update { query: Option<String> },    NextMatch { direction: FindDirection },    SelectAll,    ReplaceSelected,    ReplaceAll,    VimEnterAndFocusEditor,} pub struct CodeEditorFind {    find_editor: ViewHandle<EditorView>,    replace_editor: ViewHandle<EditorView>,    searcher: ModelHandle<Searcher>,    button_mouse_states: ButtonMouseStates,    find_editor_mouse_state: MouseStateHandle,    /// Save position of the find input, so its bounds can be resolved for mouse events.    find_editor_position_id: String,    preserve_case_enabled: bool,    is_open: bool,    is_replace_open: bool,    select_all_button: ViewHandle<ActionButton>,    replace_all_button: ViewHandle<ActionButton>,} #[derive(Copy, Clone, Debug)]pub enum FindAction {    Up,    Down,    Close,    ToggleCaseSensitivity,    ToggleRegexSearch,    CmdG,    CmdShiftG,    SelectAll,    ToggleReplaceOpen,    ReplaceAll,    TogglePreserveCase,    /// The find input was clicked, so it should become editable and take focus again.    FocusFindInput,} pub fn init(app: &mut AppContext) {    use warpui::keymap::macros::*;    app.register_editable_bindings([        EditableBinding::new(            "find:find_next_occurrence",            "Find the next occurrence of your search query",            FindAction::CmdG,        )        .with_context_predicate(id!("CodeEditorFind"))        // Both Intellij and VSCode use f3/shift-f3 to navigate find occurrences on windows / linux.        // See https://www.jetbrains.com/help/idea/reference-keymap-win-default.html#find_everything.        .with_mac_key_binding("cmd-g")        .with_linux_or_windows_key_binding("f3"),        EditableBinding::new(            "find:find_prev_occurrence",            "Find the previous occurrence of your search query",            FindAction::CmdShiftG,        )        .with_context_predicate(id!("CodeEditorFind"))        .with_mac_key_binding("cmd-shift-G")        .with_linux_or_windows_key_binding("shift-f3"),    ])} impl CodeEditorFind {    pub fn new(searcher: ModelHandle<Searcher>, ctx: &mut ViewContext<Self>) -> Self {        let find_editor = ctx.add_typed_action_view(|ctx| {            let appearance = Appearance::as_ref(ctx);            let mut editor = EditorView::single_line(                SingleLineEditorOptions {                    text: TextOptions::ui_text(Some(FIND_EDITOR_FONT_SIZE), appearance),                    select_all_on_focus: true,                    clear_selections_on_blur: true,                    propagate_and_no_op_vertical_navigation_keys:                        PropagateAndNoOpNavigationKeys::Always,                    ..Default::default()                },                ctx,            );            editor.set_placeholder_text(FIND_PLACEHOLDER_TEXT, ctx);            editor        });         let replace_editor = ctx.add_typed_action_view(|ctx| {            let appearance = Appearance::as_ref(ctx);            let mut replace_editor = EditorView::single_line(                SingleLineEditorOptions {                    text: TextOptions::ui_text(Some(FIND_EDITOR_FONT_SIZE), appearance),                    select_all_on_focus: true,                    clear_selections_on_blur: true,                    propagate_and_no_op_vertical_navigation_keys:                        PropagateAndNoOpNavigationKeys::Always,                    ..Default::default()                },                ctx,            );            replace_editor.set_placeholder_text(REPLACE_PLACEHOLDER_TEXT, ctx);            replace_editor        });         // Get the line height from the find editor's font metrics        let line_height = find_editor            .as_ref(ctx)            .line_height(ctx.font_cache(), Appearance::as_ref(ctx));        // Total editor height is:        // - line_height: The height of a single line of text        // - 2 * FIND_EDITOR_PADDING: Padding above and below the text (6px * 2 = 12px)        // - 5px: Additional spacing to account for button border width        let editor_height = line_height + (2. * FIND_EDITOR_PADDING) + 5.;         let select_all_button = ctx.add_typed_action_view(|_ctx| {            ActionButton::new("Select all", SecondaryTheme)                .on_click(|ctx| {                    ctx.dispatch_typed_action(FindAction::SelectAll);                })                .with_width(72.)                .with_height(editor_height)                .with_disabled_theme(DisabledSecondaryTheme)        });         let replace_all_button = ctx.add_typed_action_view(|ctx| {            let mut button = ActionButton::new("Replace all", SecondaryTheme)                .on_click(|ctx| {                    ctx.dispatch_typed_action(FindAction::ReplaceAll);                })                .with_width(72.)                .with_height(editor_height)                .with_disabled_theme(DisabledSecondaryTheme);            button.set_disabled(true, ctx);            button        });         ctx.subscribe_to_view(&find_editor, move |me, _, event, ctx| {            me.handle_find_editor_event(event, ctx);        });         ctx.subscribe_to_view(&replace_editor, move |me, _, event, ctx| {            me.handle_replace_editor_event(event, ctx);        });         ctx.subscribe_to_model(&searcher, |me, _, event, ctx| {            // Handle search result updates. The searcher emits three events:            // - Updated: Search results have changed, handled here to update the select all button            // - InvalidQuery: The search query is invalid (e.g. malformed regex), TODO: show error tooltip            // - SelectedResultChanged: The selected match changed, handled by the editor view            match event {                SearchEvent::Updated => {                    let has_matches = me.searcher.as_ref(ctx).match_count() > 0;                    me.select_all_button.update(ctx, |button, ctx| {                        button.set_disabled(!has_matches, ctx);                    });                    ctx.notify();                }                SearchEvent::SelectedResultChanged => {                    // The selected match index changed (e.g., via Vim n/N), needed to re-render index label.                    ctx.notify();                }                SearchEvent::InvalidQuery => {}            }        });         Self {            find_editor,            replace_editor,            searcher,            button_mouse_states: Default::default(),            find_editor_mouse_state: Default::default(),            find_editor_position_id: format!("code_editor_find_query_{}", ctx.view_id()),            preserve_case_enabled: false,            is_open: false,            is_replace_open: false,            select_all_button,            replace_all_button,        }    }     pub fn set_open(&mut self, is_open: bool) {        self.is_open = is_open;        // Replace menu doesn't persist open state        if !is_open {            self.is_replace_open = false;        }    }     pub fn is_open(&self) -> bool {        self.is_open    }     pub fn is_preserve_case_enabled(&self) -> bool {        self.preserve_case_enabled    }     pub fn replace_query(&self, ctx: &mut ViewContext<Self>) -> String {        self.replace_editor.as_ref(ctx).buffer_text(ctx)    }     pub fn set_find_query(&self, ctx: &mut ViewContext<Self>, query: &str) {        self.find_editor.update(ctx, |editor, ctx| {            editor.set_buffer_text(query, ctx);        });    }     /// Returns true if the find input is currently editable.    pub fn is_find_input_editable(&self, app: &AppContext) -> bool {        self.find_editor.as_ref(app).can_edit(app)    }     /// Enable or disable the find input editor's interactivity.    pub fn set_find_input_editable(&self, ctx: &mut ViewContext<Self>, is_editable: bool) {        self.find_editor.update(ctx, |editor, ctx| {            let state = if is_editable {                InteractionState::Editable            } else {                InteractionState::Disabled            };            editor.set_interaction_state(state, ctx);        });    }     /// Makes the find input editable, selects its current query, and focuses it.    fn activate_find_input(&mut self, ctx: &mut ViewContext<Self>) {        self.find_editor.update(ctx, |editor, ctx| {            editor.set_interaction_state(InteractionState::Editable, ctx);            editor.select_all(ctx);        });        ctx.focus(&self.find_editor);        ctx.notify();    }     fn handle_find_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext<Self>) {        match event {            EditorEvent::Edited(_) => {                let query = self.find_editor.as_ref(ctx).buffer_text(ctx);                ctx.emit(Event::Update {                    // If the query is empty, don't search for an empty string - set the query to                    // `None`.                    query: (!query.is_empty()).then_some(query),                });                 self.update_replace_button_state(ctx);                self.emit_result_a11y_content(ctx);                ctx.notify();            }            EditorEvent::Enter => {                let vim_enabled = FeatureFlag::VimCodeEditor.is_enabled()                    && AppEditorSettings::as_ref(ctx).vim_mode_enabled();                 if !vim_enabled {                    self.focus_next_match(FindDirection::Down, ctx);                } else {                    // Vim: treat "enter" as ending the search query entry and shift focus back to the editor                    self.find_editor.update(ctx, |editor, ctx| {                        editor.clear_selections(ctx);                        editor.set_interaction_state(InteractionState::Disabled, ctx);                    });                    ctx.emit(Event::VimEnterAndFocusEditor);                }            }            EditorEvent::ShiftEnter | EditorEvent::AltEnter => {                let vim_enabled = FeatureFlag::VimCodeEditor.is_enabled()                    && AppEditorSettings::as_ref(ctx).vim_mode_enabled();                if !vim_enabled {                    self.focus_next_match(FindDirection::Up, ctx);                }            }            EditorEvent::Escape => {                self.close_find_bar(ctx);            }            EditorEvent::Navigate(NavigationKey::Tab) => {                // If replace editor is currently open and the user presses 'tab', focus on the find editor                if self.is_replace_open {                    ctx.focus(&self.replace_editor);                }            }            _ => {}        }    }     fn handle_replace_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext<Self>) {        match event {            EditorEvent::Edited(_) => {                self.update_replace_button_state(ctx);            }            EditorEvent::Enter => {                ctx.emit(Event::ReplaceSelected);            }            EditorEvent::Escape => {                self.close_find_bar(ctx);            }            // If the user is focused on the replace editor and presses 'tab', focus should shift back to the find editor            EditorEvent::Navigate(NavigationKey::Tab) => ctx.focus(&self.find_editor),            _ => (),        }    }     fn update_replace_button_state(&mut self, ctx: &mut ViewContext<Self>) {        // Enable the replace all button only if there is replacement text and at least one match found        let (has_replace_query, has_match) = (            !self.replace_query(ctx).is_empty(),            self.searcher.as_ref(ctx).match_count() > 0,        );        let should_enable_replace = has_replace_query && has_match;        self.replace_all_button.update(ctx, |button, ctx| {            button.set_disabled(!should_enable_replace, ctx);        });    }     fn focus_next_match(&mut self, direction: FindDirection, ctx: &mut ViewContext<Self>) {        // All of the actual update logic for the selected match goes through the event codepath        // but for some reason the logic for updating the match index happens below.        ctx.emit(Event::NextMatch { direction });         self.emit_result_a11y_content(ctx);        ctx.notify();    }     /// Emits the a11y announcement informing about the current match/result.    /// Note that it's done outside of the regular action_accessibility_contents flow,    /// as `focus_next_match` may have multiple entrypoints (that are not Action).    pub fn emit_result_a11y_content(&mut self, ctx: &mut ViewContext<Self>) {        let content = if let Some(match_index) = self.searcher.as_ref(ctx).selected_match() {            AccessibilityContent::new(                format!(                    "Result {} of {}.",                    match_index + 1,                    self.searcher.as_ref(ctx).match_count()                ),                "Use enter and shift-enter to navigate between matches. Escape to quit.",                WarpA11yRole::UserAction,            )        } else {            AccessibilityContent::new_without_help("No results.", WarpA11yRole::UserAction)        };        ctx.emit_a11y_content(content);    }     /// Emits the a11y announcement informing about replace operation results.    /// Called when Enter is pressed in the replace field to provide feedback on the operation.    pub fn emit_replace_a11y_content(&mut self, ctx: &mut ViewContext<Self>) {        let content = if let Some(match_index) = self.searcher.as_ref(ctx).selected_match() {            let remaining_matches = self.searcher.as_ref(ctx).match_count();            AccessibilityContent::new(                format!(                    "Successfully replaced match. Selected match is {match_index} of {remaining_matches}"                ),                "Continue pressing Enter to replace more matches, or use up/down arrows to navigate.",                WarpA11yRole::UserAction,            )        } else {            AccessibilityContent::new_without_help(                "Successfully replaced the last match.",                WarpA11yRole::UserAction,            )        };        ctx.emit_a11y_content(content);    }     fn close_find_bar(&mut self, ctx: &mut ViewContext<Self>) {        ctx.emit(Event::CloseFindBar);    }     fn toggle_case_sensitivity(&mut self, ctx: &mut ViewContext<Self>) {        let new_case_sensitive = !self.searcher.as_ref(ctx).is_case_sensitive();        self.searcher.update(ctx, |searcher, ctx| {            searcher.set_case_sensitive(new_case_sensitive, ctx);        });        send_telemetry_from_ctx!(            TelemetryEvent::ToggleFindOption {                option: FindOption::CaseSensitive,                enabled: new_case_sensitive            },            ctx        );    }     fn toggle_regex_search(&mut self, ctx: &mut ViewContext<Self>) {        let new_regex_enabled = !self.searcher.as_ref(ctx).is_regex();        self.searcher.update(ctx, |searcher, ctx| {            searcher.set_regex(new_regex_enabled, ctx);        });        send_telemetry_from_ctx!(            TelemetryEvent::ToggleFindOption {                option: FindOption::Regex,                enabled: new_regex_enabled            },            ctx        );    }     fn render_match_index(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {        // If there is some match index, we add 1 to it since the UI is 1-indexed        // (i.e. first match starts at index 1 out of the total number of matches).        let index = match self.searcher.as_ref(app).selected_match() {            None => 0,            Some(idx) => idx + 1,        };        let label = format!(            "{}/{}",            if index > 0 {                index.to_string()            } else {                "?".to_string()            },            self.searcher.as_ref(app).match_count()        );        Text::new_inline(label, appearance.ui_font_family(), FIND_EDITOR_FONT_SIZE)            .with_color(blended_colors::text_sub(                appearance.theme(),                appearance.theme().surface_1(),            ))            .finish()    }     #[allow(clippy::too_many_arguments)]    pub fn render_hoverable_icon_in_editor(        &self,        appearance: &Appearance,        icon: Icon,        is_selected: bool,        mouse_state_handle: MouseStateHandle,        on_click_action: FindAction,        size: f32,        tooltip_text: Option<&str>,        right_margin: f32,    ) -> Box<dyn Element> {        Hoverable::new(mouse_state_handle, |state| {            let (border, background) = if is_selected {                (                    Border::all(1.).with_border_fill(appearance.theme().accent()),                    appearance.theme().find_bar_button_selection_color(),

Lines 1–500 of 1086.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t