diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index fc710fd41b..f21bad88f0 100644 --- a/crates/project/src/task_inventory.rs +++ b/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, - pub last_scheduled_task: Option, + last_scheduled_tasks: VecDeque, } struct SourceInInventory { @@ -21,7 +24,7 @@ impl Inventory { pub(crate) fn new(cx: &mut AppContext) -> Model { 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(&self) -> Option>> { let target_type_id = std::any::TypeId::of::(); 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> { - 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> { + 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> { - 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") + } } diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 7de495f2ae..5f517fdedf 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/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)); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 77a47847cf..e02ae65f4f 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -24,7 +24,7 @@ pub(crate) struct TasksModalDelegate { matches: Vec, selected_index: usize, workspace: WeakView, - 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> { - let oneshot_source = self - .inventory - .update(cx, |this, _| this.source::())?; - oneshot_source.update(cx, |this, _| { - let Some(this) = this.as_any().downcast_mut::() else { - return None; - }; - Some(this.spawn(self.last_prompt.clone())) - }) + self.inventory + .update(cx, |inventory, _| inventory.source::())? + .update(cx, |oneshot_source, _| { + Some( + oneshot_source + .as_any() + .downcast_mut::()? + .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