Re-activate the most recently-activated project search on cmd-shift-F

Nathan Sobo created

This commits adds the beginnings of an application state facility as a non-static place to store the most recently-activated search for each project.

I also store workspace items by descending order of their entity id so that we always fetch the newest item of a given type when calling `Workspace::item_of_type`.

Change summary

crates/gpui/src/app.rs              | 63 +++++++++++++++++++++++++++++++
crates/gpui/src/presenter.rs        |  4 +
crates/search/src/project_search.rs | 55 ++++++++++++++++++++++----
crates/workspace/src/workspace.rs   | 17 ++++---
4 files changed, 122 insertions(+), 17 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -85,6 +85,8 @@ pub trait UpgradeModelHandle {
         handle: &WeakModelHandle<T>,
     ) -> Option<ModelHandle<T>>;
 
+    fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool;
+
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle>;
 }
 
@@ -608,6 +610,10 @@ impl UpgradeModelHandle for AsyncAppContext {
         self.0.borrow().upgrade_model_handle(handle)
     }
 
+    fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
+        self.0.borrow().model_handle_is_upgradable(handle)
+    }
+
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
         self.0.borrow().upgrade_any_model_handle(handle)
     }
@@ -763,6 +769,7 @@ impl MutableAppContext {
                 models: Default::default(),
                 views: Default::default(),
                 windows: Default::default(),
+                app_states: Default::default(),
                 element_states: Default::default(),
                 ref_counts: Arc::new(Mutex::new(RefCounts::default())),
                 background,
@@ -1306,6 +1313,27 @@ impl MutableAppContext {
         Ok(pending)
     }
 
+    pub fn add_app_state<T: 'static>(&mut self, state: T) {
+        self.cx
+            .app_states
+            .insert(TypeId::of::<T>(), Box::new(state));
+    }
+
+    pub fn update_app_state<T: 'static, F, U>(&mut self, update: F) -> U
+    where
+        F: FnOnce(&mut T, &mut MutableAppContext) -> U,
+    {
+        let type_id = TypeId::of::<T>();
+        let mut state = self
+            .cx
+            .app_states
+            .remove(&type_id)
+            .expect("no app state has been added for this type");
+        let result = update(state.downcast_mut().unwrap(), self);
+        self.cx.app_states.insert(type_id, state);
+        result
+    }
+
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
     where
         T: Entity,
@@ -1828,6 +1856,10 @@ impl UpgradeModelHandle for MutableAppContext {
         self.cx.upgrade_model_handle(handle)
     }
 
+    fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
+        self.cx.model_handle_is_upgradable(handle)
+    }
+
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
         self.cx.upgrade_any_model_handle(handle)
     }
@@ -1898,6 +1930,7 @@ pub struct AppContext {
     models: HashMap<usize, Box<dyn AnyModel>>,
     views: HashMap<(usize, usize), Box<dyn AnyView>>,
     windows: HashMap<usize, Window>,
+    app_states: HashMap<TypeId, Box<dyn Any>>,
     element_states: HashMap<ElementStateId, Box<dyn Any>>,
     background: Arc<executor::Background>,
     ref_counts: Arc<Mutex<RefCounts>>,
@@ -1929,6 +1962,14 @@ impl AppContext {
     pub fn platform(&self) -> &Arc<dyn Platform> {
         &self.platform
     }
+
+    pub fn app_state<T: 'static>(&self) -> &T {
+        self.app_states
+            .get(&TypeId::of::<T>())
+            .expect("no app state has been added for this type")
+            .downcast_ref()
+            .unwrap()
+    }
 }
 
 impl ReadModel for AppContext {
@@ -1956,6 +1997,10 @@ impl UpgradeModelHandle for AppContext {
         }
     }
 
