diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ff5bed98fb..bcb09e12cd 100644 --- a/crates/editor/src/editor.rs +++ b/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)); + })); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a967895e24..65dae12ff2 100644 --- a/crates/project/src/project.rs +++ b/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::( - tasks_file_rx, - cx, - ), - ), + |tx, cx| { + StaticSource::new(TrackedFile::new_convertible::< + task::VsCodeTaskFile, + >( + tasks_file_rx, tx, cx + )) + }, cx, ); } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 4b2d53c61b..2cb9d3e524 100644 --- a/crates/project/src/project_tests.rs +++ b/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), diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index ca48700a69..66e2b9cea8 100644 --- a/crates/project/src/task_inventory.rs +++ b/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, last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>, + update_sender: UnboundedSender<()>, + _update_pooler: Task>, } struct SourceInInventory { @@ -82,9 +88,22 @@ impl TaskSourceKind { impl Inventory { pub fn new(cx: &mut AppContext) -> Model { - 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, ) { 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, + 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, ); }); diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 324183b688..3ae9ee0b70 100644 --- a/crates/task/src/static_source.rs +++ b/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 { parsed_contents: Arc>, } impl TrackedFile { /// Initializes new [`TrackedFile`] with a type that's deserializable. - pub fn new(mut tracker: UnboundedReceiver, cx: &mut AppContext) -> Self + pub fn new( + mut tracker: UnboundedReceiver, + notification_outlet: UnboundedSender<()>, + cx: &mut AppContext, + ) -> Self where T: for<'a> Deserialize<'a> + Default + Send, { @@ -46,7 +49,13 @@ impl TrackedFile { 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 TrackedFile { /// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type. pub fn new_convertible Deserialize<'a> + TryInto>( mut tracker: UnboundedReceiver, + notification_outlet: UnboundedSender<()>, cx: &mut AppContext, ) -> Self where @@ -85,7 +95,13 @@ impl TrackedFile { 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(()) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 60662b9495..5124ef0074 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -174,7 +174,7 @@ pub fn initialize_workspace(app_state: Arc, 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, ); })