tasks: Refresh available tasks in editor when tasks.json changes (#11811)

Piotr Osiewicz created

Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs          |   4 +
crates/project/src/project.rs        |  15 ++-
crates/project/src/project_tests.rs  |  20 +++-
crates/project/src/task_inventory.rs | 107 +++++++++++++++++++++--------
crates/task/src/static_source.rs     |  28 ++++++-
crates/zed/src/zed.rs                |   2 
6 files changed, 126 insertions(+), 50 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1581,6 +1581,10 @@ impl Editor {
                         editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
                     };
                 }));
+                let task_inventory = project.read(cx).task_inventory().clone();
+                project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| {
+                    editor.tasks_update_task = Some(editor.refresh_runnables(cx));
+                }));
             }
         }
 

crates/project/src/project.rs 🔗

@@ -7655,7 +7655,7 @@ impl Project {
                                 abs_path,
                                 id_base: "local_tasks_for_worktree",
                             },
-                            StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
+                            |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),
                             cx,
                         );
                     }
@@ -7675,12 +7675,13 @@ impl Project {
                                 abs_path,
                                 id_base: "local_vscode_tasks_for_worktree",
                             },
-                            StaticSource::new(
-                                TrackedFile::new_convertible::<task::VsCodeTaskFile>(
-                                    tasks_file_rx,
-                                    cx,
-                                ),
-                            ),
+                            |tx, cx| {
+                                StaticSource::new(TrackedFile::new_convertible::<
+                                    task::VsCodeTaskFile,
+                                >(
+                                    tasks_file_rx, tx, cx
+                                ))
+                            },
                             cx,
                         );
                     }

crates/project/src/project_tests.rs 🔗

@@ -242,19 +242,25 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
     }]))
     .unwrap();
     let (tx, rx) = futures::channel::mpsc::unbounded();
-
-    let templates = cx.update(|cx| TrackedFile::new(rx, cx));
+    cx.update(|cx| {
+        project.update(cx, |project, cx| {
+            project.task_inventory().update(cx, |inventory, cx| {
+                inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
+                inventory.add_source(
+                    global_task_source_kind.clone(),
+                    |tx, cx| StaticSource::new(TrackedFile::new(rx, tx, cx)),
+                    cx,
+                );
+            });
+        })
+    });
     tx.unbounded_send(tasks).unwrap();
 
-    let source = StaticSource::new(templates);
     cx.run_until_parked();