+    fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
+        self.models.contains_key(&handle.model_id)
+    }
+
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
         if self.models.contains_key(&handle.model_id) {
             self.ref_counts.lock().inc_model(handle.model_id);
@@ -2361,6 +2406,10 @@ impl<M> UpgradeModelHandle for ModelContext<'_, M> {
         self.cx.upgrade_model_handle(handle)
     }
 
+    fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
+        self.cx.model_handle_is_upgradable(handle)
+    }
+
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
         self.cx.upgrade_any_model_handle(handle)
     }
@@ -2699,6 +2748,10 @@ impl<V> UpgradeModelHandle for ViewContext<'_, V> {
         self.cx.upgrade_model_handle(handle)
     }
 
+    fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
+        self.cx.model_handle_is_upgradable(handle)
+    }
+
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
         self.cx.upgrade_any_model_handle(handle)
     }
@@ -2941,6 +2994,12 @@ impl<T> PartialEq for ModelHandle<T> {
 
 impl<T> Eq for ModelHandle<T> {}
 
+impl<T> PartialEq<WeakModelHandle<T>> for ModelHandle<T> {
+    fn eq(&self, other: &WeakModelHandle<T>) -> bool {
+        self.model_id == other.model_id
+    }
+}
+
 impl<T> Hash for ModelHandle<T> {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.model_id.hash(state);
@@ -3013,6 +3072,10 @@ impl<T: Entity> WeakModelHandle<T> {
         self.model_id
     }
 
+    pub fn is_upgradable(&self, cx: &impl UpgradeModelHandle) -> bool {
+        cx.model_handle_is_upgradable(self)
+    }
+
     pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
         cx.upgrade_model_handle(self)
     }

crates/gpui/src/presenter.rs 🔗

@@ -281,6 +281,10 @@ impl<'a> UpgradeModelHandle for LayoutContext<'a> {
         self.app.upgrade_model_handle(handle)
     }
 
+    fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
+        self.app.model_handle_is_upgradable(handle)
+    }
+
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
         self.app.upgrade_any_model_handle(handle)
     }

crates/search/src/project_search.rs 🔗

@@ -1,9 +1,10 @@
 use crate::{Direction, SearchOption, SelectMatch, ToggleSearchOption};
+use collections::HashMap;
 use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
 use gpui::{
     action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
     ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
-    ViewHandle,
+    ViewHandle, WeakModelHandle,
 };
 use postage::watch;
 use project::{search::SearchQuery, Project};
@@ -14,7 +15,9 @@ use std::{
     path::PathBuf,
 };
 use util::ResultExt as _;
-use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace};
+use workspace::{
+    Item, ItemHandle, ItemNavHistory, ItemView, Settings, WeakItemViewHandle, Workspace,
+};
 
 action!(Deploy);
 action!(Search);
@@ -23,7 +26,11 @@ action!(ToggleFocus);
 
 const MAX_TAB_TITLE_LEN: usize = 24;
 
