task_inventory.rs

  1//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
  2
  3use std::{any::TypeId, path::Path, sync::Arc};
  4
  5use collections::{HashMap, VecDeque};
  6use gpui::{AppContext, Context, Model, ModelContext, Subscription};
  7use itertools::Itertools;
  8use task::{Task, TaskId, TaskSource};
  9use util::{post_inc, NumericPrefixWithSuffix};
 10
 11/// Inventory tracks available tasks for a given project.
 12pub struct Inventory {
 13    sources: Vec<SourceInInventory>,
 14    last_scheduled_tasks: VecDeque<TaskId>,
 15}
 16
 17struct SourceInInventory {
 18    source: Model<Box<dyn TaskSource>>,
 19    _subscription: Subscription,
 20    type_id: TypeId,
 21}
 22
 23impl Inventory {
 24    pub(crate) fn new(cx: &mut AppContext) -> Model<Self> {
 25        cx.new_model(|_| Self {
 26            sources: Vec::new(),
 27            last_scheduled_tasks: VecDeque::new(),
 28        })
 29    }
 30
 31    /// Registers a new tasks source, that would be fetched for available tasks.
 32    pub fn add_source(&mut self, source: Model<Box<dyn TaskSource>>, cx: &mut ModelContext<Self>) {
 33        let _subscription = cx.observe(&source, |_, _, cx| {
 34            cx.notify();
 35        });
 36        let type_id = source.read(cx).type_id();
 37        let source = SourceInInventory {
 38            source,
 39            _subscription,
 40            type_id,
 41        };
 42        self.sources.push(source);
 43        cx.notify();
 44    }
 45
 46    pub fn source<T: TaskSource>(&self) -> Option<Model<Box<dyn TaskSource>>> {
 47        let target_type_id = std::any::TypeId::of::<T>();
 48        self.sources.iter().find_map(
 49            |SourceInInventory {
 50                 type_id, source, ..
 51             }| {
 52                if &target_type_id == type_id {
 53                    Some(source.clone())
 54                } else {
 55                    None
 56                }
 57            },
 58        )
 59    }
 60
 61    /// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path).
 62    pub fn list_tasks(
 63        &self,
 64        path: Option<&Path>,
 65        lru: bool,
 66        cx: &mut AppContext,
 67    ) -> Vec<Arc<dyn Task>> {
 68        let mut lru_score = 0_u32;
 69        let tasks_by_usage = if lru {
 70            self.last_scheduled_tasks
 71                .iter()
 72                .rev()
 73                .fold(HashMap::default(), |mut tasks, id| {
 74                    tasks.entry(id).or_insert_with(|| post_inc(&mut lru_score));
 75                    tasks
 76                })
 77        } else {
 78            HashMap::default()
 79        };
 80        let not_used_score = post_inc(&mut lru_score);
 81
 82        self.sources
 83            .iter()
 84            .flat_map(|source| {
 85                source
 86                    .source
 87                    .update(cx, |source, cx| source.tasks_for_path(path, cx))
 88            })
 89            .map(|task| {
 90                let usages = if lru {
 91                    tasks_by_usage
 92                        .get(&task.id())
 93                        .copied()
 94                        .unwrap_or(not_used_score)
 95                } else {
 96                    not_used_score
 97                };
 98                (task, usages)
 99            })
100            .sorted_unstable_by(|(task_a, usages_a), (task_b, usages_b)| {
101                usages_a.cmp(usages_b).then({
102                    NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name())
103                        .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
104                            task_b.name(),
105                        ))
106                        .then(task_a.name().cmp(task_b.name()))
107                })
108            })
109            .map(|(task, _)| task)
110            .collect()
111    }
112
113    /// Returns the last scheduled task, if any of the sources contains one with the matching id.
114    pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<Arc<dyn Task>> {
115        self.last_scheduled_tasks.back().and_then(|id| {
116            // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future.
117            self.list_tasks(None, false, cx)
118                .into_iter()
119                .find(|task| task.id() == id)
120        })
121    }
122
123    /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
124    pub fn task_scheduled(&mut self, id: TaskId) {
125        self.last_scheduled_tasks.push_back(id);
126        if self.last_scheduled_tasks.len() > 5_000 {
127            self.last_scheduled_tasks.pop_front();
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use std::path::PathBuf;
135
136    use gpui::TestAppContext;
137
138    use super::*;
139
140    #[gpui::test]
141    fn test_task_list_sorting(cx: &mut TestAppContext) {
142        let inventory = cx.update(Inventory::new);
143        let initial_tasks = list_task_names(&inventory, None, true, cx);
144        assert!(
145            initial_tasks.is_empty(),
146            "No tasks expected for empty inventory, but got {initial_tasks:?}"
147        );
148        let initial_tasks = list_task_names(&inventory, None, false, cx);
149        assert!(
150            initial_tasks.is_empty(),
151            "No tasks expected for empty inventory, but got {initial_tasks:?}"
152        );
153
154        inventory.update(cx, |inventory, cx| {
155            inventory.add_source(TestSource::new(vec!["3_task".to_string()], cx), cx);
156        });
157        inventory.update(cx, |inventory, cx| {
158            inventory.add_source(
159                TestSource::new(
160                    vec![
161                        "1_task".to_string(),
162                        "2_task".to_string(),
163                        "1_a_task".to_string(),
164                    ],
165                    cx,
166                ),
167                cx,
168            );
169        });
170
171        let expected_initial_state = [
172            "1_a_task".to_string(),
173            "1_task".to_string(),
174            "2_task".to_string(),
175            "3_task".to_string(),
176        ];
177        assert_eq!(
178            list_task_names(&inventory, None, false, cx),
179            &expected_initial_state,
180            "Task list without lru sorting, should be sorted alphanumerically"
181        );
182        assert_eq!(
183            list_task_names(&inventory, None, true, cx),
184            &expected_initial_state,
185            "Tasks with equal amount of usages should be sorted alphanumerically"
186        );
187
188        register_task_used(&inventory, "2_task", cx);
189        assert_eq!(
190            list_task_names(&inventory, None, false, cx),
191            &expected_initial_state,
192            "Task list without lru sorting, should be sorted alphanumerically"
193        );
194        assert_eq!(
195            list_task_names(&inventory, None, true, cx),
196            vec![
197                "2_task".to_string(),
198                "1_a_task".to_string(),
199                "1_task".to_string(),
200                "3_task".to_string()
201            ],
202        );
203
204        register_task_used(&inventory, "1_task", cx);
205        register_task_used(&inventory, "1_task", cx);
206        register_task_used(&inventory, "1_task", cx);
207        register_task_used(&inventory, "3_task", cx);
208        assert_eq!(
209            list_task_names(&inventory, None, false, cx),
210            &expected_initial_state,
211            "Task list without lru sorting, should be sorted alphanumerically"
212        );
213        assert_eq!(
214            list_task_names(&inventory, None, true, cx),
215            vec![
216                "3_task".to_string(),
217                "1_task".to_string(),
218                "2_task".to_string(),
219                "1_a_task".to_string(),
220            ],
221        );
222
223        inventory.update(cx, |inventory, cx| {
224            inventory.add_source(
225                TestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx),
226                cx,
227            );
228        });
229        let expected_updated_state = [
230            "1_a_task".to_string(),
231            "1_task".to_string(),
232            "2_task".to_string(),
233            "3_task".to_string(),
234            "10_hello".to_string(),
235            "11_hello".to_string(),
236        ];
237        assert_eq!(
238            list_task_names(&inventory, None, false, cx),
239            &expected_updated_state,
240            "Task list without lru sorting, should be sorted alphanumerically"
241        );
242        assert_eq!(
243            list_task_names(&inventory, None, true, cx),
244            vec![
245                "3_task".to_string(),
246                "1_task".to_string(),
247                "2_task".to_string(),
248                "1_a_task".to_string(),
249                "10_hello".to_string(),
250                "11_hello".to_string(),
251            ],
252        );
253
254        register_task_used(&inventory, "11_hello", cx);
255        assert_eq!(
256            list_task_names(&inventory, None, false, cx),
257            &expected_updated_state,
258            "Task list without lru sorting, should be sorted alphanumerically"
259        );
260        assert_eq!(
261            list_task_names(&inventory, None, true, cx),
262            vec![
263                "11_hello".to_string(),
264                "3_task".to_string(),
265                "1_task".to_string(),
266                "2_task".to_string(),
267                "1_a_task".to_string(),
268                "10_hello".to_string(),
269            ],
270        );
271    }
272
273    #[derive(Debug, Clone, PartialEq, Eq)]
274    struct TestTask {
275        id: TaskId,
276        name: String,
277    }
278
279    impl Task for TestTask {
280        fn id(&self) -> &TaskId {
281            &self.id
282        }
283
284        fn name(&self) -> &str {
285            &self.name
286        }
287
288        fn cwd(&self) -> Option<&Path> {
289            None
290        }
291
292        fn exec(&self, _cwd: Option<PathBuf>) -> Option<task::SpawnInTerminal> {
293            None
294        }
295    }
296
297    struct TestSource {
298        tasks: Vec<TestTask>,
299    }
300
301    impl TestSource {
302        fn new(
303            task_names: impl IntoIterator<Item = String>,
304            cx: &mut AppContext,
305        ) -> Model<Box<dyn TaskSource>> {
306            cx.new_model(|_| {
307                Box::new(Self {
308                    tasks: task_names
309                        .into_iter()
310                        .enumerate()
311                        .map(|(i, name)| TestTask {
312                            id: TaskId(format!("task_{i}_{name}")),
313                            name,
314                        })
315                        .collect(),
316                }) as Box<dyn TaskSource>
317            })
318        }
319    }
320
321    impl TaskSource for TestSource {
322        fn tasks_for_path(
323            &mut self,
324            _path: Option<&Path>,
325            _cx: &mut ModelContext<Box<dyn TaskSource>>,
326        ) -> Vec<Arc<dyn Task>> {
327            self.tasks
328                .clone()
329                .into_iter()
330                .map(|task| Arc::new(task) as Arc<dyn Task>)
331                .collect()
332        }
333
334        fn as_any(&mut self) -> &mut dyn std::any::Any {
335            self
336        }
337    }
338
339    fn list_task_names(
340        inventory: &Model<Inventory>,
341        path: Option<&Path>,
342        lru: bool,
343        cx: &mut TestAppContext,
344    ) -> Vec<String> {
345        inventory.update(cx, |inventory, cx| {
346            inventory
347                .list_tasks(path, lru, cx)
348                .into_iter()
349                .map(|task| task.name().to_string())
350                .collect()
351        })
352    }
353
354    fn register_task_used(inventory: &Model<Inventory>, task_name: &str, cx: &mut TestAppContext) {
355        inventory.update(cx, |inventory, cx| {
356            let task = inventory
357                .list_tasks(None, false, cx)
358                .into_iter()
359                .find(|task| task.name() == task_name)
360                .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
361            inventory.task_scheduled(task.id().clone());
362        });
363    }
364}