-
     cx.update(|cx| {
         let all_tasks = project
             .update(cx, |project, cx| {
-                project.task_inventory().update(cx, |inventory, cx| {
-                    inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
-                    inventory.add_source(global_task_source_kind.clone(), source, cx);
+                project.task_inventory().update(cx, |inventory, _| {
                     let (mut old, new) = inventory.used_and_current_resolved_tasks(
                         None,
                         Some(workree_id),

crates/project/src/task_inventory.rs 🔗

@@ -7,7 +7,11 @@ use std::{
 };
 
 use collections::{btree_map, BTreeMap, VecDeque};
-use gpui::{AppContext, Context, Model, ModelContext};
+use futures::{
+    channel::mpsc::{unbounded, UnboundedSender},
+    StreamExt,
+};
+use gpui::{AppContext, Context, Model, ModelContext, Task};
 use itertools::Itertools;
 use language::Language;
 use task::{
@@ -20,6 +24,8 @@ use worktree::WorktreeId;
 pub struct Inventory {
     sources: Vec<SourceInInventory>,
     last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
+    update_sender: UnboundedSender<()>,
+    _update_pooler: Task<anyhow::Result<()>>,
 }
 
 struct SourceInInventory {
@@ -82,9 +88,22 @@ impl TaskSourceKind {
 
 impl Inventory {
     pub fn new(cx: &mut AppContext) -> Model<Self> {
-        cx.new_model(|_| Self {
-            sources: Vec::new(),
-            last_scheduled_tasks: VecDeque::new(),
+        cx.new_model(|cx| {
+            let (update_sender, mut rx) = unbounded();
+            let _update_pooler = cx.spawn(|this, mut cx| async move {
+                while let Some(()) = rx.next().await {
+                    this.update(&mut cx, |_, cx| {
+                        cx.notify();
+                    })?;
+                }
+                Ok(())
+            });
+            Self {
+                sources: Vec::new(),
+                last_scheduled_tasks: VecDeque::new(),
+                update_sender,
+                _update_pooler,
+            }
         })
     }
 
@@ -94,7 +113,7 @@ impl Inventory {
     pub fn add_source(
         &mut self,
         kind: TaskSourceKind,
-        source: StaticSource,
+        create_source: impl FnOnce(UnboundedSender<()>, &mut AppContext) -> StaticSource,
         cx: &mut ModelContext<Self>,
     ) {
         let abs_path = kind.abs_path();
@@ -104,7 +123,7 @@ impl Inventory {
                 return;
             }
         }
-
+        let source = create_source(self.update_sender.clone(), cx);
         let source = SourceInInventory { source, kind };
         self.sources.push(source);
         cx.notify();
@@ -375,7 +394,7 @@ mod test_inventory {
 
     use crate::Inventory;
 
-    use super::{task_source_kind_preference, TaskSourceKind};
+    use super::{task_source_kind_preference, TaskSourceKind, UnboundedSender};
 
     #[derive(Debug, Clone, PartialEq, Eq)]
     pub struct TestTask {
@@ -384,6 +403,7 @@ mod test_inventory {
 
     pub(super) fn static_test_source(
         task_names: impl IntoIterator<Item = String>,
+        updates: UnboundedSender<()>,
         cx: &mut AppContext,
     ) -> StaticSource {
         let tasks = TaskTemplates(
@@ -397,7 +417,7 @@ mod test_inventory {
                 .collect(),
         );
         let (tx, rx) = futures::channel::mpsc::unbounded();
-        let file = TrackedFile::new(rx, cx);
+        let file = TrackedFile::new(rx, updates, cx);
         tx.unbounded_send(serde_json::to_string(&tasks).unwrap())
             .unwrap();
         StaticSource::new(file)
@@ -495,21 +515,24 @@ mod tests {
         inventory.update(cx, |inventory, cx| {
             inventory.add_source(
                 TaskSourceKind::UserInput,
-                static_test_source(vec!["3_task".to_string()], cx),
+                |tx, cx| static_test_source(vec!["3_task".to_string()], tx, cx),
                 cx,
             );
         });
         inventory.update(cx, |inventory, cx| {
             inventory.add_source(
                 TaskSourceKind::UserInput,
-                static_test_source(
-                    vec![
-                        "1_task".to_string(),
-                        "2_task".to_string(),
-                        "1_a_task".to_string(),
-                    ],
-                    cx,
-                ),
+                |tx, cx| {
+                    static_test_source(
+                        vec![
+                            "1_task".to_string(),
+                            "2_task".to_string(),
+                            "1_a_task".to_string(),
+                        ],
+                        tx,
+                        cx,
+                    )
+                },
                 cx,
             );
         });
@@ -570,7 +593,9 @@ mod tests {
         inventory.update(cx, |inventory, cx| {
             inventory.add_source(
                 TaskSourceKind::UserInput,
-                static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], cx),
+                |tx, cx| {
+                    static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], tx, cx)
+                },
                 cx,
             );
         });
@@ -638,7 +663,13 @@ mod tests {
         inventory_with_statics.update(cx, |inventory, cx| {
             inventory.add_source(
                 TaskSourceKind::UserInput,
-                static_test_source(vec!["user_input".to_string(), common_name.to_string()], cx),
+                |tx, cx| {
+                    static_test_source(
+                        vec!["user_input".to_string(), common_name.to_string()],
+                        tx,
+                        cx,
+                    )
+                },
                 cx,
             );
             inventory.add_source(
@@ -646,10 +677,13 @@ mod tests {
                     id_base: "test source",
                     abs_path: path_1.to_path_buf(),
                 },
-                static_test_source(
-                    vec!["static_source_1".to_string(), common_name.to_string()],
-                    cx,
-                ),
+                |tx, cx| {
+                    static_test_source(
+                        vec!["static_source_1".to_string(), common_name.to_string()],
+                        tx,
+                        cx,
+                    )
+                },
                 cx,
             );
             inventory.add_source(
@@ -657,10 +691,13 @@ mod tests {
                     id_base: "test source",
                     abs_path: path_2.to_path_buf(),
                 },
-                static_test_source(
-                    vec!["static_source_2".to_string(), common_name.to_string()],
-                    cx,
-                ),
+                |tx, cx| {
+                    static_test_source(
+                        vec!["static_source_2".to_string(), common_name.to_string()],
+                        tx,
+                        cx,
+                    )
+                },
                 cx,
             );
             inventory.add_source(
@@ -669,7 +706,13 @@ mod tests {
                     abs_path: worktree_path_1.to_path_buf(),
                     id_base: "test_source",
                 },
-                static_test_source(vec!["worktree_1".to_string(), common_name.to_string()], cx),
+                |tx, cx| {
+                    static_test_source(
+                        vec!["worktree_1".to_string(), common_name.to_string()],
+                        tx,
+                        cx,
+                    )
+                },
                 cx,
             );
             inventory.add_source(
@@ -678,7 +721,13 @@ mod tests {
                     abs_path: worktree_path_2.to_path_buf(),
                     id_base: "test_source",
                 },
-                static_test_source(vec!["worktree_2".to_string(), common_name.to_string()], cx),
+                |tx, cx| {
+                    static_test_source(
+                        vec!["worktree_2".to_string(), common_name.to_string()],
+                        tx,
+                        cx,
+                    )
+                },
                 cx,
             );
         });

crates/task/src/static_source.rs 🔗

@@ -2,7 +2,7 @@
 
 use std::sync::Arc;
 
-use futures::StreamExt;
+use futures::{channel::mpsc::UnboundedSender, StreamExt};
 use gpui::AppContext;
 use parking_lot::RwLock;
 use serde::Deserialize;
@@ -17,15 +17,18 @@ pub struct StaticSource {
 }
 
 /// A Wrapper around deserializable T that keeps track of its contents
-/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
-/// notified.
+/// via a provided channel.
 pub struct TrackedFile<T> {
     parsed_contents: Arc<RwLock<T>>,
 }
 
 impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
     /// Initializes new [`TrackedFile`] with a type that's deserializable.
-    pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Self
+    pub fn new(
+        mut tracker: UnboundedReceiver<String>,
+        notification_outlet: UnboundedSender<()>,
+        cx: &mut AppContext,
+    ) -> Self
     where
         T: for<'a> Deserialize<'a> + Default + Send,
     {
@@ -46,7 +49,13 @@ impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
                                 continue;
                             };
                             let mut contents = parsed_contents.write();
-                            *contents = new_contents;
+                            if *contents != new_contents {
+                                *contents = new_contents;
+                                if notification_outlet.unbounded_send(()).is_err() {
+                                    // Whoever cared about contents is not around anymore.
+                                    break;
+                                }
+                            }
                         }
                     }
                     anyhow::Ok(())
@@ -59,6 +68,7 @@ impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
     /// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type.
     pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
         mut tracker: UnboundedReceiver<String>,
+        notification_outlet: UnboundedSender<()>,
         cx: &mut AppContext,
     ) -> Self
     where
@@ -85,7 +95,13 @@ impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
                                 continue;
                             };
                             let mut contents = parsed_contents.write();
-                            *contents = new_contents;
+                            if *contents != new_contents {
+                                *contents = new_contents;
+                                if notification_outlet.unbounded_send(()).is_err() {
+                                    // Whoever cared about contents is not around anymore.
+                                    break;
+                                }
+                            }
                         }
                     }
                     anyhow::Ok(())

crates/zed/src/zed.rs 🔗

@@ -174,7 +174,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                             id_base: "global_tasks",
                             abs_path: paths::TASKS.clone(),
                         },
-                        StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
+                        |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),
                         cx,
                     );
                 })