+#[derive(Default)]
+struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakModelHandle<ProjectSearch>>);
+
 pub fn init(cx: &mut MutableAppContext) {
+    cx.add_app_state(ActiveSearches::default());
     cx.add_bindings([
         Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
         Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
@@ -194,6 +201,13 @@ impl View for ProjectSearchView {
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.update_app_state(|state: &mut ActiveSearches, cx| {
+            state.0.insert(
+                self.model.read(cx).project.downgrade(),
+                self.model.downgrade(),
+            )
+        });
+
         if self.model.read(cx).match_ranges.is_empty() {
             cx.focus(&self.query_editor);
         } else {
@@ -383,11 +397,28 @@ impl ProjectSearchView {
         this
     }
 
+    // Re-activate the most recently activated search or the most recent if it has been closed.
+    // If no search exists in the workspace, create a new one.
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
-        if let Some(existing) = workspace
-            .items_of_type::<ProjectSearch>(cx)
-            .max_by_key(|existing| existing.id())
-        {
+        // Clean up entries for dropped projects
+        cx.update_app_state(|state: &mut ActiveSearches, cx| {
+            state.0.retain(|project, _| project.is_upgradable(cx))
+        });
+
+        let active_search = cx
+            .app_state::<ActiveSearches>()
+            .0
+            .get(&workspace.project().downgrade());
+
+        let existing = active_search
+            .and_then(|active_search| {
+                workspace
+                    .items_of_type::<ProjectSearch>(cx)
+                    .find(|search| search == active_search)
+            })
+            .or_else(|| workspace.item_of_type::<ProjectSearch>(cx));
+
+        if let Some(existing) = existing {
             workspace.activate_item(&existing, cx);
         } else {
             let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
@@ -515,10 +546,7 @@ impl ProjectSearchView {
                 self.focus_results_editor(cx);
             }
         } else {
-            self.query_editor.update(cx, |query_editor, cx| {
-                query_editor.select_all(&SelectAll, cx);
-            });
-            cx.focus(&self.query_editor);
+            self.focus_query_editor(cx);
         }
     }
 
@@ -532,6 +560,13 @@ impl ProjectSearchView {
         }
     }
 
+    fn focus_query_editor(&self, cx: &mut ViewContext<Self>) {
+        self.query_editor.update(cx, |query_editor, cx| {
+            query_editor.select_all(&SelectAll, cx);
+        });
+        cx.focus(&self.query_editor);
+    }
+
     fn focus_results_editor(&self, cx: &mut ViewContext<Self>) {
         self.query_editor.update(cx, |query_editor, cx| {
             let cursor = query_editor.newest_anchor_selection().head();

crates/workspace/src/workspace.rs 🔗

@@ -9,7 +9,7 @@ mod status_bar;
 use anyhow::{anyhow, Result};
 use client::{Authenticate, ChannelList, Client, User, UserStore};
 use clock::ReplicaId;
-use collections::HashSet;
+use collections::BTreeMap;
 use gpui::{
     action,
     color::Color,
@@ -36,6 +36,7 @@ pub use status_bar::StatusItemView;
 use std::{
     any::{Any, TypeId},
     cell::RefCell,
+    cmp::Reverse,
     future::Future,
     hash::{Hash, Hasher},
     path::{Path, PathBuf},
@@ -569,7 +570,7 @@ pub struct Workspace {
     status_bar: ViewHandle<StatusBar>,
     project: ModelHandle<Project>,
     path_openers: Arc<[Box<dyn PathOpener>]>,
-    items: HashSet<Box<dyn WeakItemHandle>>,
+    items: BTreeMap<Reverse<usize>, Box<dyn WeakItemHandle>>,
     _observe_current_user: Task<()>,
 }
 
@@ -815,14 +816,14 @@ impl Workspace {
 
     fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
         self.items
-            .iter()
+            .values()
             .filter_map(|i| i.upgrade(cx))
             .find(|i| i.project_path(cx).as_ref() == Some(path))
     }
 
     pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ModelHandle<T>> {
         self.items
-            .iter()
+            .values()
             .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
     }
 
@@ -831,7 +832,7 @@ impl Workspace {
         cx: &'a AppContext,
     ) -> impl 'a + Iterator<Item = ModelHandle<T>> {
         self.items
-            .iter()
+            .values()
             .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
     }
 
@@ -974,7 +975,8 @@ impl Workspace {
     where
         T: 'static + ItemHandle,
     {
-        self.items.insert(item_handle.downgrade());
+        self.items
+            .insert(Reverse(item_handle.id()), item_handle.downgrade());
         pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx))
     }
 
@@ -1068,7 +1070,8 @@ impl Workspace {
         if let Some(item) = pane.read(cx).active_item() {
             let nav_history = new_pane.read(cx).nav_history().clone();
             if let Some(clone) = item.clone_on_split(nav_history, cx.as_mut()) {
-                self.items.insert(clone.item(cx).downgrade());
+                let item = clone.item(cx).downgrade();
+                self.items.insert(Reverse(item.id()), item);
                 new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx));
             }
         }