3721 lines · 139.9 KB · Rust
RawDownload
use crate::{    buffer_search::Deploy, BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery,    ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,    ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord,};use collections::{HashMap, HashSet};use editor::{    actions::SelectAll,    items::active_match_index,    scroll::{Autoscroll, Axis},    Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer,    MAX_TAB_TITLE_LEN,};use futures::StreamExt;use gpui::{    actions, div, Action, AnyElement, AnyView, AppContext, Context as _, EntityId, EventEmitter,    FocusHandle, FocusableView, Global, Hsla, InteractiveElement, IntoElement, KeyContext, Model,    ModelContext, ParentElement, Point, Render, SharedString, Styled, Subscription, Task,    TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext,};use language::Buffer;use menu::Confirm;use project::{    search::{SearchInputKind, SearchQuery},    search_history::SearchHistoryCursor,    Project, ProjectPath,};use settings::Settings;use std::{    any::{Any, TypeId},    mem,    ops::{Not, Range},    path::Path,};use theme::ThemeSettings;use ui::{    h_flex, prelude::*, utils::SearchInputWidth, v_flex, Icon, IconButton, IconButtonShape,    IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip,};use util::paths::PathMatcher;use workspace::{    item::{BreadcrumbText, Item, ItemEvent, ItemHandle},    searchable::{Direction, SearchableItem, SearchableItemHandle},    DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,    ToolbarItemView, Workspace, WorkspaceId,}; actions!(    project_search,    [SearchInNew, ToggleFocus, NextField, ToggleFilters]); #[derive(Default)]struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>); impl Global for ActiveSettings {} pub fn init(cx: &mut AppContext) {    cx.set_global(ActiveSettings::default());    cx.observe_new_views(|workspace: &mut Workspace, _cx| {        register_workspace_action(workspace, move |search_bar, _: &Deploy, cx| {            search_bar.focus_search(cx);        });        register_workspace_action(workspace, move |search_bar, _: &FocusSearch, cx| {            search_bar.focus_search(cx);        });        register_workspace_action(workspace, move |search_bar, _: &ToggleFilters, cx| {            search_bar.toggle_filters(cx);        });        register_workspace_action(workspace, move |search_bar, _: &ToggleCaseSensitive, cx| {            search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);        });        register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, cx| {            search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);        });        register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, cx| {            search_bar.toggle_search_option(SearchOptions::REGEX, cx);        });        register_workspace_action(workspace, move |search_bar, action: &ToggleReplace, cx| {            search_bar.toggle_replace(action, cx)        });        register_workspace_action(            workspace,            move |search_bar, action: &SelectPrevMatch, cx| {                search_bar.select_prev_match(action, cx)            },        );        register_workspace_action(            workspace,            move |search_bar, action: &SelectNextMatch, cx| {                search_bar.select_next_match(action, cx)            },        );         // Only handle search_in_new if there is a search present        register_workspace_action_for_present_search(workspace, |workspace, action, cx| {            ProjectSearchView::search_in_new(workspace, action, cx)        });         // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor.        workspace.register_action(move |workspace, action: &DeploySearch, cx| {            if workspace.has_active_modal(cx) {                cx.propagate();                return;            }            ProjectSearchView::deploy_search(workspace, action, cx);            cx.notify();        });        workspace.register_action(move |workspace, action: &NewSearch, cx| {            if workspace.has_active_modal(cx) {                cx.propagate();                return;            }            ProjectSearchView::new_search(workspace, action, cx);            cx.notify();        });    })    .detach();} fn is_contains_uppercase(str: &str) -> bool {    str.chars().any(|c| c.is_uppercase())} pub struct ProjectSearch {    project: Model<Project>,    excerpts: Model<MultiBuffer>,    pending_search: Option<Task<Option<()>>>,    match_ranges: Vec<Range<Anchor>>,    active_query: Option<SearchQuery>,    last_search_query_text: Option<String>,    search_id: usize,    no_results: Option<bool>,    limit_reached: bool,    search_history_cursor: SearchHistoryCursor,    search_included_history_cursor: SearchHistoryCursor,    search_excluded_history_cursor: SearchHistoryCursor,} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]enum InputPanel {    Query,    Exclude,    Include,} pub struct ProjectSearchView {    workspace: WeakView<Workspace>,    focus_handle: FocusHandle,    model: Model<ProjectSearch>,    query_editor: View<Editor>,    replacement_editor: View<Editor>,    results_editor: View<Editor>,    search_options: SearchOptions,    panels_with_errors: HashSet<InputPanel>,    active_match_index: Option<usize>,    search_id: usize,    included_files_editor: View<Editor>,    excluded_files_editor: View<Editor>,    filters_enabled: bool,    replace_enabled: bool,    included_opened_only: bool,    _subscriptions: Vec<Subscription>,} #[derive(Debug, Clone)]pub struct ProjectSearchSettings {    search_options: SearchOptions,    filters_enabled: bool,} pub struct ProjectSearchBar {    active_project_search: Option<View<ProjectSearchView>>,    subscription: Option<Subscription>,} impl ProjectSearch {    pub fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {        let capability = project.read(cx).capability();         Self {            project,            excerpts: cx.new_model(|_| MultiBuffer::new(capability)),            pending_search: Default::default(),            match_ranges: Default::default(),            active_query: None,            last_search_query_text: None,            search_id: 0,            no_results: None,            limit_reached: false,            search_history_cursor: Default::default(),            search_included_history_cursor: Default::default(),            search_excluded_history_cursor: Default::default(),        }    }     fn clone(&self, cx: &mut ModelContext<Self>) -> Model<Self> {        cx.new_model(|cx| Self {            project: self.project.clone(),            excerpts: self                .excerpts                .update(cx, |excerpts, cx| cx.new_model(|cx| excerpts.clone(cx))),            pending_search: Default::default(),            match_ranges: self.match_ranges.clone(),            active_query: self.active_query.clone(),            last_search_query_text: self.last_search_query_text.clone(),            search_id: self.search_id,            no_results: self.no_results,            limit_reached: self.limit_reached,            search_history_cursor: self.search_history_cursor.clone(),            search_included_history_cursor: self.search_included_history_cursor.clone(),            search_excluded_history_cursor: self.search_excluded_history_cursor.clone(),        })    }    fn cursor(&self, kind: SearchInputKind) -> &SearchHistoryCursor {        match kind {            SearchInputKind::Query => &self.search_history_cursor,            SearchInputKind::Include => &self.search_included_history_cursor,            SearchInputKind::Exclude => &self.search_excluded_history_cursor,        }    }    fn cursor_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistoryCursor {        match kind {            SearchInputKind::Query => &mut self.search_history_cursor,            SearchInputKind::Include => &mut self.search_included_history_cursor,            SearchInputKind::Exclude => &mut self.search_excluded_history_cursor,        }    }     fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {        let search = self.project.update(cx, |project, cx| {            project                .search_history_mut(SearchInputKind::Query)                .add(&mut self.search_history_cursor, query.as_str().to_string());            let included = query.as_inner().files_to_include().sources().join(",");            if !included.is_empty() {                project                    .search_history_mut(SearchInputKind::Include)                    .add(&mut self.search_included_history_cursor, included);            }            let excluded = query.as_inner().files_to_exclude().sources().join(",");            if !excluded.is_empty() {                project                    .search_history_mut(SearchInputKind::Exclude)                    .add(&mut self.search_excluded_history_cursor, excluded);            }            project.search(query.clone(), cx)        });        self.last_search_query_text = Some(query.as_str().to_string());        self.search_id += 1;        self.active_query = Some(query);        self.match_ranges.clear();        self.pending_search = Some(cx.spawn(|this, mut cx| async move {            let mut matches = search.ready_chunks(1024);            let this = this.upgrade()?;            this.update(&mut cx, |this, cx| {                this.match_ranges.clear();                this.excerpts.update(cx, |this, cx| this.clear(cx));                this.no_results = Some(true);                this.limit_reached = false;            })            .ok()?;             let mut limit_reached = false;            while let Some(results) = matches.next().await {                let mut buffers_with_ranges = Vec::with_capacity(results.len());                for result in results {                    match result {                        project::search::SearchResult::Buffer { buffer, ranges } => {                            buffers_with_ranges.push((buffer, ranges));                        }                        project::search::SearchResult::LimitReached => {                            limit_reached = true;                        }                    }                }                 let match_ranges = this                    .update(&mut cx, |this, cx| {                        this.excerpts.update(cx, |excerpts, cx| {                            excerpts.push_multiple_excerpts_with_context_lines(                                buffers_with_ranges,                                editor::DEFAULT_MULTIBUFFER_CONTEXT,                                cx,                            )                        })                    })                    .ok()?                    .await;                 this.update(&mut cx, |this, cx| {                    this.match_ranges.extend(match_ranges);                    cx.notify();                })                .ok()?;            }             this.update(&mut cx, |this, cx| {                if !this.match_ranges.is_empty() {                    this.no_results = Some(false);                }                this.limit_reached = limit_reached;                this.pending_search.take();                cx.notify();            })            .ok()?;             None        }));        cx.notify();    }} #[derive(Clone, Debug, PartialEq, Eq)]pub enum ViewEvent {    UpdateTab,    Activate,    EditorEvent(editor::EditorEvent),    Dismiss,} impl EventEmitter<ViewEvent> for ProjectSearchView {} impl Render for ProjectSearchView {    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {        if self.has_matches() {            div()                .flex_1()                .size_full()                .track_focus(&self.focus_handle(cx))                .child(self.results_editor.clone())        } else {            let model = self.model.read(cx);            let has_no_results = model.no_results.unwrap_or(false);            let is_search_underway = model.pending_search.is_some();             let heading_text = if is_search_underway {                "Searching…"            } else if has_no_results {                "No Results"            } else {                "Search All Files"            };             let heading_text = div()                .justify_center()                .child(Label::new(heading_text).size(LabelSize::Large));             let page_content: Option<AnyElement> = if let Some(no_results) = model.no_results {                if model.pending_search.is_none() && no_results {                    Some(                        Label::new("No results found in this project for the provided query")                            .size(LabelSize::Small)                            .into_any_element(),                    )                } else {                    None                }            } else {                Some(self.landing_text_minor(cx).into_any_element())            };             let page_content = page_content.map(|text| div().child(text));             v_flex()                .size_full()                .items_center()                .justify_center()                .overflow_hidden()                .bg(cx.theme().colors().editor_background)                .track_focus(&self.focus_handle(cx))                .child(                    v_flex()                        .id("project-search-landing-page")                        .overflow_y_scroll()                        .max_w_80()                        .gap_1()                        .child(heading_text)                        .children(page_content),                )        }    }} impl FocusableView for ProjectSearchView {    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {        self.focus_handle.clone()    }} impl Item for ProjectSearchView {    type Event = ViewEvent;    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {        let query_text = self.query_editor.read(cx).text(cx);         query_text            .is_empty()            .not()            .then(|| query_text.into())            .or_else(|| Some("Project Search".into()))    }     fn act_as_type<'a>(        &'a self,        type_id: TypeId,        self_handle: &'a View<Self>,        _: &'a AppContext,    ) -> Option<AnyView> {        if type_id == TypeId::of::<Self>() {            Some(self_handle.clone().into())        } else if type_id == TypeId::of::<Editor>() {            Some(self.results_editor.clone().into())        } else {            None        }    }     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {        self.results_editor            .update(cx, |editor, cx| editor.deactivated(cx));    }     fn tab_icon(&self, _cx: &WindowContext) -> Option<Icon> {        Some(Icon::new(IconName::MagnifyingGlass))    }     fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {        let last_query: Option<SharedString> = self            .model            .read(cx)            .last_search_query_text            .as_ref()            .map(|query| {                let query = query.replace('\n', "");                let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN);                query_text.into()            });        Some(            last_query                .filter(|query| !query.is_empty())                .unwrap_or_else(|| "Project Search".into()),        )    }     fn telemetry_event_text(&self) -> Option<&'static str> {        Some("project search")    }     fn for_each_project_item(        &self,        cx: &AppContext,        f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),    ) {        self.results_editor.for_each_project_item(cx, f)    }     fn is_singleton(&self, _: &AppContext) -> bool {        false    }     fn can_save(&self, _: &AppContext) -> bool {        true    }     fn is_dirty(&self, cx: &AppContext) -> bool {        self.results_editor.read(cx).is_dirty(cx)    }     fn has_conflict(&self, cx: &AppContext) -> bool {        self.results_editor.read(cx).has_conflict(cx)    }     fn save(        &mut self,        format: bool,        project: Model<Project>,        cx: &mut ViewContext<Self>,    ) -> Task<anyhow::Result<()>> {        self.results_editor            .update(cx, |editor, cx| editor.save(format, project, cx))    }     fn save_as(        &mut self,        _: Model<Project>,        _: ProjectPath,        _: &mut ViewContext<Self>,    ) -> Task<anyhow::Result<()>> {        unreachable!("save_as should not have been called")    }     fn reload(        &mut self,        project: Model<Project>,        cx: &mut ViewContext<Self>,    ) -> Task<anyhow::Result<()>> {        self.results_editor            .update(cx, |editor, cx| editor.reload(project, cx))    } 

Lines 1–500 of 3721.