Sort tasks modal entries by last used time

Kirill Bulatov created

Change summary

crates/project/src/task_inventory.rs | 78 +++++++++++++++++++++++++----
crates/tasks_ui/src/lib.rs           |  2 
crates/tasks_ui/src/modal.rs         | 34 +++++-------
3 files changed, 82 insertions(+), 32 deletions(-)

Detailed changes

crates/project/src/task_inventory.rs 🔗

@@ -2,13 +2,16 @@
 
 use std::{any::TypeId, path::Path, sync::Arc};
 
+use collections::{HashMap, VecDeque};
 use gpui::{AppContext, Context, Model, ModelContext, Subscription};
+use itertools::Itertools;
 use task::{Source, Task, TaskId};
+use util::post_inc;
 
 /// Inventory tracks available tasks for a given project.
 pub struct Inventory {
     sources: Vec<SourceInInventory>,
-    pub last_scheduled_task: Option<TaskId>,
+    last_scheduled_tasks: VecDeque<TaskId>,
 }
 
 struct SourceInInventory {
@@ -21,7 +24,7 @@ impl Inventory {
     pub(crate) fn new(cx: &mut AppContext) -> Model<Self> {
         cx.new_model(|_| Self {
             sources: Vec::new(),
-            last_scheduled_task: None,
+            last_scheduled_tasks: VecDeque::new(),
         })
     }
 
@@ -39,6 +42,7 @@ impl Inventory {
         self.sources.push(source);
         cx.notify();
     }
+
     pub fn source<T: Source>(&self) -> Option<Model<Box<dyn Source>>> {
         let target_type_id = std::any::TypeId::of::<T>();
         self.sources.iter().find_map(
@@ -55,25 +59,75 @@ impl Inventory {
     }
 
     /// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path).
-    pub fn list_tasks(&self, path: Option<&Path>, cx: &mut AppContext) -> Vec<Arc<dyn Task>> {
-        let mut tasks = Vec::new();
-        for source in &self.sources {
-            tasks.extend(
+    pub fn list_tasks(
+        &self,
+        path: Option<&Path>,
+        lru: bool,
+        cx: &mut AppContext,
+    ) -> Vec<Arc<dyn Task>> {
+        let mut lru_score = 0_u32;
+        let tasks_by_usage = if lru {
+            self.last_scheduled_tasks
+                .iter()
+                .rev()
+                .fold(HashMap::default(), |mut tasks, id| {
+                    tasks.entry(id).or_insert_with(|| post_inc(&mut lru_score));
+                    tasks
+                })
+        } else {
+            HashMap::default()
+        };
+        self.sources
+            .iter()
+            .flat_map(|source| {
                 source
                     .source
-                    .update(cx, |source, cx| source.tasks_for_path(path, cx)),
-            );
-        }
-        tasks
+                    .update(cx, |source, cx| source.tasks_for_path(path, cx))
+            })
+            .map(|task| {
+                let usages = if lru {
+                    tasks_by_usage
+                        .get(&task.id())
+                        .copied()
+                        .unwrap_or_else(|| post_inc(&mut lru_score))
+                } else {
+                    post_inc(&mut lru_score)
+                };
+                (task, usages)
+            })
+            .sorted_unstable_by(|(task_a, usages_a), (task_b, usages_b)| {
+                usages_a
+                    .cmp(usages_b)
+                    .then(task_a.name().cmp(task_b.name()))
+            })
+            .map(|(task, _)| task)
+            .collect()
     }
 
     /// Returns the last scheduled task, if any of the sources contains one with the matching id.
     pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<Arc<dyn Task>> {
-        self.last_scheduled_task.as_ref().and_then(|id| {
+        self.last_scheduled_tasks.back().and_then(|id| {
             // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future.
-            self.list_tasks(None, cx)
+            self.list_tasks(None, false, cx)
                 .into_iter()
                 .find(|task| task.id() == id)
         })
     }
+
+    pub fn task_scheduled(&mut self, id: TaskId) {
+        self.last_scheduled_tasks.push_back(id);
+        if self.last_scheduled_tasks.len() > 5_000 {
+            self.last_scheduled_tasks.pop_front();
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn todo_kb() {
+        todo!("TODO kb LRU tests")
+    }
 }

crates/tasks_ui/src/lib.rs 🔗

@@ -41,7 +41,7 @@ fn schedule_task(workspace: &Workspace, task: &dyn Task, cx: &mut ViewContext<'_
     if let Some(spawn_in_terminal) = spawn_in_terminal {
         workspace.project().update(cx, |project, cx| {
             project.task_inventory().update(cx, |inventory, _| {
-                inventory.last_scheduled_task = Some(task.id().clone());
+                inventory.task_scheduled(task.id().clone());
             })
         });
         cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));

crates/tasks_ui/src/modal.rs 🔗

@@ -24,7 +24,7 @@ pub(crate) struct TasksModalDelegate {
     matches: Vec<StringMatch>,
     selected_index: usize,
     workspace: WeakView<Workspace>,
-    last_prompt: String,
+    prompt: String,
 }
 
 impl TasksModalDelegate {
@@ -35,20 +35,21 @@ impl TasksModalDelegate {
             candidates: Vec::new(),
             matches: Vec::new(),
             selected_index: 0,
-            last_prompt: String::default(),
+            prompt: String::default(),
         }
     }
 
     fn spawn_oneshot(&mut self, cx: &mut AppContext) -> Option<Arc<dyn Task>> {
-        let oneshot_source = self
-            .inventory
-            .update(cx, |this, _| this.source::<OneshotSource>())?;
-        oneshot_source.update(cx, |this, _| {
-            let Some(this) = this.as_any().downcast_mut::<OneshotSource>() else {
-                return None;
-            };
-            Some(this.spawn(self.last_prompt.clone()))
-        })
+        self.inventory
+            .update(cx, |inventory, _| inventory.source::<OneshotSource>())?
+            .update(cx, |oneshot_source, _| {
+                Some(
+                    oneshot_source
+                        .as_any()
+                        .downcast_mut::<OneshotSource>()?
+                        .spawn(self.prompt.clone()),
+                )
+            })
     }
 }
 
@@ -132,12 +133,7 @@ impl PickerDelegate for TasksModalDelegate {
                     picker.delegate.candidates = picker
                         .delegate
                         .inventory
-                        .update(cx, |inventory, cx| inventory.list_tasks(None, cx));
-                    picker
-                        .delegate
-                        .candidates
-                        .sort_by(|a, b| a.name().cmp(&b.name()));
-
+                        .update(cx, |inventory, cx| inventory.list_tasks(None, true, cx));
                     picker
                         .delegate
                         .candidates
@@ -167,7 +163,7 @@ impl PickerDelegate for TasksModalDelegate {
                 .update(&mut cx, |picker, _| {
                     let delegate = &mut picker.delegate;
                     delegate.matches = matches;
-                    delegate.last_prompt = query;
+                    delegate.prompt = query;
 
                     if delegate.matches.is_empty() {
                         delegate.selected_index = 0;
@@ -184,7 +180,7 @@ impl PickerDelegate for TasksModalDelegate {
         let current_match_index = self.selected_index();
 
         let task = if secondary {
-            if !self.last_prompt.trim().is_empty() {
+            if !self.prompt.trim().is_empty() {
                 self.spawn_oneshot(cx)
             } else {
                 None