@@ -5,8 +5,8 @@ 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;
+use task::{Task, TaskId, TaskSource};
+use util::{post_inc, NumericPrefixWithSuffix};
/// Inventory tracks available tasks for a given project.
pub struct Inventory {
@@ -15,7 +15,7 @@ pub struct Inventory {
}
struct SourceInInventory {
- source: Model<Box<dyn Source>>,
+ source: Model<Box<dyn TaskSource>>,
_subscription: Subscription,
type_id: TypeId,
}
@@ -29,7 +29,7 @@ impl Inventory {
}
/// Registers a new tasks source, that would be fetched for available tasks.
- pub fn add_source(&mut self, source: Model<Box<dyn Source>>, cx: &mut ModelContext<Self>) {
+ pub fn add_source(&mut self, source: Model<Box<dyn TaskSource>>, cx: &mut ModelContext<Self>) {
let _subscription = cx.observe(&source, |_, _, cx| {
cx.notify();
});
@@ -43,7 +43,7 @@ impl Inventory {
cx.notify();
}
- pub fn source<T: Source>(&self) -> Option<Model<Box<dyn Source>>> {
+ pub fn source<T: TaskSource>(&self) -> Option<Model<Box<dyn TaskSource>>> {
let target_type_id = std::any::TypeId::of::<T>();
self.sources.iter().find_map(
|SourceInInventory {
@@ -77,6 +77,8 @@ impl Inventory {
} else {
HashMap::default()
};
+ let not_used_score = post_inc(&mut lru_score);
+
self.sources
.iter()
.flat_map(|source| {
@@ -89,16 +91,20 @@ impl Inventory {
tasks_by_usage
.get(&task.id())
.copied()
- .unwrap_or_else(|| post_inc(&mut lru_score))
+ .unwrap_or(not_used_score)
} else {
- post_inc(&mut lru_score)
+ not_used_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()))
+ usages_a.cmp(usages_b).then({
+ NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name())
+ .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
+ task_b.name(),
+ ))
+ .then(task_a.name().cmp(task_b.name()))
+ })
})
.map(|(task, _)| task)
.collect()
@@ -114,6 +120,7 @@ impl Inventory {
})
}
+ /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
pub fn task_scheduled(&mut self, id: TaskId) {
self.last_scheduled_tasks.push_back(id);
if self.last_scheduled_tasks.len() > 5_000 {
@@ -124,10 +131,234 @@ impl Inventory {
#[cfg(test)]
mod tests {
+ use std::path::PathBuf;
+
+ use gpui::TestAppContext;
+
use super::*;
- #[test]
- fn todo_kb() {
- todo!("TODO kb LRU tests")
+ #[gpui::test]
+ fn test_task_list_sorting(cx: &mut TestAppContext) {
+ let inventory = cx.update(Inventory::new);
+ let initial_tasks = list_task_names(&inventory, None, true, cx);
+ assert!(
+ initial_tasks.is_empty(),
+ "No tasks expected for empty inventory, but got {initial_tasks:?}"
+ );
+ let initial_tasks = list_task_names(&inventory, None, false, cx);
+ assert!(
+ initial_tasks.is_empty(),
+ "No tasks expected for empty inventory, but got {initial_tasks:?}"
+ );
+
+ inventory.update(cx, |inventory, cx| {
+ inventory.add_source(TestSource::new(vec!["3_task".to_string()], cx), cx);
+ });
+ inventory.update(cx, |inventory, cx| {
+ inventory.add_source(
+ TestSource::new(
+ vec![
+ "1_task".to_string(),
+ "2_task".to_string(),
+ "1_a_task".to_string(),
+ ],
+ cx,
+ ),
+ cx,
+ );
+ });
+
+ let expected_initial_state = [
+ "1_a_task".to_string(),
+ "1_task".to_string(),
+ "2_task".to_string(),
+ "3_task".to_string(),
+ ];
+ assert_eq!(
+ list_task_names(&inventory, None, false, cx),
+ &expected_initial_state,
+ "Task list without lru sorting, should be sorted alphanumerically"
+ );
+ assert_eq!(
+ list_task_names(&inventory, None, true, cx),
+ &expected_initial_state,
+ "Tasks with equal amount of usages should be sorted alphanumerically"
+ );
+
+ register_task_used(&inventory, "2_task", cx);
+ assert_eq!(
+ list_task_names(&inventory, None, false, cx),
+ &expected_initial_state,
+ "Task list without lru sorting, should be sorted alphanumerically"
+ );
+ assert_eq!(
+ list_task_names(&inventory, None, true, cx),
+ vec![
+ "2_task".to_string(),
+ "1_a_task".to_string(),
+ "1_task".to_string(),
+ "3_task".to_string()
+ ],
+ );
+
+ register_task_used(&inventory, "1_task", cx);
+ register_task_used(&inventory, "1_task", cx);
+ register_task_used(&inventory, "1_task", cx);
+ register_task_used(&inventory, "3_task", cx);
+ assert_eq!(
+ list_task_names(&inventory, None, false, cx),
+ &expected_initial_state,
+ "Task list without lru sorting, should be sorted alphanumerically"
+ );
+ assert_eq!(
+ list_task_names(&inventory, None, true, cx),
+ vec![
+ "3_task".to_string(),
+ "1_task".to_string(),
+ "2_task".to_string(),
+ "1_a_task".to_string(),
+ ],
+ );
+
+ inventory.update(cx, |inventory, cx| {
+ inventory.add_source(
+ TestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx),
+ cx,
+ );
+ });
+ let expected_updated_state = [
+ "1_a_task".to_string(),
+ "1_task".to_string(),
+ "2_task".to_string(),
+ "3_task".to_string(),
+ "10_hello".to_string(),
+ "11_hello".to_string(),
+ ];
+ assert_eq!(
+ list_task_names(&inventory, None, false, cx),
+ &expected_updated_state,
+ "Task list without lru sorting, should be sorted alphanumerically"
+ );
+ assert_eq!(
+ list_task_names(&inventory, None, true, cx),
+ vec![
+ "3_task".to_string(),
+ "1_task".to_string(),
+ "2_task".to_string(),
+ "1_a_task".to_string(),
+ "10_hello".to_string(),
+ "11_hello".to_string(),
+ ],
+ );
+
+ register_task_used(&inventory, "11_hello", cx);
+ assert_eq!(
+ list_task_names(&inventory, None, false, cx),
+ &expected_updated_state,
+ "Task list without lru sorting, should be sorted alphanumerically"
+ );
+ assert_eq!(
+ list_task_names(&inventory, None, true, cx),
+ vec![
+ "11_hello".to_string(),
+ "3_task".to_string(),
+ "1_task".to_string(),
+ "2_task".to_string(),
+ "1_a_task".to_string(),
+ "10_hello".to_string(),
+ ],
+ );
+ }
+
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ struct TestTask {
+ id: TaskId,
+ name: String,
+ }
+
+ impl Task for TestTask {
+ fn id(&self) -> &TaskId {
+ &self.id
+ }
+
+ fn name(&self) -> &str {
+ &self.name
+ }
+
+ fn cwd(&self) -> Option<&Path> {
+ None
+ }
+
+ fn exec(&self, _cwd: Option<PathBuf>) -> Option<task::SpawnInTerminal> {
+ None
+ }
+ }
+
+ struct TestSource {
+ tasks: Vec<TestTask>,
+ }
+
+ impl TestSource {
+ fn new(
+ task_names: impl IntoIterator<Item = String>,
+ cx: &mut AppContext,
+ ) -> Model<Box<dyn TaskSource>> {
+ cx.new_model(|_| {
+ Box::new(Self {
+ tasks: task_names
+ .into_iter()
+ .enumerate()
+ .map(|(i, name)| TestTask {
+ id: TaskId(format!("task_{i}_{name}")),
+ name,
+ })
+ .collect(),
+ }) as Box<dyn TaskSource>
+ })
+ }
+ }
+
+ impl TaskSource for TestSource {
+ fn tasks_for_path(
+ &mut self,
+ _path: Option<&Path>,
+ _cx: &mut ModelContext<Box<dyn TaskSource>>,
+ ) -> Vec<Arc<dyn Task>> {
+ self.tasks
+ .clone()
+ .into_iter()
+ .map(|task| Arc::new(task) as Arc<dyn Task>)
+ .collect()
+ }
+
+ fn as_any(&mut self) -> &mut dyn std::any::Any {
+ self
+ }
+ }
+
+ fn list_task_names(
+ inventory: &Model<Inventory>,
+ path: Option<&Path>,
+ lru: bool,
+ cx: &mut TestAppContext,
+ ) -> Vec<String> {
+ inventory.update(cx, |inventory, cx| {
+ inventory
+ .list_tasks(path, lru, cx)
+ .into_iter()
+ .map(|task| task.name().to_string())
+ .collect()
+ })
+ }
+
+ fn register_task_used(inventory: &Model<Inventory>, task_name: &str, cx: &mut TestAppContext) {
+ inventory.update(cx, |inventory, cx| {
+ let task = inventory
+ .list_tasks(None, false, cx)
+ .into_iter()
+ .find(|task| task.name() == task_name)
+ .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
+ inventory.task_scheduled(task.id().clone());
+ });
}
}