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

Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2024-05-14 21:26:35 +02:00 committed by GitHub
parent 0ae0b08c38
commit 1db136ff65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 126 additions and 50 deletions

View file

@ -1581,6 +1581,10 @@ impl Editor {
editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); 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));
}));
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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