diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 95e2e62041..e5e401e493 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -469,9 +469,6 @@ impl Server { .add_request_handler(user_handler( forward_project_request_for_owner::, )) - .add_request_handler(user_handler( - forward_project_request_for_owner::, - )) .add_request_handler(user_handler( forward_read_only_project_request::, )) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 412c7d3051..15b833c1dc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1879,10 +1879,17 @@ impl Editor { } } })); - 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)); - })); + if let Some(task_inventory) = project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + { + project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| { + editor.tasks_update_task = Some(editor.refresh_runnables(cx)); + })); + } } } @@ -4717,11 +4724,13 @@ impl Editor { ); } project.update(cx, |project, cx| { - project.task_context_for_location( - captured_task_variables, - location, - cx, - ) + project.task_store().update(cx, |task_store, cx| { + task_store.task_context_for_location( + captured_task_variables, + location, + cx, + ) + }) }) }); @@ -9134,23 +9143,29 @@ impl Editor { .map(|file| (file.worktree_id(cx), file.clone())) .unzip(); - (project.task_inventory().clone(), worktree_id, file) + ( + project.task_store().read(cx).task_inventory().cloned(), + worktree_id, + file, + ) }); - let inventory = inventory.read(cx); let tags = mem::take(&mut runnable.tags); let mut tags: Vec<_> = tags .into_iter() .flat_map(|tag| { let tag = tag.0.clone(); inventory - .list_tasks( - file.clone(), - Some(runnable.language.clone()), - worktree_id, - cx, - ) + .as_ref() .into_iter() + .flat_map(|inventory| { + inventory.read(cx).list_tasks( + file.clone(), + Some(runnable.language.clone()), + worktree_id, + cx, + ) + }) .filter(move |(_, template)| { template.tags.iter().any(|source_tag| source_tag == &tag) }) diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index eb5131e252..51945a1780 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -67,10 +67,11 @@ fn task_context_with_editor( variables }; - let context_task = project.update(cx, |project, cx| { - project.task_context_for_location(captured_variables, location.clone(), cx) - }); - cx.spawn(|_| context_task) + project.update(cx, |project, cx| { + project.task_store().update(cx, |task_store, cx| { + task_store.task_context_for_location(captured_variables, location, cx) + }) + }) } pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> AsyncTask { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index de8601044a..214436231d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -9,6 +9,7 @@ pub mod prettier_store; pub mod project_settings; pub mod search; mod task_inventory; +pub mod task_store; pub mod terminals; pub mod worktree_store; @@ -21,7 +22,7 @@ pub use environment::EnvironmentErrorMessage; pub mod search_history; mod yarn; -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{anyhow, Result}; use buffer_store::{BufferStore, BufferStoreEvent}; use client::{ proto, Client, Collaborator, DevServerProjectId, PendingEntitySubscription, ProjectId, @@ -44,11 +45,10 @@ use gpui::{ }; use itertools::Itertools; use language::{ - language_settings::InlayHintKind, - proto::{deserialize_anchor, serialize_anchor, split_operations}, - Buffer, BufferEvent, CachedLspAdapter, Capability, CodeLabel, ContextProvider, DiagnosticEntry, - Documentation, File as _, Language, LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, - ToPointUtf16, Transaction, Unclipped, + language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent, + CachedLspAdapter, Capability, CodeLabel, DiagnosticEntry, Documentation, File as _, Language, + LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{ CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId, @@ -56,16 +56,13 @@ use lsp::{ use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::{Mutex, RwLock}; -use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path}; pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; use remote::SshRemoteClient; use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode}; use search::{SearchInputKind, SearchQuery, SearchResult}; use search_history::SearchHistory; -use settings::{ - watch_config_file, InvalidSettingsError, Settings, SettingsLocation, SettingsStore, -}; +use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore}; use smol::channel::Receiver; use snippet::Snippet; use snippet_provider::SnippetProvider; @@ -77,10 +74,7 @@ use std::{ sync::Arc, time::Duration, }; -use task::{ - static_source::{StaticSource, TrackedFile}, - HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, TaskVariables, VariableName, -}; +use task_store::TaskStore; use terminals::Terminals; use text::{Anchor, BufferId}; use util::{paths::compare_paths, ResultExt as _}; @@ -141,6 +135,7 @@ pub struct Project { languages: Arc, client: Arc, join_project_response_message_id: u32, + task_store: Model, user_store: Model, fs: Arc, ssh_client: Option>, @@ -156,7 +151,6 @@ pub struct Project { remotely_created_models: Arc>, terminals: Terminals, node: Option, - tasks: Model, hosted_project_id: Option, dev_server_project_id: Option, search_history: SearchHistory, @@ -567,14 +561,13 @@ impl Project { client.add_model_request_handler(Self::handle_open_buffer_by_id); client.add_model_request_handler(Self::handle_open_buffer_by_path); client.add_model_request_handler(Self::handle_open_new_buffer); - client.add_model_request_handler(Self::handle_task_context_for_location); - client.add_model_request_handler(Self::handle_task_templates); client.add_model_message_handler(Self::handle_create_buffer_for_peer); WorktreeStore::init(&client); BufferStore::init(&client); LspStore::init(&client); SettingsObserver::init(&client); + TaskStore::init(Some(&client)); } pub fn local( @@ -590,7 +583,6 @@ impl Project { let (tx, rx) = mpsc::unbounded(); cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) .detach(); - let tasks = Inventory::new(cx); let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx); let worktree_store = cx.new_model(|_| WorktreeStore::local(false, fs.clone())); cx.subscribe(&worktree_store, Self::on_worktree_store_event) @@ -610,13 +602,29 @@ impl Project { ) }); + let environment = ProjectEnvironment::new(&worktree_store, env, cx); + + let task_store = cx.new_model(|cx| { + TaskStore::local( + fs.clone(), + buffer_store.downgrade(), + worktree_store.clone(), + environment.clone(), + cx, + ) + }); + let settings_observer = cx.new_model(|cx| { - SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx) + SettingsObserver::new_local( + fs.clone(), + worktree_store.clone(), + task_store.clone(), + cx, + ) }); cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); - let environment = ProjectEnvironment::new(&worktree_store, env, cx); let lsp_store = cx.new_model(|cx| { LspStore::new_local( buffer_store.clone(), @@ -645,6 +653,7 @@ impl Project { snippets, languages, client, + task_store, user_store, settings_observer, fs, @@ -655,7 +664,6 @@ impl Project { local_handles: Vec::new(), }, node: Some(node), - tasks, hosted_project_id: None, dev_server_project_id: None, search_history: Self::new_search_history(), @@ -681,7 +689,6 @@ impl Project { let (tx, rx) = mpsc::unbounded(); cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) .detach(); - let tasks = Inventory::new(cx); let global_snippets_dir = paths::config_dir().join("snippets"); let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); @@ -703,8 +710,24 @@ impl Project { cx.subscribe(&buffer_store, Self::on_buffer_store_event) .detach(); + let task_store = cx.new_model(|cx| { + TaskStore::remote( + fs.clone(), + buffer_store.downgrade(), + worktree_store.clone(), + ssh.read(cx).to_proto_client(), + SSH_PROJECT_ID, + cx, + ) + }); + let settings_observer = cx.new_model(|cx| { - SettingsObserver::new_ssh(ssh_proto.clone(), worktree_store.clone(), cx) + SettingsObserver::new_ssh( + ssh_proto.clone(), + worktree_store.clone(), + task_store.clone(), + cx, + ) }); cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); @@ -748,6 +771,7 @@ impl Project { snippets, languages, client, + task_store, user_store, settings_observer, fs, @@ -758,7 +782,6 @@ impl Project { local_handles: Vec::new(), }, node: Some(node), - tasks, hosted_project_id: None, dev_server_project_id: None, search_history: Self::new_search_history(), @@ -783,6 +806,7 @@ impl Project { BufferStore::init(&ssh_proto); LspStore::init(&ssh_proto); SettingsObserver::init(&ssh_proto); + TaskStore::init(Some(&ssh_proto)); this }) @@ -836,6 +860,7 @@ impl Project { response, subscriptions, client, + false, user_store, languages, fs, @@ -844,10 +869,12 @@ impl Project { .await } + #[allow(clippy::too_many_arguments)] async fn from_join_project_response( response: TypedEnvelope, subscriptions: [EntitySubscription; 5], client: Arc, + run_tasks: bool, user_store: Model, languages: Arc, fs: Arc, @@ -884,12 +911,27 @@ impl Project { lsp_store })?; - let settings_observer = - cx.new_model(|cx| SettingsObserver::new_remote(worktree_store.clone(), cx))?; + let task_store = cx.new_model(|cx| { + if run_tasks { + TaskStore::remote( + fs.clone(), + buffer_store.downgrade(), + worktree_store.clone(), + client.clone().into(), + remote_id, + cx, + ) + } else { + TaskStore::Noop + } + })?; + + let settings_observer = cx.new_model(|cx| { + SettingsObserver::new_remote(worktree_store.clone(), task_store.clone(), cx) + })?; let this = cx.new_model(|cx| { let replica_id = response.payload.replica_id as ReplicaId; - let tasks = Inventory::new(cx); let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx); @@ -923,6 +965,7 @@ impl Project { join_project_response_message_id: response.message_id, languages, user_store: user_store.clone(), + task_store, snippets, fs, ssh_client: None, @@ -943,7 +986,6 @@ impl Project { local_handles: Vec::new(), }, node: None, - tasks, hosted_project_id: None, dev_server_project_id: response .payload @@ -1032,6 +1074,7 @@ impl Project { response, subscriptions, client, + true, user_store, languages, fs, @@ -1283,8 +1326,8 @@ impl Project { } } - pub fn task_inventory(&self) -> &Model { - &self.tasks + pub fn task_store(&self) -> &Model { + &self.task_store } pub fn snippets(&self) -> &Model { @@ -1505,6 +1548,9 @@ impl Project { self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.shared(project_id, self.client.clone().into(), cx) }); + self.task_store.update(cx, |task_store, cx| { + task_store.shared(project_id, self.client.clone().into(), cx); + }); self.settings_observer.update(cx, |settings_observer, cx| { settings_observer.shared(project_id, self.client.clone().into(), cx) }); @@ -1593,9 +1639,13 @@ impl Project { buffer_store.forget_shared_buffers(); buffer_store.unshared(cx) }); + self.task_store.update(cx, |task_store, cx| { + task_store.unshared(cx); + }); self.settings_observer.update(cx, |settings_observer, cx| { settings_observer.unshared(cx); }); + self.client .send(proto::UnshareProject { project_id: remote_id, @@ -2105,29 +2155,23 @@ impl Project { } } cx.observe(worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(worktree, |this, worktree, event, cx| { - let is_local = worktree.read(cx).is_local(); - match event { - worktree::Event::UpdatedEntries(changes) => { - if is_local { - this.update_local_worktree_settings(&worktree, changes, cx); - } + cx.subscribe(worktree, |project, worktree, event, cx| match event { + worktree::Event::UpdatedEntries(changes) => { + cx.emit(Event::WorktreeUpdatedEntries( + worktree.read(cx).id(), + changes.clone(), + )); - cx.emit(Event::WorktreeUpdatedEntries( - worktree.read(cx).id(), - changes.clone(), - )); - - let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); - this.client() - .telemetry() - .report_discovered_project_events(worktree_id, changes); - } - worktree::Event::UpdatedGitRepositories(_) => { - cx.emit(Event::WorktreeUpdatedGitRepositories); - } - worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(*id)), + let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); + project + .client() + .telemetry() + .report_discovered_project_events(worktree_id, changes); } + worktree::Event::UpdatedGitRepositories(_) => { + cx.emit(Event::WorktreeUpdatedGitRepositories); + } + worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(*id)), }) .detach(); cx.notify(); @@ -2157,10 +2201,6 @@ impl Project { return; } - self.task_inventory().update(cx, |inventory, _| { - inventory.remove_worktree_sources(id_to_remove); - }); - cx.notify(); } @@ -3139,77 +3179,6 @@ impl Project { }); } - fn update_local_worktree_settings( - &mut self, - worktree: &Model, - changes: &UpdatedEntriesSet, - cx: &mut ModelContext, - ) { - if worktree.read(cx).is_remote() { - return; - } - let remote_worktree_id = worktree.read(cx).id(); - - for (path, _, change) in changes.iter() { - let removed = change == &PathChange::Removed; - let abs_path = match worktree.read(cx).absolutize(path) { - Ok(abs_path) => abs_path, - Err(e) => { - log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}"); - continue; - } - }; - - if path.ends_with(local_tasks_file_relative_path()) { - self.task_inventory().update(cx, |task_inventory, cx| { - if removed { - task_inventory.remove_local_static_source(&abs_path); - } else { - let fs = self.fs.clone(); - let task_abs_path = abs_path.clone(); - let tasks_file_rx = - watch_config_file(cx.background_executor(), fs, task_abs_path); - task_inventory.add_source( - TaskSourceKind::Worktree { - id: remote_worktree_id, - abs_path, - id_base: "local_tasks_for_worktree".into(), - }, - |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)), - cx, - ); - } - }) - } else if path.ends_with(local_vscode_tasks_file_relative_path()) { - self.task_inventory().update(cx, |task_inventory, cx| { - if removed { - task_inventory.remove_local_static_source(&abs_path); - } else { - let fs = self.fs.clone(); - let task_abs_path = abs_path.clone(); - let tasks_file_rx = - watch_config_file(cx.background_executor(), fs, task_abs_path); - task_inventory.add_source( - TaskSourceKind::Worktree { - id: remote_worktree_id, - abs_path, - id_base: "local_vscode_tasks_for_worktree".into(), - }, - |tx, cx| { - StaticSource::new(TrackedFile::new_convertible::< - task::VsCodeTaskFile, - >( - tasks_file_rx, tx, cx - )) - }, - cx, - ); - } - }) - } - } - } - pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; @@ -3518,7 +3487,7 @@ impl Project { let buffer_store = this.read_with(&cx, |this, cx| { if let Some(ssh) = &this.ssh_client { let mut payload = envelope.payload.clone(); - payload.project_id = 0; + payload.project_id = SSH_PROJECT_ID; cx.background_executor() .spawn(ssh.read(cx).to_proto_client().request(payload)) .detach_and_log_err(cx); @@ -3578,137 +3547,6 @@ impl Project { Ok(response) } - async fn handle_task_context_for_location( - project: Model, - envelope: TypedEnvelope, - mut cx: AsyncAppContext, - ) -> Result { - let location = envelope - .payload - .location - .context("no location given for task context handling")?; - let location = cx - .update(|cx| deserialize_location(&project, location, cx))? - .await?; - let context_task = project.update(&mut cx, |project, cx| { - let captured_variables = { - let mut variables = TaskVariables::default(); - for range in location - .buffer - .read(cx) - .snapshot() - .runnable_ranges(location.range.clone()) - { - for (capture_name, value) in range.extra_captures { - variables.insert(VariableName::Custom(capture_name.into()), value); - } - } - variables - }; - project.task_context_for_location(captured_variables, location, cx) - })?; - let task_context = context_task.await.unwrap_or_default(); - Ok(proto::TaskContext { - project_env: task_context.project_env.into_iter().collect(), - cwd: task_context - .cwd - .map(|cwd| cwd.to_string_lossy().to_string()), - task_variables: task_context - .task_variables - .into_iter() - .map(|(variable_name, variable_value)| (variable_name.to_string(), variable_value)) - .collect(), - }) - } - - async fn handle_task_templates( - project: Model, - envelope: TypedEnvelope, - mut cx: AsyncAppContext, - ) -> Result { - let worktree = envelope.payload.worktree_id.map(WorktreeId::from_proto); - let location = match envelope.payload.location { - Some(location) => Some( - cx.update(|cx| deserialize_location(&project, location, cx))? - .await - .context("task templates request location deserializing")?, - ), - None => None, - }; - - let templates = project - .update(&mut cx, |project, cx| { - project.task_templates(worktree, location, cx) - })? - .await - .context("receiving task templates")? - .into_iter() - .map(|(kind, template)| { - let kind = Some(match kind { - TaskSourceKind::UserInput => proto::task_source_kind::Kind::UserInput( - proto::task_source_kind::UserInput {}, - ), - TaskSourceKind::Worktree { - id, - abs_path, - id_base, - } => { - proto::task_source_kind::Kind::Worktree(proto::task_source_kind::Worktree { - id: id.to_proto(), - abs_path: abs_path.to_string_lossy().to_string(), - id_base: id_base.to_string(), - }) - } - TaskSourceKind::AbsPath { id_base, abs_path } => { - proto::task_source_kind::Kind::AbsPath(proto::task_source_kind::AbsPath { - abs_path: abs_path.to_string_lossy().to_string(), - id_base: id_base.to_string(), - }) - } - TaskSourceKind::Language { name } => { - proto::task_source_kind::Kind::Language(proto::task_source_kind::Language { - name: name.to_string(), - }) - } - }); - let kind = Some(proto::TaskSourceKind { kind }); - let template = Some(proto::TaskTemplate { - label: template.label, - command: template.command, - args: template.args, - env: template.env.into_iter().collect(), - cwd: template.cwd, - use_new_terminal: template.use_new_terminal, - allow_concurrent_runs: template.allow_concurrent_runs, - reveal: match template.reveal { - RevealStrategy::Always => proto::RevealStrategy::RevealAlways as i32, - RevealStrategy::Never => proto::RevealStrategy::RevealNever as i32, - }, - hide: match template.hide { - HideStrategy::Always => proto::HideStrategy::HideAlways as i32, - HideStrategy::Never => proto::HideStrategy::HideNever as i32, - HideStrategy::OnSuccess => proto::HideStrategy::HideOnSuccess as i32, - }, - shell: Some(proto::Shell { - shell_type: Some(match template.shell { - Shell::System => proto::shell::ShellType::System(proto::System {}), - Shell::Program(program) => proto::shell::ShellType::Program(program), - Shell::WithArguments { program, args } => { - proto::shell::ShellType::WithArguments( - proto::shell::WithArguments { program, args }, - ) - } - }), - }), - tags: template.tags, - }); - proto::TemplatePair { kind, template } - }) - .collect(); - - Ok(proto::TaskTemplatesResponse { templates }) - } - async fn handle_search_candidate_buffers( this: Model, envelope: TypedEnvelope, @@ -3996,267 +3834,6 @@ impl Project { .read(cx) .language_server_for_buffer(buffer, server_id, cx) } - - pub fn task_context_for_location( - &self, - captured_variables: TaskVariables, - location: Location, - cx: &mut ModelContext<'_, Project>, - ) -> Task> { - if self.is_local() { - let (worktree_id, worktree_abs_path) = if let Some(worktree) = self.task_worktree(cx) { - ( - Some(worktree.read(cx).id()), - Some(worktree.read(cx).abs_path()), - ) - } else { - (None, None) - }; - - cx.spawn(|project, mut cx| async move { - let project_env = project - .update(&mut cx, |project, cx| { - let worktree_abs_path = worktree_abs_path.clone(); - project.environment.update(cx, |environment, cx| { - environment.get_environment(worktree_id, worktree_abs_path, cx) - }) - }) - .ok()? - .await; - - let mut task_variables = cx - .update(|cx| { - combine_task_variables( - captured_variables, - location, - project_env.as_ref(), - BasicContextProvider::new(project.upgrade()?), - cx, - ) - .log_err() - }) - .ok() - .flatten()?; - // Remove all custom entries starting with _, as they're not intended for use by the end user. - task_variables.sweep(); - - Some(TaskContext { - project_env: project_env.unwrap_or_default(), - cwd: worktree_abs_path.map(|p| p.to_path_buf()), - task_variables, - }) - }) - } else if let Some(project_id) = self - .remote_id() - .filter(|_| self.ssh_connection_string(cx).is_some()) - { - let task_context = self.client().request(proto::TaskContextForLocation { - project_id, - location: Some(proto::Location { - buffer_id: location.buffer.read(cx).remote_id().into(), - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - }), - }); - cx.background_executor().spawn(async move { - let task_context = task_context.await.log_err()?; - Some(TaskContext { - project_env: task_context.project_env.into_iter().collect(), - cwd: task_context.cwd.map(PathBuf::from), - task_variables: task_context - .task_variables - .into_iter() - .filter_map( - |(variable_name, variable_value)| match variable_name.parse() { - Ok(variable_name) => Some((variable_name, variable_value)), - Err(()) => { - log::error!("Unknown variable name: {variable_name}"); - None - } - }, - ) - .collect(), - }) - }) - } else { - Task::ready(None) - } - } - - pub fn task_templates( - &self, - worktree: Option, - location: Option, - cx: &mut ModelContext, - ) -> Task>> { - if self.is_local() { - let (file, language) = location - .map(|location| { - let buffer = location.buffer.read(cx); - ( - buffer.file().cloned(), - buffer.language_at(location.range.start), - ) - }) - .unwrap_or_default(); - Task::ready(Ok(self - .task_inventory() - .read(cx) - .list_tasks(file, language, worktree, cx))) - } else if let Some(project_id) = self - .remote_id() - .filter(|_| self.ssh_connection_string(cx).is_some()) - { - let remote_templates = - self.query_remote_task_templates(project_id, worktree, location.as_ref(), cx); - cx.background_executor().spawn(remote_templates) - } else { - Task::ready(Ok(Vec::new())) - } - } - - pub fn query_remote_task_templates( - &self, - project_id: u64, - worktree: Option, - location: Option<&Location>, - cx: &AppContext, - ) -> Task>> { - let client = self.client(); - let location = location.map(|location| serialize_location(location, cx)); - cx.spawn(|_| async move { - let response = client - .request(proto::TaskTemplates { - project_id, - worktree_id: worktree.map(|id| id.to_proto()), - location, - }) - .await?; - - Ok(response - .templates - .into_iter() - .filter_map(|template_pair| { - let task_source_kind = match template_pair.kind?.kind? { - proto::task_source_kind::Kind::UserInput(_) => TaskSourceKind::UserInput, - proto::task_source_kind::Kind::Worktree(worktree) => { - TaskSourceKind::Worktree { - id: WorktreeId::from_proto(worktree.id), - abs_path: PathBuf::from(worktree.abs_path), - id_base: Cow::Owned(worktree.id_base), - } - } - proto::task_source_kind::Kind::AbsPath(abs_path) => { - TaskSourceKind::AbsPath { - id_base: Cow::Owned(abs_path.id_base), - abs_path: PathBuf::from(abs_path.abs_path), - } - } - proto::task_source_kind::Kind::Language(language) => { - TaskSourceKind::Language { - name: language.name.into(), - } - } - }; - - let proto_template = template_pair.template?; - let reveal = match proto::RevealStrategy::from_i32(proto_template.reveal) - .unwrap_or(proto::RevealStrategy::RevealAlways) - { - proto::RevealStrategy::RevealAlways => RevealStrategy::Always, - proto::RevealStrategy::RevealNever => RevealStrategy::Never, - }; - let hide = match proto::HideStrategy::from_i32(proto_template.hide) - .unwrap_or(proto::HideStrategy::HideNever) - { - proto::HideStrategy::HideAlways => HideStrategy::Always, - proto::HideStrategy::HideNever => HideStrategy::Never, - proto::HideStrategy::HideOnSuccess => HideStrategy::OnSuccess, - }; - let shell = match proto_template - .shell - .and_then(|shell| shell.shell_type) - .unwrap_or(proto::shell::ShellType::System(proto::System {})) - { - proto::shell::ShellType::System(_) => Shell::System, - proto::shell::ShellType::Program(program) => Shell::Program(program), - proto::shell::ShellType::WithArguments(with_arguments) => { - Shell::WithArguments { - program: with_arguments.program, - args: with_arguments.args, - } - } - }; - let task_template = TaskTemplate { - label: proto_template.label, - command: proto_template.command, - args: proto_template.args, - env: proto_template.env.into_iter().collect(), - cwd: proto_template.cwd, - use_new_terminal: proto_template.use_new_terminal, - allow_concurrent_runs: proto_template.allow_concurrent_runs, - reveal, - hide, - shell, - tags: proto_template.tags, - }; - Some((task_source_kind, task_template)) - }) - .collect()) - }) - } - - fn task_worktree(&self, cx: &AppContext) -> Option> { - let available_worktrees = self - .worktrees(cx) - .filter(|worktree| { - let worktree = worktree.read(cx); - worktree.is_visible() - && worktree.is_local() - && worktree.root_entry().map_or(false, |e| e.is_dir()) - }) - .collect::>(); - - match available_worktrees.len() { - 0 => None, - 1 => Some(available_worktrees[0].clone()), - _ => self.active_entry().and_then(|entry_id| { - available_worktrees.into_iter().find_map(|worktree| { - if worktree.read(cx).contains_entry(entry_id) { - Some(worktree) - } else { - None - } - }) - }), - } - } -} - -fn combine_task_variables( - mut captured_variables: TaskVariables, - location: Location, - project_env: Option<&HashMap>, - baseline: BasicContextProvider, - cx: &mut AppContext, -) -> anyhow::Result { - let language_context_provider = location - .buffer - .read(cx) - .language() - .and_then(|language| language.context_provider()); - let baseline = baseline - .build_context(&captured_variables, &location, project_env, cx) - .context("building basic default context")?; - captured_variables.extend(baseline); - if let Some(provider) = language_context_provider { - captured_variables.extend( - provider - .build_context(&captured_variables, &location, project_env, cx) - .context("building provider context")?, - ); - } - Ok(captured_variables) } fn deserialize_code_actions(code_actions: &HashMap) -> Vec { @@ -4509,43 +4086,6 @@ impl std::fmt::Display for NoRepositoryError { impl std::error::Error for NoRepositoryError {} -fn serialize_location(location: &Location, cx: &AppContext) -> proto::Location { - proto::Location { - buffer_id: location.buffer.read(cx).remote_id().into(), - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - } -} - -fn deserialize_location( - project: &Model, - location: proto::Location, - cx: &mut AppContext, -) -> Task> { - let buffer_id = match BufferId::new(location.buffer_id) { - Ok(id) => id, - Err(e) => return Task::ready(Err(e)), - }; - let buffer_task = project.update(cx, |project, cx| { - project.wait_for_remote_buffer(buffer_id, cx) - }); - cx.spawn(|_| async move { - let buffer = buffer_task.await?; - let start = location - .start - .and_then(deserialize_anchor) - .context("missing task context location start")?; - let end = location - .end - .and_then(deserialize_anchor) - .context("missing task context location end")?; - Ok(Location { - buffer, - range: start..end, - }) - }) -} - pub fn sort_worktree_entries(entries: &mut [Entry]) { entries.sort_by(|entry_a, entry_b| { compare_paths( diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index e956f67260..49378fbc1d 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -3,20 +3,30 @@ use collections::HashMap; use fs::Fs; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext}; use language::LanguageServerName; -use paths::local_settings_file_relative_path; +use paths::{ + local_settings_file_relative_path, local_tasks_file_relative_path, + local_vscode_tasks_file_relative_path, +}; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{InvalidSettingsError, LocalSettingsKind, Settings, SettingsSources, SettingsStore}; +use settings::{ + parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, + SettingsSources, SettingsStore, +}; use std::{ path::{Path, PathBuf}, sync::Arc, time::Duration, }; +use task::{TaskTemplates, VsCodeTaskFile}; use util::ResultExt; use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId}; -use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent}; +use crate::{ + task_store::TaskStore, + worktree_store::{WorktreeStore, WorktreeStoreEvent}, +}; #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ProjectSettings { @@ -202,12 +212,13 @@ pub struct SettingsObserver { downstream_client: Option, worktree_store: Model, project_id: u64, + task_store: Model, } -/// SettingsObserver observers changes to .zed/settings.json files in local worktrees +/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees /// (or the equivalent protobuf messages from upstream) and updates local settings /// and sends notifications downstream. -/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content +/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content /// upstream. impl SettingsObserver { pub fn init(client: &AnyProtoClient) { @@ -218,6 +229,7 @@ impl SettingsObserver { pub fn new_local( fs: Arc, worktree_store: Model, + task_store: Model, cx: &mut ModelContext, ) -> Self { cx.subscribe(&worktree_store, Self::on_worktree_store_event) @@ -225,6 +237,7 @@ impl SettingsObserver { Self { worktree_store, + task_store, mode: SettingsObserverMode::Local(fs), downstream_client: None, project_id: 0, @@ -234,10 +247,12 @@ impl SettingsObserver { pub fn new_ssh( client: AnyProtoClient, worktree_store: Model, + task_store: Model, cx: &mut ModelContext, ) -> Self { let this = Self { worktree_store, + task_store, mode: SettingsObserverMode::Ssh(client.clone()), downstream_client: None, project_id: 0, @@ -246,9 +261,14 @@ impl SettingsObserver { this } - pub fn new_remote(worktree_store: Model, _: &mut ModelContext) -> Self { + pub fn new_remote( + worktree_store: Model, + task_store: Model, + _: &mut ModelContext, + ) -> Self { Self { worktree_store, + task_store, mode: SettingsObserverMode::Remote, downstream_client: None, project_id: 0, @@ -319,19 +339,32 @@ impl SettingsObserver { } pub async fn handle_update_user_settings( - _: Model, + settings_observer: Model, envelope: TypedEnvelope, - cx: AsyncAppContext, + mut cx: AsyncAppContext, ) -> anyhow::Result<()> { - cx.update_global(move |settings_store: &mut SettingsStore, cx| { - settings_store.set_user_settings(&envelope.payload.content, cx) - })??; + match envelope.payload.kind() { + proto::update_user_settings::Kind::Settings => { + cx.update_global(move |settings_store: &mut SettingsStore, cx| { + settings_store.set_user_settings(&envelope.payload.content, cx) + }) + } + proto::update_user_settings::Kind::Tasks => { + settings_observer.update(&mut cx, |settings_observer, cx| { + settings_observer.task_store.update(cx, |task_store, cx| { + task_store.update_user_tasks(None, Some(&envelope.payload.content), cx) + }) + }) + } + }??; Ok(()) } pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext) { - let mut settings = cx.global::().raw_user_settings().clone(); + let settings_store = cx.global::(); + + let mut settings = settings_store.raw_user_settings().clone(); if let Some(content) = serde_json::to_string(&settings).log_err() { ssh.send(proto::UpdateUserSettings { project_id: 0, @@ -389,7 +422,43 @@ impl SettingsObserver { let mut settings_contents = Vec::new(); for (path, _, change) in changes.iter() { + let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) { + let settings_dir = Arc::::from( + path.ancestors() + .nth(local_settings_file_relative_path().components().count()) + .unwrap(), + ); + (settings_dir, LocalSettingsKind::Settings) + } else if path.ends_with(local_tasks_file_relative_path()) { + let settings_dir = Arc::::from( + path.ancestors() + .nth( + local_tasks_file_relative_path() + .components() + .count() + .saturating_sub(1), + ) + .unwrap(), + ); + (settings_dir, LocalSettingsKind::Tasks) + } else if path.ends_with(local_vscode_tasks_file_relative_path()) { + let settings_dir = Arc::::from( + path.ancestors() + .nth( + local_vscode_tasks_file_relative_path() + .components() + .count() + .saturating_sub(1), + ) + .unwrap(), + ); + (settings_dir, LocalSettingsKind::Tasks) + } else { + continue; + }; + let removed = change == &PathChange::Removed; + let fs = fs.clone(); let abs_path = match worktree.read(cx).absolutize(path) { Ok(abs_path) => abs_path, Err(e) => { @@ -397,26 +466,42 @@ impl SettingsObserver { continue; } }; - - if path.ends_with(local_settings_file_relative_path()) { - let settings_dir = Arc::from( - path.ancestors() - .nth(local_settings_file_relative_path().components().count()) - .unwrap(), - ); - let fs = fs.clone(); - settings_contents.push(async move { - ( - settings_dir, - LocalSettingsKind::Settings, - if removed { - None - } else { - Some(async move { fs.load(&abs_path).await }.await) - }, - ) - }); - } + settings_contents.push(async move { + ( + settings_dir, + kind, + if removed { + None + } else { + Some( + async move { + let content = fs.load(&abs_path).await?; + if abs_path.ends_with(local_vscode_tasks_file_relative_path()) { + let vscode_tasks = + parse_json_with_comments::(&content) + .with_context(|| { + format!("parsing VSCode tasks, file {abs_path:?}") + })?; + let zed_tasks = TaskTemplates::try_from(vscode_tasks) + .with_context(|| { + format!( + "converting VSCode tasks into Zed ones, file {abs_path:?}" + ) + })?; + serde_json::to_string(&zed_tasks).with_context(|| { + format!( + "serializing Zed tasks into JSON, file {abs_path:?}" + ) + }) + } else { + Ok(content) + } + } + .await, + ) + }, + ) + }); } if settings_contents.is_empty() { @@ -450,47 +535,64 @@ impl SettingsObserver { ) { let worktree_id = worktree.read(cx).id(); let remote_worktree_id = worktree.read(cx).id(); + let task_store = self.task_store.clone(); - let result = cx.update_global::>(|store, cx| { - for (directory, kind, file_content) in settings_contents { - store.set_local_settings( - worktree_id, - directory.clone(), - kind, - file_content.as_deref(), - cx, - )?; + for (directory, kind, file_content) in settings_contents { + let result = match kind { + LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx + .update_global::>(|store, cx| { + store.set_local_settings( + worktree_id, + directory.clone(), + kind, + file_content.as_deref(), + cx, + ) + }), + LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| { + task_store.update_user_tasks( + Some(SettingsLocation { + worktree_id, + path: directory.as_ref(), + }), + file_content.as_deref(), + cx, + ) + }), + }; - if let Some(downstream_client) = &self.downstream_client { - downstream_client - .send(proto::UpdateWorktreeSettings { - project_id: self.project_id, - worktree_id: remote_worktree_id.to_proto(), - path: directory.to_string_lossy().into_owned(), - content: file_content, - kind: Some(local_settings_kind_to_proto(kind).into()), - }) - .log_err(); - } + if let Some(downstream_client) = &self.downstream_client { + downstream_client + .send(proto::UpdateWorktreeSettings { + project_id: self.project_id, + worktree_id: remote_worktree_id.to_proto(), + path: directory.to_string_lossy().into_owned(), + content: file_content, + kind: Some(local_settings_kind_to_proto(kind).into()), + }) + .log_err(); } - anyhow::Ok(()) - }); - match result { - Err(error) => { - if let Ok(error) = error.downcast::() { - if let InvalidSettingsError::LocalSettings { - ref path, - ref message, - } = error - { - log::error!("Failed to set local settings in {:?}: {:?}", path, message); - cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error))); + match result { + Err(error) => { + if let Ok(error) = error.downcast::() { + if let InvalidSettingsError::LocalSettings { + ref path, + ref message, + } = error + { + log::error!( + "Failed to set local settings in {:?}: {:?}", + path, + message + ); + cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error))); + } } } - } - Ok(()) => { - cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(()))); + Ok(()) => { + cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(()))); + } } } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index dd14ccd60f..ee448543be 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -16,7 +16,7 @@ use serde_json::json; use std::os; use std::{mem, ops::Range, task::Poll}; -use task::{ResolvedTask, TaskContext, TaskTemplate, TaskTemplates}; +use task::{ResolvedTask, TaskContext}; use unindent::Unindent as _; use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt as _}; @@ -94,6 +94,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { init_test(cx); + TaskStore::init(None); let fs = FakeFs::new(cx.executor()); fs.insert_tree( @@ -102,7 +103,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ".zed": { "settings.json": r#"{ "tab_size": 8 }"#, "tasks.json": r#"[{ - "label": "cargo check", + "label": "cargo check all", "command": "cargo", "args": ["check", "--all"] },]"#, @@ -135,10 +136,10 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) project.worktrees(cx).next().unwrap().read(cx).id() }) }); - let global_task_source_kind = TaskSourceKind::Worktree { + let topmost_local_task_source_kind = TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from("/the-root/.zed/tasks.json"), - id_base: "local_tasks_for_worktree".into(), + directory_in_worktree: PathBuf::from(".zed"), + id_base: "local worktree tasks from directory \".zed\"".into(), }; let all_tasks = cx @@ -171,7 +172,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) get_all_tasks(&project, Some(worktree_id), &task_context, cx) }) - .await .into_iter() .map(|(source_kind, task)| { let resolved = task.resolved.unwrap(); @@ -186,71 +186,65 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) assert_eq!( all_tasks, vec![ - ( - global_task_source_kind.clone(), - "cargo check".to_string(), - vec!["check".to_string(), "--all".to_string()], - HashMap::default(), - ), ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), - id_base: "local_tasks_for_worktree".into(), + directory_in_worktree: PathBuf::from("b/.zed"), + id_base: "local worktree tasks from directory \"b/.zed\"".into(), }, "cargo check".to_string(), vec!["check".to_string()], HashMap::default(), ), + ( + topmost_local_task_source_kind.clone(), + "cargo check all".to_string(), + vec!["check".to_string(), "--all".to_string()], + HashMap::default(), + ), ] ); let (_, resolved_task) = cx .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx)) - .await .into_iter() - .find(|(source_kind, _)| source_kind == &global_task_source_kind) + .find(|(source_kind, _)| source_kind == &topmost_local_task_source_kind) .expect("should have one global task"); project.update(cx, |project, cx| { - project.task_inventory().update(cx, |inventory, _| { - inventory.task_scheduled(global_task_source_kind.clone(), resolved_task); + let task_inventory = project + .task_store + .read(cx) + .task_inventory() + .cloned() + .unwrap(); + task_inventory.update(cx, |inventory, _| { + inventory.task_scheduled(topmost_local_task_source_kind.clone(), resolved_task); + inventory + .update_file_based_tasks( + None, + Some( + &json!([{ + "label": "cargo check unstable", + "command": "cargo", + "args": [ + "check", + "--all", + "--all-targets" + ], + "env": { + "RUSTFLAGS": "-Zunstable-options" + } + }]) + .to_string(), + ), + ) + .unwrap(); }); }); - - let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate { - label: "cargo check".to_string(), - command: "cargo".to_string(), - args: vec![ - "check".to_string(), - "--all".to_string(), - "--all-targets".to_string(), - ], - env: HashMap::from_iter(Some(( - "RUSTFLAGS".to_string(), - "-Zunstable-options".to_string(), - ))), - ..TaskTemplate::default() - }])) - .unwrap(); - let (tx, rx) = futures::channel::mpsc::unbounded(); - 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(); - cx.run_until_parked(); + let all_tasks = cx .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx)) - .await .into_iter() .map(|(source_kind, task)| { let resolved = task.resolved.unwrap(); @@ -265,33 +259,38 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) assert_eq!( all_tasks, vec![ + ( + topmost_local_task_source_kind.clone(), + "cargo check all".to_string(), + vec!["check".to_string(), "--all".to_string()], + HashMap::default(), + ), ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from("/the-root/.zed/tasks.json"), - id_base: "local_tasks_for_worktree".into(), + directory_in_worktree: PathBuf::from("b/.zed"), + id_base: "local worktree tasks from directory \"b/.zed\"".into(), }, "cargo check".to_string(), + vec!["check".to_string()], + HashMap::default(), + ), + ( + TaskSourceKind::AbsPath { + abs_path: paths::tasks_file().clone(), + id_base: "global tasks.json".into(), + }, + "cargo check unstable".to_string(), vec![ "check".to_string(), "--all".to_string(), - "--all-targets".to_string() + "--all-targets".to_string(), ], HashMap::from_iter(Some(( "RUSTFLAGS".to_string(), "-Zunstable-options".to_string() ))), ), - ( - TaskSourceKind::Worktree { - id: worktree_id, - abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), - id_base: "local_tasks_for_worktree".into(), - }, - "cargo check".to_string(), - vec!["check".to_string()], - HashMap::default(), - ), ] ); } @@ -5416,17 +5415,16 @@ fn get_all_tasks( worktree_id: Option, task_context: &TaskContext, cx: &mut AppContext, -) -> Task> { - let resolved_tasks = project.update(cx, |project, cx| { +) -> Vec<(TaskSourceKind, ResolvedTask)> { + let (mut old, new) = project.update(cx, |project, cx| { project - .task_inventory() + .task_store .read(cx) - .used_and_current_resolved_tasks(None, worktree_id, None, task_context, cx) + .task_inventory() + .unwrap() + .read(cx) + .used_and_current_resolved_tasks(worktree_id, None, task_context, cx) }); - - cx.spawn(|_| async move { - let (mut old, new) = resolved_tasks.await; - old.extend(new); - old - }) + old.extend(new); + old } diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 83c9c1f8e5..eb982fdcd7 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -3,40 +3,37 @@ use std::{ borrow::Cow, cmp::{self, Reverse}, + collections::hash_map, path::{Path, PathBuf}, sync::Arc, }; -use anyhow::Result; -use collections::{btree_map, BTreeMap, HashMap, VecDeque}; -use futures::{ - channel::mpsc::{unbounded, UnboundedSender}, - StreamExt, -}; -use gpui::{AppContext, Context, Model, ModelContext, Task}; +use anyhow::{Context, Result}; +use collections::{HashMap, HashSet, VecDeque}; +use gpui::{AppContext, Context as _, Model}; use itertools::Itertools; use language::{ContextProvider, File, Language, Location}; +use settings::{parse_json_with_comments, SettingsLocation}; use task::{ - static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, - TaskVariables, VariableName, + ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName, }; use text::{Point, ToPoint}; -use util::{post_inc, NumericPrefixWithSuffix, ResultExt}; +use util::{post_inc, NumericPrefixWithSuffix, ResultExt as _}; use worktree::WorktreeId; -use crate::Project; +use crate::worktree_store::WorktreeStore; /// Inventory tracks available tasks for a given project. +#[derive(Debug, Default)] pub struct Inventory { - sources: Vec, last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>, - update_sender: UnboundedSender<()>, - _update_pooler: Task>, + templates_from_settings: ParsedTemplates, } -struct SourceInInventory { - source: StaticSource, - kind: TaskSourceKind, +#[derive(Debug, Default)] +struct ParsedTemplates { + global: Vec, + worktree: HashMap, Vec>>, } /// Kind of a source the tasks are fetched from, used to display more source information in the UI. @@ -47,7 +44,7 @@ pub enum TaskSourceKind { /// Tasks from the worktree's .zed/task.json Worktree { id: WorktreeId, - abs_path: PathBuf, + directory_in_worktree: PathBuf, id_base: Cow<'static, str>, }, /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path @@ -60,20 +57,6 @@ pub enum TaskSourceKind { } impl TaskSourceKind { - pub fn abs_path(&self) -> Option<&Path> { - match self { - Self::AbsPath { abs_path, .. } | Self::Worktree { abs_path, .. } => Some(abs_path), - Self::UserInput | Self::Language { .. } => None, - } - } - - pub fn worktree(&self) -> Option { - match self { - Self::Worktree { id, .. } => Some(*id), - _ => None, - } - } - pub fn to_id_base(&self) -> String { match self { TaskSourceKind::UserInput => "oneshot".to_string(), @@ -83,9 +66,9 @@ impl TaskSourceKind { TaskSourceKind::Worktree { id, id_base, - abs_path, + directory_in_worktree, } => { - format!("{id_base}_{id}_{}", abs_path.display()) + format!("{id_base}_{id}_{}", directory_in_worktree.display()) } TaskSourceKind::Language { name } => format!("language_{name}"), } @@ -94,61 +77,7 @@ impl TaskSourceKind { impl Inventory { pub fn new(cx: &mut AppContext) -> Model { - 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, - } - }) - } - - /// If the task with the same path was not added yet, - /// registers a new tasks source to fetch for available tasks later. - /// Unless a source is removed, ignores future additions for the same path. - pub fn add_source( - &mut self, - kind: TaskSourceKind, - create_source: impl FnOnce(UnboundedSender<()>, &mut AppContext) -> StaticSource, - cx: &mut ModelContext, - ) { - let abs_path = kind.abs_path(); - if abs_path.is_some() { - if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) { - log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind); - return; - } - } - let source = create_source(self.update_sender.clone(), cx); - let source = SourceInInventory { source, kind }; - self.sources.push(source); - cx.notify(); - } - - /// If present, removes the local static source entry that has the given path, - /// making corresponding task definitions unavailable in the fetch results. - /// - /// Now, entry for this path can be re-added again. - pub fn remove_local_static_source(&mut self, abs_path: &Path) { - self.sources.retain(|s| s.kind.abs_path() != Some(abs_path)); - } - - /// If present, removes the worktree source entry that has the given worktree id, - /// making corresponding task definitions unavailable in the fetch results. - /// - /// Now, entry for this path can be re-added again. - pub fn remove_worktree_sources(&mut self, worktree: WorktreeId) { - self.sources.retain(|s| s.kind.worktree() != Some(worktree)); + cx.new_model(|_| Self::default()) } /// Pulls its task sources relevant to the worktree and the language given, @@ -167,42 +96,27 @@ impl Inventory { .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) - .flat_map(|task| Some((task_source_kind.as_ref()?, task))); + .flat_map(|task| Some((task_source_kind.clone()?, task))); - self.sources - .iter() - .filter(|source| { - let source_worktree = source.kind.worktree(); - worktree.is_none() || source_worktree.is_none() || source_worktree == worktree - }) - .flat_map(|source| { - source - .source - .tasks_to_schedule() - .0 - .into_iter() - .map(|task| (&source.kind, task)) - }) + self.templates_from_settings(worktree) .chain(language_tasks) - .map(|(task_source_kind, task)| (task_source_kind.clone(), task)) .collect() } /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given. /// Joins the new resolutions with the resolved tasks that were used (spawned) before, /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first. - /// Deduplicates the tasks by their labels and splits the ordered list into two: used tasks and the rest, newly resolved tasks. + /// Deduplicates the tasks by their labels and contenxt and splits the ordered list into two: used tasks and the rest, newly resolved tasks. pub fn used_and_current_resolved_tasks( &self, - remote_templates_task: Option>>>, worktree: Option, location: Option, task_context: &TaskContext, cx: &AppContext, - ) -> Task<( + ) -> ( Vec<(TaskSourceKind, ResolvedTask)>, Vec<(TaskSourceKind, ResolvedTask)>, - )> { + ) { let language = location .as_ref() .and_then(|location| location.buffer.read(cx).language_at(location.range.start)); @@ -212,14 +126,10 @@ impl Inventory { let file = location .as_ref() .and_then(|location| location.buffer.read(cx).file().cloned()); - let language_tasks = language - .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) - .into_iter() - .flat_map(|tasks| tasks.0.into_iter()) - .flat_map(|task| Some((task_source_kind.as_ref()?, task))); + let mut task_labels_to_ids = HashMap::>::default(); let mut lru_score = 0_u32; - let mut task_usage = self + let previously_spawned_tasks = self .last_scheduled_tasks .iter() .rev() @@ -230,127 +140,64 @@ impl Inventory { true } }) - .fold( - BTreeMap::default(), - |mut tasks, (task_source_kind, resolved_task)| { - tasks.entry(&resolved_task.id).or_insert_with(|| { - (task_source_kind, resolved_task, post_inc(&mut lru_score)) - }); - tasks - }, - ); - let not_used_score = post_inc(&mut lru_score); - let mut currently_resolved_tasks = self - .sources - .iter() - .filter(|source| { - let source_worktree = source.kind.worktree(); - worktree.is_none() || source_worktree.is_none() || source_worktree == worktree + .filter(|(_, resolved_task)| { + match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) { + hash_map::Entry::Occupied(mut o) => { + o.get_mut().insert(resolved_task.id.clone()); + // Neber allow duplicate reused tasks with the same labels + false + } + hash_map::Entry::Vacant(v) => { + v.insert(HashSet::from_iter(Some(resolved_task.id.clone()))); + true + } + } }) - .flat_map(|source| { - source - .source - .tasks_to_schedule() - .0 - .into_iter() - .map(|task| (&source.kind, task)) + .map(|(task_source_kind, resolved_task)| { + ( + task_source_kind.clone(), + resolved_task.clone(), + post_inc(&mut lru_score), + ) }) - .chain(language_tasks.filter(|_| remote_templates_task.is_none())) - .filter_map(|(kind, task)| { - let id_base = kind.to_id_base(); - Some((kind, task.resolve_task(&id_base, task_context)?)) - }) - .map(|(kind, task)| { - let lru_score = task_usage - .remove(&task.id) - .map(|(_, _, lru_score)| lru_score) - .unwrap_or(not_used_score); - (kind.clone(), task, lru_score) - }) - .collect::>(); - let previously_spawned_tasks = task_usage - .into_iter() - .map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score)) + .sorted_unstable_by(task_lru_comparator) + .map(|(kind, task, _)| (kind, task)) .collect::>(); - let task_context = task_context.clone(); - cx.spawn(move |_| async move { - let remote_templates = match remote_templates_task { - Some(task) => match task.await.log_err() { - Some(remote_templates) => remote_templates, - None => return (Vec::new(), Vec::new()), - }, - None => Vec::new(), - }; - let remote_tasks = remote_templates.into_iter().filter_map(|(kind, task)| { + let not_used_score = post_inc(&mut lru_score); + let language_tasks = language + .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) + .into_iter() + .flat_map(|tasks| tasks.0.into_iter()) + .flat_map(|task| Some((task_source_kind.clone()?, task))); + let new_resolved_tasks = self + .templates_from_settings(worktree) + .chain(language_tasks) + .filter_map(|(kind, task)| { let id_base = kind.to_id_base(); Some(( kind, - task.resolve_task(&id_base, &task_context)?, + task.resolve_task(&id_base, task_context)?, not_used_score, )) - }); - currently_resolved_tasks.extend(remote_tasks); - - let mut tasks_by_label = BTreeMap::default(); - tasks_by_label = previously_spawned_tasks.into_iter().fold( - tasks_by_label, - |mut tasks_by_label, (source, task, lru_score)| { - match tasks_by_label.entry((source, task.resolved_label.clone())) { - btree_map::Entry::Occupied(mut o) => { - let (_, previous_lru_score) = o.get(); - if previous_lru_score >= &lru_score { - o.insert((task, lru_score)); - } - } - btree_map::Entry::Vacant(v) => { - v.insert((task, lru_score)); - } + }) + .filter(|(_, resolved_task, _)| { + match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) { + hash_map::Entry::Occupied(mut o) => { + // Allow new tasks with the same label, if their context is different + o.get_mut().insert(resolved_task.id.clone()) } - tasks_by_label - }, - ); - tasks_by_label = currently_resolved_tasks.iter().fold( - tasks_by_label, - |mut tasks_by_label, (source, task, lru_score)| { - match tasks_by_label.entry((source.clone(), task.resolved_label.clone())) { - btree_map::Entry::Occupied(mut o) => { - let (previous_task, _) = o.get(); - let new_template = task.original_task(); - if new_template != previous_task.original_task() { - o.insert((task.clone(), *lru_score)); - } - } - btree_map::Entry::Vacant(v) => { - v.insert((task.clone(), *lru_score)); - } + hash_map::Entry::Vacant(v) => { + v.insert(HashSet::from_iter(Some(resolved_task.id.clone()))); + true } - tasks_by_label - }, - ); + } + }) + .sorted_unstable_by(task_lru_comparator) + .map(|(kind, task, _)| (kind, task)) + .collect::>(); - let resolved = tasks_by_label - .into_iter() - .map(|((kind, _), (task, lru_score))| (kind, task, lru_score)) - .sorted_by(task_lru_comparator) - .filter_map(|(kind, task, lru_score)| { - if lru_score < not_used_score { - Some((kind, task)) - } else { - None - } - }) - .collect::>(); - - ( - resolved, - currently_resolved_tasks - .into_iter() - .sorted_unstable_by(task_lru_comparator) - .map(|(kind, task, _)| (kind, task)) - .collect(), - ) - }) + (previously_spawned_tasks, new_resolved_tasks) } /// Returns the last scheduled task by task_id if provided. @@ -387,6 +234,86 @@ impl Inventory { pub fn delete_previously_used(&mut self, id: &TaskId) { self.last_scheduled_tasks.retain(|(_, task)| &task.id != id); } + + fn templates_from_settings( + &self, + worktree: Option, + ) -> impl '_ + Iterator { + self.templates_from_settings + .global + .clone() + .into_iter() + .map(|template| { + ( + TaskSourceKind::AbsPath { + id_base: Cow::Borrowed("global tasks.json"), + abs_path: paths::tasks_file().clone(), + }, + template, + ) + }) + .chain(worktree.into_iter().flat_map(|worktree| { + self.templates_from_settings + .worktree + .get(&worktree) + .into_iter() + .flatten() + .flat_map(|(directory, templates)| { + templates.iter().map(move |template| (directory, template)) + }) + .map(move |(directory, template)| { + ( + TaskSourceKind::Worktree { + id: worktree, + directory_in_worktree: directory.to_path_buf(), + id_base: Cow::Owned(format!( + "local worktree tasks from directory {directory:?}" + )), + }, + template.clone(), + ) + }) + })) + } + + /// Updates in-memory task metadata from the JSON string given. + /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`]. + /// + /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated. + pub(crate) fn update_file_based_tasks( + &mut self, + location: Option>, + raw_tasks_json: Option<&str>, + ) -> anyhow::Result<()> { + let raw_tasks = + parse_json_with_comments::>(raw_tasks_json.unwrap_or("[]")) + .context("parsing tasks file content as a JSON array")?; + let new_templates = raw_tasks.into_iter().filter_map(|raw_template| { + serde_json::from_value::(raw_template).log_err() + }); + + let parsed_templates = &mut self.templates_from_settings; + match location { + Some(location) => { + let new_templates = new_templates.collect::>(); + if new_templates.is_empty() { + if let Some(worktree_tasks) = + parsed_templates.worktree.get_mut(&location.worktree_id) + { + worktree_tasks.remove(location.path); + } + } else { + parsed_templates + .worktree + .entry(location.worktree_id) + .or_default() + .insert(Arc::from(location.path), new_templates); + } + } + None => parsed_templates.global = new_templates.collect(), + } + Ok(()) + } } fn task_lru_comparator( @@ -432,39 +359,14 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse { #[cfg(test)] mod test_inventory { - use gpui::{AppContext, Model, TestAppContext}; + use gpui::{Model, TestAppContext}; use itertools::Itertools; - use task::{ - static_source::{StaticSource, TrackedFile}, - TaskContext, TaskTemplate, TaskTemplates, - }; + use task::TaskContext; use worktree::WorktreeId; use crate::Inventory; - use super::{task_source_kind_preference, TaskSourceKind, UnboundedSender}; - - pub(super) fn static_test_source( - task_names: impl IntoIterator, - updates: UnboundedSender<()>, - cx: &mut AppContext, - ) -> StaticSource { - let tasks = TaskTemplates( - task_names - .into_iter() - .map(|name| TaskTemplate { - label: name, - command: "test command".to_owned(), - ..TaskTemplate::default() - }) - .collect(), - ); - let (tx, rx) = futures::channel::mpsc::unbounded(); - let file = TrackedFile::new(rx, updates, cx); - tx.unbounded_send(serde_json::to_string(&tasks).unwrap()) - .unwrap(); - StaticSource::new(file) - } + use super::{task_source_kind_preference, TaskSourceKind}; pub(super) fn task_template_names( inventory: &Model, @@ -506,17 +408,9 @@ mod test_inventory { worktree: Option, cx: &mut TestAppContext, ) -> Vec<(TaskSourceKind, String)> { - let (used, current) = inventory - .update(cx, |inventory, cx| { - inventory.used_and_current_resolved_tasks( - None, - worktree, - None, - &TaskContext::default(), - cx, - ) - }) - .await; + let (used, current) = inventory.update(cx, |inventory, cx| { + inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx) + }); let mut all = used; all.extend(current); all.into_iter() @@ -529,12 +423,12 @@ mod test_inventory { /// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file. /// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out. pub struct BasicContextProvider { - project: Model, + worktree_store: Model, } impl BasicContextProvider { - pub fn new(project: Model) -> Self { - Self { project } + pub fn new(worktree_store: Model) -> Self { + Self { worktree_store } } } @@ -585,7 +479,7 @@ impl ContextProvider for BasicContextProvider { .file() .map(|file| file.worktree_id(cx)) .and_then(|worktree_id| { - self.project + self.worktree_store .read(cx) .worktree_for_id(worktree_id, cx) .map(|worktree| worktree.read(cx).abs_path()) @@ -653,12 +547,17 @@ impl ContextProvider for ContextProviderWithTasks { #[cfg(test)] mod tests { use gpui::TestAppContext; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::task_store::TaskStore; use super::test_inventory::*; use super::*; #[gpui::test] async fn test_task_list_sorting(cx: &mut TestAppContext) { + init_test(cx); let inventory = cx.update(Inventory::new); let initial_tasks = resolved_task_names(&inventory, None, cx).await; assert!( @@ -670,31 +569,6 @@ mod tests { initial_tasks.is_empty(), "No tasks expected for empty inventory, but got {initial_tasks:?}" ); - - inventory.update(cx, |inventory, cx| { - inventory.add_source( - TaskSourceKind::UserInput, - |tx, cx| static_test_source(vec!["3_task".to_string()], tx, cx), - cx, - ); - }); - inventory.update(cx, |inventory, cx| { - inventory.add_source( - TaskSourceKind::UserInput, - |tx, cx| { - static_test_source( - vec![ - "1_task".to_string(), - "2_task".to_string(), - "1_a_task".to_string(), - ], - tx, - cx, - ) - }, - cx, - ); - }); cx.run_until_parked(); let expected_initial_state = [ "1_a_task".to_string(), @@ -702,6 +576,17 @@ mod tests { "2_task".to_string(), "3_task".to_string(), ]; + + inventory.update(cx, |inventory, _| { + inventory + .update_file_based_tasks( + None, + Some(&mock_tasks_from_names( + expected_initial_state.iter().map(|name| name.as_str()), + )), + ) + .unwrap(); + }); assert_eq!( task_template_names(&inventory, None, cx), &expected_initial_state, @@ -720,7 +605,6 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx).await, vec![ - "2_task".to_string(), "2_task".to_string(), "1_a_task".to_string(), "1_task".to_string(), @@ -739,9 +623,6 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx).await, vec![ - "3_task".to_string(), - "1_task".to_string(), - "2_task".to_string(), "3_task".to_string(), "1_task".to_string(), "2_task".to_string(), @@ -749,14 +630,17 @@ mod tests { ], ); - inventory.update(cx, |inventory, cx| { - inventory.add_source( - TaskSourceKind::UserInput, - |tx, cx| { - static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], tx, cx) - }, - cx, - ); + inventory.update(cx, |inventory, _| { + inventory + .update_file_based_tasks( + None, + Some(&mock_tasks_from_names( + ["10_hello", "11_hello"] + .into_iter() + .chain(expected_initial_state.iter().map(|name| name.as_str())), + )), + ) + .unwrap(); }); cx.run_until_parked(); let expected_updated_state = [ @@ -774,9 +658,6 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx).await, vec![ - "3_task".to_string(), - "1_task".to_string(), - "2_task".to_string(), "3_task".to_string(), "1_task".to_string(), "2_task".to_string(), @@ -794,10 +675,6 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx).await, vec![ - "11_hello".to_string(), - "3_task".to_string(), - "1_task".to_string(), - "2_task".to_string(), "11_hello".to_string(), "3_task".to_string(), "1_task".to_string(), @@ -810,133 +687,50 @@ mod tests { #[gpui::test] async fn test_inventory_static_task_filters(cx: &mut TestAppContext) { - let inventory_with_statics = cx.update(Inventory::new); + init_test(cx); + let inventory = cx.update(Inventory::new); let common_name = "common_task_name"; - let path_1 = Path::new("path_1"); - let path_2 = Path::new("path_2"); let worktree_1 = WorktreeId::from_usize(1); - let worktree_path_1 = Path::new("worktree_path_1"); let worktree_2 = WorktreeId::from_usize(2); - let worktree_path_2 = Path::new("worktree_path_2"); - inventory_with_statics.update(cx, |inventory, cx| { - inventory.add_source( - TaskSourceKind::UserInput, - |tx, cx| { - static_test_source( - vec!["user_input".to_string(), common_name.to_string()], - tx, - cx, - ) - }, - cx, - ); - inventory.add_source( - TaskSourceKind::AbsPath { - id_base: "test source".into(), - abs_path: path_1.to_path_buf(), - }, - |tx, cx| { - static_test_source( - vec!["static_source_1".to_string(), common_name.to_string()], - tx, - cx, - ) - }, - cx, - ); - inventory.add_source( - TaskSourceKind::AbsPath { - id_base: "test source".into(), - abs_path: path_2.to_path_buf(), - }, - |tx, cx| { - static_test_source( - vec!["static_source_2".to_string(), common_name.to_string()], - tx, - cx, - ) - }, - cx, - ); - inventory.add_source( - TaskSourceKind::Worktree { - id: worktree_1, - abs_path: worktree_path_1.to_path_buf(), - id_base: "test_source".into(), - }, - |tx, cx| { - static_test_source( - vec!["worktree_1".to_string(), common_name.to_string()], - tx, - cx, - ) - }, - cx, - ); - inventory.add_source( - TaskSourceKind::Worktree { - id: worktree_2, - abs_path: worktree_path_2.to_path_buf(), - id_base: "test_source".into(), - }, - |tx, cx| { - static_test_source( - vec!["worktree_2".to_string(), common_name.to_string()], - tx, - cx, - ) - }, - cx, - ); - }); cx.run_until_parked(); let worktree_independent_tasks = vec![ ( TaskSourceKind::AbsPath { - id_base: "test source".into(), - abs_path: path_1.to_path_buf(), + id_base: "global tasks.json".into(), + abs_path: paths::tasks_file().clone(), + }, + common_name.to_string(), + ), + ( + TaskSourceKind::AbsPath { + id_base: "global tasks.json".into(), + abs_path: paths::tasks_file().clone(), }, "static_source_1".to_string(), ), ( TaskSourceKind::AbsPath { - id_base: "test source".into(), - abs_path: path_1.to_path_buf(), - }, - common_name.to_string(), - ), - ( - TaskSourceKind::AbsPath { - id_base: "test source".into(), - abs_path: path_2.to_path_buf(), - }, - common_name.to_string(), - ), - ( - TaskSourceKind::AbsPath { - id_base: "test source".into(), - abs_path: path_2.to_path_buf(), + id_base: "global tasks.json".into(), + abs_path: paths::tasks_file().clone(), }, "static_source_2".to_string(), ), - (TaskSourceKind::UserInput, common_name.to_string()), - (TaskSourceKind::UserInput, "user_input".to_string()), ]; let worktree_1_tasks = [ ( TaskSourceKind::Worktree { id: worktree_1, - abs_path: worktree_path_1.to_path_buf(), - id_base: "test_source".into(), + directory_in_worktree: PathBuf::from(".zed"), + id_base: "local worktree tasks from directory \".zed\"".into(), }, common_name.to_string(), ), ( TaskSourceKind::Worktree { id: worktree_1, - abs_path: worktree_path_1.to_path_buf(), - id_base: "test_source".into(), + directory_in_worktree: PathBuf::from(".zed"), + id_base: "local worktree tasks from directory \".zed\"".into(), }, "worktree_1".to_string(), ), @@ -945,36 +739,63 @@ mod tests { ( TaskSourceKind::Worktree { id: worktree_2, - abs_path: worktree_path_2.to_path_buf(), - id_base: "test_source".into(), + directory_in_worktree: PathBuf::from(".zed"), + id_base: "local worktree tasks from directory \".zed\"".into(), }, common_name.to_string(), ), ( TaskSourceKind::Worktree { id: worktree_2, - abs_path: worktree_path_2.to_path_buf(), - id_base: "test_source".into(), + directory_in_worktree: PathBuf::from(".zed"), + id_base: "local worktree tasks from directory \".zed\"".into(), }, "worktree_2".to_string(), ), ]; - let all_tasks = worktree_1_tasks - .iter() - .chain(worktree_2_tasks.iter()) - // worktree-less tasks come later in the list - .chain(worktree_independent_tasks.iter()) - .cloned() - .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone())) - .collect::>(); + inventory.update(cx, |inventory, _| { + inventory + .update_file_based_tasks( + None, + Some(&mock_tasks_from_names( + worktree_independent_tasks + .iter() + .map(|(_, name)| name.as_str()), + )), + ) + .unwrap(); + inventory + .update_file_based_tasks( + Some(SettingsLocation { + worktree_id: worktree_1, + path: Path::new(".zed"), + }), + Some(&mock_tasks_from_names( + worktree_1_tasks.iter().map(|(_, name)| name.as_str()), + )), + ) + .unwrap(); + inventory + .update_file_based_tasks( + Some(SettingsLocation { + worktree_id: worktree_2, + path: Path::new(".zed"), + }), + Some(&mock_tasks_from_names( + worktree_2_tasks.iter().map(|(_, name)| name.as_str()), + )), + ) + .unwrap(); + }); assert_eq!( - list_tasks(&inventory_with_statics, None, cx).await, - all_tasks + list_tasks(&inventory, None, cx).await, + worktree_independent_tasks, + "Without a worktree, only worktree-independent tasks should be listed" ); assert_eq!( - list_tasks(&inventory_with_statics, Some(worktree_1), cx).await, + list_tasks(&inventory, Some(worktree_1), cx).await, worktree_1_tasks .iter() .chain(worktree_independent_tasks.iter()) @@ -983,7 +804,7 @@ mod tests { .collect::>(), ); assert_eq!( - list_tasks(&inventory_with_statics, Some(worktree_2), cx).await, + list_tasks(&inventory, Some(worktree_2), cx).await, worktree_2_tasks .iter() .chain(worktree_independent_tasks.iter()) @@ -993,25 +814,39 @@ mod tests { ); } + fn init_test(_cx: &mut TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } + TaskStore::init(None); + } + pub(super) async fn resolved_task_names( inventory: &Model, worktree: Option, cx: &mut TestAppContext, ) -> Vec { - let (used, current) = inventory - .update(cx, |inventory, cx| { - inventory.used_and_current_resolved_tasks( - None, - worktree, - None, - &TaskContext::default(), - cx, - ) - }) - .await; + let (used, current) = inventory.update(cx, |inventory, cx| { + inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx) + }); used.into_iter() .chain(current) .map(|(_, task)| task.original_task().label.clone()) .collect() } + + fn mock_tasks_from_names<'a>(task_names: impl Iterator + 'a) -> String { + serde_json::to_string(&serde_json::Value::Array( + task_names + .map(|task_name| { + json!({ + "label": task_name, + "command": "echo", + "args": vec![task_name], + }) + }) + .collect::>(), + )) + .unwrap() + } } diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs new file mode 100644 index 0000000000..55ee780fc5 --- /dev/null +++ b/crates/project/src/task_store.rs @@ -0,0 +1,432 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::Context as _; +use collections::HashMap; +use fs::Fs; +use futures::StreamExt as _; +use gpui::{AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, Task, WeakModel}; +use language::{ + proto::{deserialize_anchor, serialize_anchor}, + ContextProvider as _, Location, +}; +use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use settings::{watch_config_file, SettingsLocation}; +use task::{TaskContext, TaskVariables, VariableName}; +use text::BufferId; +use util::ResultExt; + +use crate::{ + buffer_store::BufferStore, worktree_store::WorktreeStore, BasicContextProvider, Inventory, + ProjectEnvironment, +}; + +pub enum TaskStore { + Functional(StoreState), + Noop, +} + +pub struct StoreState { + mode: StoreMode, + task_inventory: Model, + buffer_store: WeakModel, + worktree_store: Model, + _global_task_config_watcher: Task<()>, +} + +enum StoreMode { + Local { + downstream_client: Option<(AnyProtoClient, u64)>, + environment: Model, + }, + Remote { + upstream_client: AnyProtoClient, + project_id: u64, + }, +} + +impl EventEmitter for TaskStore {} + +impl TaskStore { + pub fn init(client: Option<&AnyProtoClient>) { + if let Some(client) = client { + client.add_model_request_handler(Self::handle_task_context_for_location); + } + } + + async fn handle_task_context_for_location( + store: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> anyhow::Result { + let location = envelope + .payload + .location + .context("no location given for task context handling")?; + let (buffer_store, is_remote) = store.update(&mut cx, |store, _| { + Ok(match store { + TaskStore::Functional(state) => ( + state.buffer_store.clone(), + match &state.mode { + StoreMode::Local { .. } => false, + StoreMode::Remote { .. } => true, + }, + ), + TaskStore::Noop => { + anyhow::bail!("empty task store cannot handle task context requests") + } + }) + })??; + let buffer_store = buffer_store + .upgrade() + .context("no buffer store when handling task context request")?; + + let buffer_id = BufferId::new(location.buffer_id).with_context(|| { + format!( + "cannot handle task context request for invalid buffer id: {}", + location.buffer_id + ) + })?; + + let start = location + .start + .and_then(deserialize_anchor) + .context("missing task context location start")?; + let end = location + .end + .and_then(deserialize_anchor) + .context("missing task context location end")?; + let buffer = buffer_store + .update(&mut cx, |buffer_store, cx| { + if is_remote { + buffer_store.wait_for_remote_buffer(buffer_id, cx) + } else { + Task::ready( + buffer_store + .get(buffer_id) + .with_context(|| format!("no local buffer with id {buffer_id}")), + ) + } + })? + .await?; + + let location = Location { + buffer, + range: start..end, + }; + let context_task = store.update(&mut cx, |store, cx| { + let captured_variables = { + let mut variables = TaskVariables::from_iter( + envelope + .payload + .task_variables + .into_iter() + .filter_map(|(k, v)| Some((k.parse().log_err()?, v))), + ); + + for range in location + .buffer + .read(cx) + .snapshot() + .runnable_ranges(location.range.clone()) + { + for (capture_name, value) in range.extra_captures { + variables.insert(VariableName::Custom(capture_name.into()), value); + } + } + variables + }; + store.task_context_for_location(captured_variables, location, cx) + })?; + let task_context = context_task.await.unwrap_or_default(); + Ok(proto::TaskContext { + project_env: task_context.project_env.into_iter().collect(), + cwd: task_context + .cwd + .map(|cwd| cwd.to_string_lossy().to_string()), + task_variables: task_context + .task_variables + .into_iter() + .map(|(variable_name, variable_value)| (variable_name.to_string(), variable_value)) + .collect(), + }) + } + + pub fn local( + fs: Arc, + buffer_store: WeakModel, + worktree_store: Model, + environment: Model, + cx: &mut ModelContext<'_, Self>, + ) -> Self { + Self::Functional(StoreState { + mode: StoreMode::Local { + downstream_client: None, + environment, + }, + task_inventory: Inventory::new(cx), + buffer_store, + worktree_store, + _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx), + }) + } + + pub fn remote( + fs: Arc, + buffer_store: WeakModel, + worktree_store: Model, + upstream_client: AnyProtoClient, + project_id: u64, + cx: &mut ModelContext<'_, Self>, + ) -> Self { + Self::Functional(StoreState { + mode: StoreMode::Remote { + upstream_client, + project_id, + }, + task_inventory: Inventory::new(cx), + buffer_store, + worktree_store, + _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx), + }) + } + + pub fn task_context_for_location( + &self, + captured_variables: TaskVariables, + location: Location, + cx: &mut AppContext, + ) -> Task> { + match self { + TaskStore::Functional(state) => match &state.mode { + StoreMode::Local { environment, .. } => local_task_context_for_location( + state.worktree_store.clone(), + environment.clone(), + captured_variables, + location, + cx, + ), + StoreMode::Remote { + upstream_client, + project_id, + } => remote_task_context_for_location( + *project_id, + upstream_client, + state.worktree_store.clone(), + captured_variables, + location, + cx, + ), + }, + TaskStore::Noop => Task::ready(None), + } + } + + pub fn task_inventory(&self) -> Option<&Model> { + match self { + TaskStore::Functional(state) => Some(&state.task_inventory), + TaskStore::Noop => None, + } + } + + pub fn shared( + &mut self, + remote_id: u64, + new_downstream_client: AnyProtoClient, + _cx: &mut AppContext, + ) { + if let Self::Functional(StoreState { + mode: StoreMode::Local { + downstream_client, .. + }, + .. + }) = self + { + *downstream_client = Some((new_downstream_client, remote_id)); + } + } + + pub fn unshared(&mut self, _: &mut ModelContext) { + if let Self::Functional(StoreState { + mode: StoreMode::Local { + downstream_client, .. + }, + .. + }) = self + { + *downstream_client = None; + } + } + + pub(super) fn update_user_tasks( + &self, + location: Option>, + raw_tasks_json: Option<&str>, + cx: &mut ModelContext<'_, Self>, + ) -> anyhow::Result<()> { + let task_inventory = match self { + TaskStore::Functional(state) => &state.task_inventory, + TaskStore::Noop => return Ok(()), + }; + let raw_tasks_json = raw_tasks_json + .map(|json| json.trim()) + .filter(|json| !json.is_empty()); + + task_inventory.update(cx, |inventory, _| { + inventory.update_file_based_tasks(location, raw_tasks_json) + }) + } + + fn subscribe_to_global_task_file_changes( + fs: Arc, + cx: &mut ModelContext<'_, Self>, + ) -> Task<()> { + let mut user_tasks_file_rx = + watch_config_file(&cx.background_executor(), fs, paths::tasks_file().clone()); + let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next()); + cx.spawn(move |task_store, mut cx| async move { + if let Some(user_tasks_content) = user_tasks_content { + let Ok(_) = task_store.update(&mut cx, |task_store, cx| { + task_store + .update_user_tasks(None, Some(&user_tasks_content), cx) + .log_err(); + }) else { + return; + }; + } + while let Some(user_tasks_content) = user_tasks_file_rx.next().await { + let Ok(()) = task_store.update(&mut cx, |task_store, cx| { + let result = task_store.update_user_tasks(None, Some(&user_tasks_content), cx); + if let Err(err) = &result { + log::error!("Failed to load user tasks: {err}"); + cx.emit(crate::Event::Notification(format!( + "Invalid global tasks file\n{err}" + ))); + } + cx.refresh(); + }) else { + break; // App dropped + }; + } + }) + } +} + +fn local_task_context_for_location( + worktree_store: Model, + environment: Model, + captured_variables: TaskVariables, + location: Location, + cx: &AppContext, +) -> Task> { + let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)); + let worktree_abs_path = worktree_id + .and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx)) + .map(|worktree| worktree.read(cx).abs_path()); + + cx.spawn(|mut cx| async move { + let worktree_abs_path = worktree_abs_path.clone(); + let project_env = environment + .update(&mut cx, |environment, cx| { + environment.get_environment(worktree_id, worktree_abs_path.clone(), cx) + }) + .ok()? + .await; + + let mut task_variables = cx + .update(|cx| { + combine_task_variables( + captured_variables, + location, + project_env.as_ref(), + BasicContextProvider::new(worktree_store), + cx, + ) + .log_err() + }) + .ok() + .flatten()?; + // Remove all custom entries starting with _, as they're not intended for use by the end user. + task_variables.sweep(); + + Some(TaskContext { + project_env: project_env.unwrap_or_default(), + cwd: worktree_abs_path.map(|p| p.to_path_buf()), + task_variables, + }) + }) +} + +fn remote_task_context_for_location( + project_id: u64, + upstream_client: &AnyProtoClient, + worktree_store: Model, + captured_variables: TaskVariables, + location: Location, + cx: &mut AppContext, +) -> Task> { + // We need to gather a client context, as the headless one may lack certain information (e.g. tree-sitter parsing is disabled there, so symbols are not available). + let mut remote_context = BasicContextProvider::new(worktree_store) + .build_context(&TaskVariables::default(), &location, None, cx) + .log_err() + .unwrap_or_default(); + remote_context.extend(captured_variables); + + let context_task = upstream_client.request(proto::TaskContextForLocation { + project_id, + location: Some(proto::Location { + buffer_id: location.buffer.read(cx).remote_id().into(), + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + }), + task_variables: remote_context + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + }); + cx.spawn(|_| async move { + let task_context = context_task.await.log_err()?; + Some(TaskContext { + cwd: task_context.cwd.map(PathBuf::from), + task_variables: task_context + .task_variables + .into_iter() + .filter_map( + |(variable_name, variable_value)| match variable_name.parse() { + Ok(variable_name) => Some((variable_name, variable_value)), + Err(()) => { + log::error!("Unknown variable name: {variable_name}"); + None + } + }, + ) + .collect(), + project_env: task_context.project_env.into_iter().collect(), + }) + }) +} + +fn combine_task_variables( + mut captured_variables: TaskVariables, + location: Location, + project_env: Option<&HashMap>, + baseline: BasicContextProvider, + cx: &mut AppContext, +) -> anyhow::Result { + let language_context_provider = location + .buffer + .read(cx) + .language() + .and_then(|language| language.context_provider()); + let baseline = baseline + .build_context(&captured_variables, &location, project_env, cx) + .context("building basic default context")?; + captured_variables.extend(baseline); + if let Some(provider) = language_context_provider { + captured_variables.extend( + provider + .build_context(&captured_variables, &location, project_env, cx) + .context("building provider context")?, + ); + } + Ok(captured_variables) +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 4e101f4305..aac2c9ae4d 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -245,8 +245,6 @@ message Envelope { TaskContextForLocation task_context_for_location = 203; TaskContext task_context = 204; - TaskTemplatesResponse task_templates_response = 205; - TaskTemplates task_templates = 206; LinkedEditingRange linked_editing_range = 209; LinkedEditingRangeResponse linked_editing_range_response = 210; @@ -290,6 +288,7 @@ message Envelope { reserved 87 to 88; reserved 158 to 161; reserved 166 to 169; + reserved 205 to 206; reserved 224 to 229; reserved 247 to 254; } @@ -2260,6 +2259,7 @@ message GetSupermavenApiKeyResponse { message TaskContextForLocation { uint64 project_id = 1; Location location = 2; + map task_variables = 3; } message TaskContext { @@ -2268,35 +2268,6 @@ message TaskContext { map project_env = 3; } -message TaskTemplates { - uint64 project_id = 1; - optional Location location = 2; - optional uint64 worktree_id = 3; -} - -message TaskTemplatesResponse { - repeated TemplatePair templates = 1; -} - -message TemplatePair { - TaskSourceKind kind = 1; - TaskTemplate template = 2; -} - -message TaskTemplate { - string label = 1; - string command = 2; - repeated string args = 3; - map env = 4; - optional string cwd = 5; - bool use_new_terminal = 6; - bool allow_concurrent_runs = 7; - RevealStrategy reveal = 8; - HideStrategy hide = 10; - repeated string tags = 9; - Shell shell = 11; -} - message Shell { message WithArguments { string program = 1; @@ -2323,32 +2294,6 @@ enum HideStrategy { HideOnSuccess = 2; } -message TaskSourceKind { - oneof kind { - UserInput user_input = 1; - Worktree worktree = 2; - AbsPath abs_path = 3; - Language language = 4; - } - - message UserInput {} - - message Worktree { - uint64 id = 1; - string abs_path = 2; - string id_base = 3; - } - - message AbsPath { - string id_base = 1; - string abs_path = 2; - } - - message Language { - string name = 1; - } -} - message ContextMessageStatus { oneof variant { Done done = 1; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 48733c449c..ae28555a2c 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -290,8 +290,6 @@ messages!( (SynchronizeBuffersResponse, Foreground), (TaskContextForLocation, Background), (TaskContext, Background), - (TaskTemplates, Background), - (TaskTemplatesResponse, Background), (Test, Foreground), (Unfollow, Foreground), (UnshareProject, Foreground), @@ -460,7 +458,6 @@ request_messages!( (ShareProject, ShareProjectResponse), (SynchronizeBuffers, SynchronizeBuffersResponse), (TaskContextForLocation, TaskContext), - (TaskTemplates, TaskTemplatesResponse), (Test, Test), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), @@ -543,7 +540,6 @@ entity_messages!( StartLanguageServer, SynchronizeBuffers, TaskContextForLocation, - TaskTemplates, UnshareProject, UpdateBuffer, UpdateBufferFile, diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 0ad16caacc..8ebe8905ad 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -7,6 +7,7 @@ use project::{ buffer_store::{BufferStore, BufferStoreEvent}, project_settings::SettingsObserver, search::SearchQuery, + task_store::TaskStore, worktree_store::WorktreeStore, LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId, }; @@ -29,6 +30,7 @@ pub struct HeadlessProject { pub worktree_store: Model, pub buffer_store: Model, pub lsp_store: Model, + pub task_store: Model, pub settings_observer: Model, pub next_entry_id: Arc, pub languages: Arc, @@ -68,12 +70,28 @@ impl HeadlessProject { ) }); + let environment = project::ProjectEnvironment::new(&worktree_store, None, cx); + let task_store = cx.new_model(|cx| { + let mut task_store = TaskStore::local( + fs.clone(), + buffer_store.downgrade(), + worktree_store.clone(), + environment.clone(), + cx, + ); + task_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + task_store + }); let settings_observer = cx.new_model(|cx| { - let mut observer = SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx); + let mut observer = SettingsObserver::new_local( + fs.clone(), + worktree_store.clone(), + task_store.clone(), + cx, + ); observer.shared(SSH_PROJECT_ID, session.clone().into(), cx); observer }); - let environment = project::ProjectEnvironment::new(&worktree_store, None, cx); let lsp_store = cx.new_model(|cx| { let mut lsp_store = LspStore::new_local( buffer_store.clone(), @@ -108,6 +126,7 @@ impl HeadlessProject { session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store); session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle()); session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store); + session.subscribe_to_entity(SSH_PROJECT_ID, &task_store); session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory); @@ -126,6 +145,7 @@ impl HeadlessProject { WorktreeStore::init(&client); SettingsObserver::init(&client); LspStore::init(&client); + TaskStore::init(Some(&client)); HeadlessProject { session: client, @@ -134,6 +154,7 @@ impl HeadlessProject { worktree_store, buffer_store, lsp_store, + task_store, next_entry_id: Default::default(), languages, } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 2ed01dc7c7..1092a9bd4d 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -14,8 +14,8 @@ pub use json_schema::*; pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{ - InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, - SettingsStore, + parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, + SettingsSources, SettingsStore, }; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 445420c1db..c8768f007d 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -515,13 +515,11 @@ impl SettingsStore { } else { parse_json_with_comments(user_settings_content)? }; - if settings.is_object() { - self.raw_user_settings = settings; - self.recompute_values(None, cx)?; - Ok(()) - } else { - Err(anyhow!("settings must be an object")) - } + + anyhow::ensure!(settings.is_object(), "settings must be an object"); + self.raw_user_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) } /// Add or remove a set of local settings via a JSON string. @@ -533,16 +531,29 @@ impl SettingsStore { settings_content: Option<&str>, cx: &mut AppContext, ) -> Result<()> { + anyhow::ensure!( + kind != LocalSettingsKind::Tasks, + "Attempted to submit tasks into the settings store" + ); + let raw_local_settings = self .raw_local_settings .entry((root_id, directory_path.clone())) .or_default(); - if settings_content.is_some_and(|content| !content.is_empty()) { - raw_local_settings.insert(kind, parse_json_with_comments(settings_content.unwrap())?); + let changed = if settings_content.is_some_and(|content| !content.is_empty()) { + let new_contents = parse_json_with_comments(settings_content.unwrap())?; + if Some(&new_contents) == raw_local_settings.get(&kind) { + false + } else { + raw_local_settings.insert(kind, new_contents); + true + } } else { - raw_local_settings.remove(&kind); + raw_local_settings.remove(&kind).is_some() + }; + if changed { + self.recompute_values(Some((root_id, &directory_path)), cx)?; } - self.recompute_values(Some((root_id, &directory_path)), cx)?; Ok(()) } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 8321518e03..1687f8f696 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -140,8 +140,13 @@ impl FromStr for VariableName { let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?; let value = match without_prefix { "FILE" => Self::File, + "FILENAME" => Self::Filename, + "RELATIVE_FILE" => Self::RelativeFile, + "DIRNAME" => Self::Dirname, + "STEM" => Self::Stem, "WORKTREE_ROOT" => Self::WorktreeRoot, "SYMBOL" => Self::Symbol, + "RUNNABLE_SYMBOL" => Self::RunnableSymbol, "SELECTED_TEXT" => Self::SelectedText, "ROW" => Self::Row, "COLUMN" => Self::Column, diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index fd14f9aaef..38b15403e2 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -18,10 +18,14 @@ pub fn init(cx: &mut AppContext) { workspace .register_action(spawn_task_or_modal) .register_action(move |workspace, action: &modal::Rerun, cx| { - if let Some((task_source_kind, mut last_scheduled_task)) = - workspace.project().update(cx, |project, cx| { - project - .task_inventory() + if let Some((task_source_kind, mut last_scheduled_task)) = workspace + .project() + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .and_then(|inventory| { + inventory .read(cx) .last_scheduled_task(action.task_id.as_ref()) }) @@ -86,23 +90,26 @@ fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewC } fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> { - let project = workspace.project().clone(); + let task_store = workspace.project().read(cx).task_store().clone(); let workspace_handle = workspace.weak_handle(); - let context_task = task_context(workspace, cx); - cx.spawn(|workspace, mut cx| async move { - let task_context = context_task.await; - workspace - .update(&mut cx, |workspace, cx| { - if workspace.project().update(cx, |project, cx| { - project.is_local() || project.ssh_connection_string(cx).is_some() - }) { + let can_open_modal = workspace.project().update(cx, |project, cx| { + project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh() + }); + if can_open_modal { + let context_task = task_context(workspace, cx); + cx.spawn(|workspace, mut cx| async move { + let task_context = context_task.await; + workspace + .update(&mut cx, |workspace, cx| { workspace.toggle_modal(cx, |cx| { - TasksModal::new(project, task_context, workspace_handle, cx) + TasksModal::new(task_store.clone(), task_context, workspace_handle, cx) }) - } - }) - .ok(); - }) + }) + .ok(); + }) + } else { + AsyncTask::ready(()) + } } fn spawn_task_with_name( @@ -113,14 +120,31 @@ fn spawn_task_with_name( let context_task = workspace.update(&mut cx, |workspace, cx| task_context(workspace, cx))?; let task_context = context_task.await; - let tasks = workspace - .update(&mut cx, |workspace, cx| { - let (worktree, location) = active_item_selection_properties(workspace, cx); - workspace.project().update(cx, |project, cx| { - project.task_templates(worktree, location, cx) + let tasks = workspace.update(&mut cx, |workspace, cx| { + let Some(task_inventory) = workspace + .project() + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + else { + return Vec::new(); + }; + let (worktree, location) = active_item_selection_properties(workspace, cx); + let (file, language) = location + .map(|location| { + let buffer = location.buffer.read(cx); + ( + buffer.file().cloned(), + buffer.language_at(location.range.start), + ) }) - })? - .await?; + .unwrap_or_default(); + task_inventory + .read(cx) + .list_tasks(file, language, worktree, cx) + })?; let did_spawn = workspace .update(&mut cx, |workspace, cx| { @@ -185,7 +209,7 @@ mod tests { use editor::Editor; use gpui::{Entity, TestAppContext}; use language::{Language, LanguageConfig}; - use project::{BasicContextProvider, FakeFs, Project}; + use project::{task_store::TaskStore, BasicContextProvider, FakeFs, Project}; use serde_json::json; use task::{TaskContext, TaskVariables, VariableName}; use ui::VisualContext; @@ -223,6 +247,7 @@ mod tests { ) .await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let worktree_store = project.update(cx, |project, _| project.worktree_store().clone()); let rust_language = Arc::new( Language::new( LanguageConfig::default(), @@ -234,7 +259,9 @@ mod tests { name: (_) @name) @item"#, ) .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))), + .with_context_provider(Some(Arc::new(BasicContextProvider::new( + worktree_store.clone(), + )))), ); let typescript_language = Arc::new( @@ -252,7 +279,9 @@ mod tests { ")" @context)) @item"#, ) .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))), + .with_context_provider(Some(Arc::new(BasicContextProvider::new( + worktree_store.clone(), + )))), ); let worktree_id = project.update(cx, |project, cx| { @@ -373,6 +402,7 @@ mod tests { editor::init(cx); workspace::init_settings(cx); Project::init_settings(cx); + TaskStore::init(None); state }) } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 662e3f11fd..f11f58e010 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -8,7 +8,7 @@ use gpui::{ View, ViewContext, VisualContext, WeakView, }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; -use project::{Project, TaskSourceKind}; +use project::{task_store::TaskStore, TaskSourceKind}; use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, @@ -63,7 +63,7 @@ impl_actions!(task, [Rerun, Spawn]); /// A modal used to spawn new tasks. pub(crate) struct TasksModalDelegate { - project: Model, + task_store: Model, candidates: Option>, last_used_candidate_index: Option, divider_index: Option, @@ -77,12 +77,12 @@ pub(crate) struct TasksModalDelegate { impl TasksModalDelegate { fn new( - project: Model, + task_store: Model, task_context: TaskContext, workspace: WeakView, ) -> Self { Self { - project, + task_store, workspace, candidates: None, matches: Vec::new(), @@ -124,11 +124,11 @@ impl TasksModalDelegate { // it doesn't make sense to requery the inventory for new candidates, as that's potentially costly and more often than not it should just return back // the original list without a removed entry. candidates.remove(ix); - self.project.update(cx, |project, cx| { - project.task_inventory().update(cx, |inventory, _| { + if let Some(inventory) = self.task_store.read(cx).task_inventory().cloned() { + inventory.update(cx, |inventory, _| { inventory.delete_previously_used(&task.id); }) - }); + }; } } @@ -139,14 +139,14 @@ pub(crate) struct TasksModal { impl TasksModal { pub(crate) fn new( - project: Model, + task_store: Model, task_context: TaskContext, workspace: WeakView, cx: &mut ViewContext, ) -> Self { let picker = cx.new_view(|cx| { Picker::uniform_list( - TasksModalDelegate::new(project, task_context, workspace), + TasksModalDelegate::new(task_store, task_context, workspace), cx, ) }); @@ -204,71 +204,46 @@ impl PickerDelegate for TasksModalDelegate { cx: &mut ViewContext>, ) -> Task<()> { cx.spawn(move |picker, mut cx| async move { - let Some(candidates_task) = picker + let Some(candidates) = picker .update(&mut cx, |picker, cx| { match &mut picker.delegate.candidates { - Some(candidates) => { - Task::ready(Ok(string_match_candidates(candidates.iter()))) - } + Some(candidates) => string_match_candidates(candidates.iter()), None => { let Ok((worktree, location)) = picker.delegate.workspace.update(cx, |workspace, cx| { active_item_selection_properties(workspace, cx) }) else { - return Task::ready(Ok(Vec::new())); + return Vec::new(); + }; + let Some(task_inventory) = picker + .delegate + .task_store + .read(cx) + .task_inventory() + .cloned() + else { + return Vec::new(); }; - let resolved_task = - picker.delegate.project.update(cx, |project, cx| { - let ssh_connection_string = project.ssh_connection_string(cx); - if project.is_via_collab() && ssh_connection_string.is_none() { - Task::ready((Vec::new(), Vec::new())) - } else { - let remote_templates = if project.is_local() { - None - } else { - project - .remote_id() - .filter(|_| ssh_connection_string.is_some()) - .map(|project_id| { - project.query_remote_task_templates( - project_id, - worktree, - location.as_ref(), - cx, - ) - }) - }; - project - .task_inventory() - .read(cx) - .used_and_current_resolved_tasks( - remote_templates, - worktree, - location, - &picker.delegate.task_context, - cx, - ) - } - }); - cx.spawn(|picker, mut cx| async move { - let (used, current) = resolved_task.await; - picker.update(&mut cx, |picker, _| { - picker.delegate.last_used_candidate_index = if used.is_empty() { - None - } else { - Some(used.len() - 1) - }; + let (used, current) = + task_inventory.read(cx).used_and_current_resolved_tasks( + worktree, + location, + &picker.delegate.task_context, + cx, + ); + picker.delegate.last_used_candidate_index = if used.is_empty() { + None + } else { + Some(used.len() - 1) + }; - let mut new_candidates = used; - new_candidates.extend(current); - let match_candidates = - string_match_candidates(new_candidates.iter()); - let _ = picker.delegate.candidates.insert(new_candidates); - match_candidates - }) - }) + let mut new_candidates = used; + new_candidates.extend(current); + let match_candidates = string_match_candidates(new_candidates.iter()); + let _ = picker.delegate.candidates.insert(new_candidates); + match_candidates } } }) @@ -276,11 +251,6 @@ impl PickerDelegate for TasksModalDelegate { else { return; }; - let Some(candidates): Option> = - candidates_task.await.log_err() - else { - return; - }; let matches = fuzzy::match_strings( &candidates, &query, @@ -492,9 +462,9 @@ impl PickerDelegate for TasksModalDelegate { let is_recent_selected = self.divider_index >= Some(self.selected_index); let current_modifiers = cx.modifiers(); let left_button = if self - .project + .task_store .read(cx) - .task_inventory() + .task_inventory()? .read(cx) .last_scheduled_task(None) .is_some() @@ -646,6 +616,20 @@ mod tests { "", "Initial query should be empty" ); + assert_eq!( + task_names(&tasks_picker, cx), + Vec::::new(), + "With no global tasks and no open item, no tasks should be listed" + ); + drop(tasks_picker); + + let _ = workspace + .update(cx, |workspace, cx| { + workspace.open_abs_path(PathBuf::from("/dir/a.ts"), true, cx) + }) + .await + .unwrap(); + let tasks_picker = open_spawn_tasks(&workspace, cx); assert_eq!( task_names(&tasks_picker, cx), vec!["another one", "example task"], @@ -951,8 +935,9 @@ mod tests { let tasks_picker = open_spawn_tasks(&workspace, cx); assert_eq!( task_names(&tasks_picker, cx), - vec!["TypeScript task from file /dir/a1.ts", "TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"], - "After spawning the task and getting it into the history, it should be up in the sort as recently used" + vec!["TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"], + "After spawning the task and getting it into the history, it should be up in the sort as recently used. + Tasks with the same labels and context are deduplicated." ); tasks_picker.update(cx, |_, cx| { cx.emit(DismissEvent); @@ -1035,10 +1020,12 @@ mod tests { .unwrap() }); project.update(cx, |project, cx| { - project.task_inventory().update(cx, |inventory, _| { - let (kind, task) = scheduled_task; - inventory.task_scheduled(kind, task); - }) + if let Some(task_inventory) = project.task_store().read(cx).task_inventory().cloned() { + task_inventory.update(cx, |inventory, _| { + let (kind, task) = scheduled_task; + inventory.task_scheduled(kind, task); + }); + } }); tasks_picker.update(cx, |_, cx| { cx.emit(DismissEvent); diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 98726096cb..377875ee1b 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -36,9 +36,13 @@ pub fn schedule_resolved_task( if !omit_history { resolved_task.resolved = Some(spawn_in_terminal.clone()); workspace.project().update(cx, |project, cx| { - project.task_inventory().update(cx, |inventory, _| { - inventory.task_scheduled(task_source_kind, resolved_task); - }) + if let Some(task_inventory) = + project.task_store().read(cx).task_inventory().cloned() + { + task_inventory.update(cx, |inventory, _| { + inventory.task_scheduled(task_source_kind, resolved_task); + }) + } }); } cx.emit(crate::Event::SpawnTask(Box::new(spawn_in_terminal))); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index eb60ab9fa8..f5bc3a1847 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -27,19 +27,17 @@ use anyhow::Context as _; use assets::Assets; use futures::{channel::mpsc, select_biased, StreamExt}; use outline_panel::OutlinePanel; -use project::TaskSourceKind; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings, - SettingsStore, DEFAULT_KEYMAP_PATH, + initial_local_settings_content, initial_tasks_content, KeymapFile, Settings, SettingsStore, + DEFAULT_KEYMAP_PATH, }; use std::any::TypeId; use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc}; -use task::static_source::{StaticSource, TrackedFile}; use theme::ActiveTheme; use workspace::notifications::NotificationId; use workspace::CloseIntent; @@ -229,27 +227,6 @@ pub fn initialize_workspace( .unwrap_or(true) }); - let project = workspace.project().clone(); - if project.update(cx, |project, cx| { - project.is_local() || project.is_via_ssh() || project.ssh_connection_string(cx).is_some() - }) { - project.update(cx, |project, cx| { - let fs = app_state.fs.clone(); - project.task_inventory().update(cx, |inventory, cx| { - let tasks_file_rx = - watch_config_file(cx.background_executor(), fs, paths::tasks_file().clone()); - inventory.add_source( - TaskSourceKind::AbsPath { - id_base: "global_tasks".into(), - abs_path: paths::tasks_file().clone(), - }, - |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)), - cx, - ); - }) - }); - } - let prompt_builder = prompt_builder.clone(); cx.spawn(|workspace_handle, mut cx| async move { let assistant_panel =