Detailed changes
@@ -469,9 +469,6 @@ impl Server {
.add_request_handler(user_handler(
forward_project_request_for_owner::<proto::TaskContextForLocation>,
))
- .add_request_handler(user_handler(
- forward_project_request_for_owner::<proto::TaskTemplates>,
- ))
.add_request_handler(user_handler(
forward_read_only_project_request::<proto::GetHover>,
))
@@ -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)
})
@@ -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<TaskContext> {
@@ -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<LanguageRegistry>,
client: Arc<client::Client>,
join_project_response_message_id: u32,
+ task_store: Model<TaskStore>,
user_store: Model<UserStore>,
fs: Arc<dyn Fs>,
ssh_client: Option<Model<SshRemoteClient>>,
@@ -156,7 +151,6 @@ pub struct Project {
remotely_created_models: Arc<Mutex<RemotelyCreatedModels>>,
terminals: Terminals,
node: Option<NodeRuntime>,
- tasks: Model<Inventory>,
hosted_project_id: Option<ProjectId>,
dev_server_project_id: Option<client::DevServerProjectId>,
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<proto::JoinProjectResponse>,
subscriptions: [EntitySubscription; 5],
client: Arc<Client>,
+ run_tasks: bool,
user_store: Model<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
@@ -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<Inventory> {
- &self.tasks
+ pub fn task_store(&self) -> &Model<TaskStore> {
+ &self.task_store
}
pub fn snippets(&self) -> &Model<SnippetProvider> {
@@ -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.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)),
+ cx.subscribe(worktree, |project, worktree, event, cx| match event {
+ worktree::Event::UpdatedEntries(changes) => {
+ cx.emit(Event::WorktreeUpdatedEntries(
+ worktree.read(cx).id(),
+ changes.clone(),
+ ));
+
+ 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<Worktree>,
- changes: &UpdatedEntriesSet,
- cx: &mut ModelContext<Self>,
- ) {
- 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<ProjectPath>, cx: &mut ModelContext<Self>) {
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<Self>,
- envelope: TypedEnvelope<proto::TaskContextForLocation>,
- mut cx: AsyncAppContext,
- ) -> Result<proto::TaskContext> {
- 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<Self>,
- envelope: TypedEnvelope<proto::TaskTemplates>,
- mut cx: AsyncAppContext,
- ) -> Result<proto::TaskTemplatesResponse> {
- 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<Self>,
envelope: TypedEnvelope<proto::FindSearchCandidates>,
@@ -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<Option<TaskContext>> {
- 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<WorktreeId>,
- location: Option<Location>,
- cx: &mut ModelContext<Self>,
- ) -> Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>> {
- 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<WorktreeId>,
- location: Option<&Location>,
- cx: &AppContext,
- ) -> Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>> {
- 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<Model<Worktree>> {
- 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::<Vec<_>>();
-
- 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<String, String>>,
- baseline: BasicContextProvider,
- cx: &mut AppContext,
-) -> anyhow::Result<TaskVariables> {
- 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<String, bool>) -> Vec<lsp::CodeActionKind> {
@@ -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<Project>,
- location: proto::Location,
- cx: &mut AppContext,
-) -> Task<Result<Location>> {
- 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(
@@ -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<AnyProtoClient>,
worktree_store: Model<WorktreeStore>,
project_id: u64,
+ task_store: Model<TaskStore>,
}
-/// 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<dyn Fs>,
worktree_store: Model<WorktreeStore>,
+ task_store: Model<TaskStore>,
cx: &mut ModelContext<Self>,
) -> 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<WorktreeStore>,
+ task_store: Model<TaskStore>,
cx: &mut ModelContext<Self>,
) -> 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<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
+ pub fn new_remote(
+ worktree_store: Model<WorktreeStore>,
+ task_store: Model<TaskStore>,
+ _: &mut ModelContext<Self>,
+ ) -> 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<Self>,
+ settings_observer: Model<Self>,
envelope: TypedEnvelope<proto::UpdateUserSettings>,
- 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<Self>) {
- let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
+ let settings_store = cx.global::<SettingsStore>();
+
+ 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::<Path>::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::<Path>::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::<Path>::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::<VsCodeTaskFile>(&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();
+
+ for (directory, kind, file_content) in settings_contents {
+ let result = match kind {
+ LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
+ .update_global::<SettingsStore, anyhow::Result<()>>(|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,
+ )
+ }),
+ };
- let result = cx.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
- for (directory, kind, file_content) in settings_contents {
- store.set_local_settings(
- worktree_id,
- directory.clone(),
- kind,
- 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::<InvalidSettingsError>() {
- 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::<InvalidSettingsError>() {
+ 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(())));
+ }
}
}
}
@@ -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<WorktreeId>,
task_context: &TaskContext,
cx: &mut AppContext,
-) -> Task<Vec<(TaskSourceKind, ResolvedTask)>> {
- let resolved_tasks = project.update(cx, |project, cx| {
+) -> Vec<(TaskSourceKind, ResolvedTask)> {
+ let (mut old, new) = project.update(cx, |project, cx| {
project
+ .task_store
+ .read(cx)
.task_inventory()
+ .unwrap()
.read(cx)
- .used_and_current_resolved_tasks(None, worktree_id, None, task_context, 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
}
@@ -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<SourceInInventory>,
last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
- update_sender: UnboundedSender<()>,
- _update_pooler: Task<anyhow::Result<()>>,
+ templates_from_settings: ParsedTemplates,
}
-struct SourceInInventory {
- source: StaticSource,
- kind: TaskSourceKind,
+#[derive(Debug, Default)]
+struct ParsedTemplates {
+ global: Vec<TaskTemplate>,
+ worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<TaskTemplate>>>,
}
/// 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<WorktreeId> {
- 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<Self> {
- 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<Self>,
- ) {
- 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<Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>>>,
worktree: Option<WorktreeId>,
location: Option<Location>,
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::<String, HashSet<TaskId>>::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
- })
- .flat_map(|source| {
- source
- .source
- .tasks_to_schedule()
- .0
- .into_iter()
- .map(|task| (&source.kind, task))
- })
- .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)?))
+ .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
+ }
+ }
})
- .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)
+ .map(|(task_source_kind, resolved_task)| {
+ (
+ task_source_kind.clone(),
+ resolved_task.clone(),
+ post_inc(&mut lru_score),
+ )
})
- .collect::<Vec<_>>();
- 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::<Vec<_>>();
- 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));
- }
- }
- 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));
- }
+ })
+ .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
- },
- );
-
- 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
+ hash_map::Entry::Vacant(v) => {
+ v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
+ true
}
- })
- .collect::<Vec<_>>();
+ }
+ })
+ .sorted_unstable_by(task_lru_comparator)
+ .map(|(kind, task, _)| (kind, task))
+ .collect::<Vec<_>>();
- (
- 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<WorktreeId>,
+ ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
+ 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<SettingsLocation<'_>>,
+ raw_tasks_json: Option<&str>,
+ ) -> anyhow::Result<()> {
+ let raw_tasks =
+ parse_json_with_comments::<Vec<serde_json::Value>>(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::<TaskTemplate>(raw_template).log_err()
+ });
+
+ let parsed_templates = &mut self.templates_from_settings;
+ match location {
+ Some(location) => {
+ let new_templates = new_templates.collect::<Vec<_>>();
+ 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<usize> {
#[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<Item = String>,
- 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<Inventory>,
@@ -506,17 +408,9 @@ mod test_inventory {
worktree: Option<WorktreeId>,
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<Project>,
+ worktree_store: Model<WorktreeStore>,
}
impl BasicContextProvider {
- pub fn new(project: Model<Project>) -> Self {
- Self { project }
+ pub fn new(worktree_store: Model<WorktreeStore>) -> 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(),
- },
- "static_source_1".to_string(),
- ),
- (
- 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: "test source".into(),
- abs_path: path_2.to_path_buf(),
+ id_base: "global tasks.json".into(),
+ abs_path: paths::tasks_file().clone(),
},
- common_name.to_string(),
+ "static_source_1".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::<Vec<_>>();
+ 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::<Vec<_>>(),
);
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<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<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)
+ });
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task().label.clone())
.collect()
}
+
+ fn mock_tasks_from_names<'a>(task_names: impl Iterator<Item = &'a str> + '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::<Vec<_>>(),
+ ))
+ .unwrap()
+ }
}
@@ -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<Inventory>,
+ buffer_store: WeakModel<BufferStore>,
+ worktree_store: Model<WorktreeStore>,
+ _global_task_config_watcher: Task<()>,
+}
+
+enum StoreMode {
+ Local {
+ downstream_client: Option<(AnyProtoClient, u64)>,
+ environment: Model<ProjectEnvironment>,
+ },
+ Remote {
+ upstream_client: AnyProtoClient,
+ project_id: u64,
+ },
+}
+
+impl EventEmitter<crate::Event> 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<Self>,
+ envelope: TypedEnvelope<proto::TaskContextForLocation>,
+ mut cx: AsyncAppContext,
+ ) -> anyhow::Result<proto::TaskContext> {
+ 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<dyn Fs>,
+ buffer_store: WeakModel<BufferStore>,
+ worktree_store: Model<WorktreeStore>,
+ environment: Model<ProjectEnvironment>,
+ 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<dyn Fs>,
+ buffer_store: WeakModel<BufferStore>,
+ worktree_store: Model<WorktreeStore>,
+ 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<Option<TaskContext>> {
+ 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<Inventory>> {
+ 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<Self>) {
+ if let Self::Functional(StoreState {
+ mode: StoreMode::Local {
+ downstream_client, ..
+ },
+ ..
+ }) = self
+ {
+ *downstream_client = None;
+ }
+ }
+
+ pub(super) fn update_user_tasks(
+ &self,
+ location: Option<SettingsLocation<'_>>,
+ 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<dyn Fs>,
+ 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<WorktreeStore>,
+ environment: Model<ProjectEnvironment>,
+ captured_variables: TaskVariables,
+ location: Location,
+ cx: &AppContext,
+) -> Task<Option<TaskContext>> {
+ 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<WorktreeStore>,
+ captured_variables: TaskVariables,
+ location: Location,
+ cx: &mut AppContext,
+) -> Task<Option<TaskContext>> {
+ // 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<String, String>>,
+ baseline: BasicContextProvider,
+ cx: &mut AppContext,
+) -> anyhow::Result<TaskVariables> {
+ 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)
+}
@@ -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<string, string> task_variables = 3;
}
message TaskContext {
@@ -2268,35 +2268,6 @@ message TaskContext {
map<string, string> 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<string, string> 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;
@@ -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,
@@ -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<WorktreeStore>,
pub buffer_store: Model<BufferStore>,
pub lsp_store: Model<LspStore>,
+ pub task_store: Model<TaskStore>,
pub settings_observer: Model<SettingsObserver>,
pub next_entry_id: Arc<AtomicUsize>,
pub languages: Arc<LanguageRegistry>,
@@ -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,
}
@@ -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)]
@@ -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(())
}
@@ -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,
@@ -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
})
}
@@ -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<Project>,
+ task_store: Model<TaskStore>,
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
last_used_candidate_index: Option<usize>,
divider_index: Option<usize>,
@@ -77,12 +77,12 @@ pub(crate) struct TasksModalDelegate {
impl TasksModalDelegate {
fn new(
- project: Model<Project>,
+ task_store: Model<TaskStore>,
task_context: TaskContext,
workspace: WeakView<Workspace>,
) -> 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<Project>,
+ task_store: Model<TaskStore>,
task_context: TaskContext,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> 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<picker::Picker<Self>>,
) -> 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 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 (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
}
}
})
@@ -276,11 +251,6 @@ impl PickerDelegate for TasksModalDelegate {
else {
return;
};
- let Some(candidates): Option<Vec<StringMatchCandidate>> =
- 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::<String>::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);
@@ -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)));
@@ -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 =