Rework remote task synchronization (#18746)

Reworks the way tasks are stored, accessed and synchronized in the
`project`.
Now both collab and ssh remote projects use the same TaskStorage kind to
get the task context from the remote host, and worktree task templates
are synchronized along with other worktree settings.

Release Notes:

- Adds ssh support to tasks, improves collab-remote projects' tasks sync
This commit is contained in:
Kirill Bulatov 2024-10-09 22:28:42 +03:00 committed by GitHub
parent f1053ff525
commit 49c75eb062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1262 additions and 1366 deletions

View file

@ -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>,
))

View file

@ -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)
})

View file

@ -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> {

View file

@ -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.subscribe(worktree, |project, worktree, event, cx| match event {
worktree::Event::UpdatedEntries(changes) => {
cx.emit(Event::WorktreeUpdatedEntries(
worktree.read(cx).id(),
changes.clone(),
));
cx.emit(Event::WorktreeUpdatedEntries(
worktree.read(cx).id(),
changes.clone(),
));
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
this.client()
.telemetry()
.report_discovered_project_events(worktree_id, changes);
}
worktree::Event::UpdatedGitRepositories(_) => {
cx.emit(Event::WorktreeUpdatedGitRepositories);
}
worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(*id)),
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
project
.client()
.telemetry()
.report_discovered_project_events(worktree_id, changes);
}
worktree::Event::UpdatedGitRepositories(_) => {
cx.emit(Event::WorktreeUpdatedGitRepositories);
}
worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(*id)),
})
.detach();
cx.notify();
@ -2157,10 +2201,6 @@ impl Project {
return;
}
self.task_inventory().update(cx, |inventory, _| {
inventory.remove_worktree_sources(id_to_remove);
});
cx.notify();
}
@ -3139,77 +3179,6 @@ impl Project {
});
}
fn update_local_worktree_settings(
&mut self,
worktree: &Model<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(

View file

@ -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();
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,
)?;
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,
)
}),
};
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(())));
}
}
}
}

View file

@ -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_inventory()
.task_store
.read(cx)
.used_and_current_resolved_tasks(None, worktree_id, None, task_context, cx)
.task_inventory()
.unwrap()
.read(cx)
.used_and_current_resolved_tasks(worktree_id, None, task_context, cx)
});
cx.spawn(|_| async move {
let (mut old, new) = resolved_tasks.await;
old.extend(new);
old
})
old.extend(new);
old
}

View file

@ -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
.filter(|(_, resolved_task)| {
match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
hash_map::Entry::Occupied(mut o) => {
o.get_mut().insert(resolved_task.id.clone());
// Neber allow duplicate reused tasks with the same labels
false
}
hash_map::Entry::Vacant(v) => {
v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
true
}
}
})
.flat_map(|source| {
source
.source
.tasks_to_schedule()
.0
.into_iter()
.map(|task| (&source.kind, task))
.map(|(task_source_kind, resolved_task)| {
(
task_source_kind.clone(),
resolved_task.clone(),
post_inc(&mut lru_score),
)
})
.chain(language_tasks.filter(|_| remote_templates_task.is_none()))
.filter_map(|(kind, task)| {
let id_base = kind.to_id_base();
Some((kind, task.resolve_task(&id_base, task_context)?))
})
.map(|(kind, task)| {
let lru_score = task_usage
.remove(&task.id)
.map(|(_, _, lru_score)| lru_score)
.unwrap_or(not_used_score);
(kind.clone(), task, lru_score)
})
.collect::<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));
}
})
.filter(|(_, resolved_task, _)| {
match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
hash_map::Entry::Occupied(mut o) => {
// Allow new tasks with the same label, if their context is different
o.get_mut().insert(resolved_task.id.clone())
}
tasks_by_label
},
);
tasks_by_label = currently_resolved_tasks.iter().fold(
tasks_by_label,
|mut tasks_by_label, (source, task, lru_score)| {
match tasks_by_label.entry((source.clone(), task.resolved_label.clone())) {
btree_map::Entry::Occupied(mut o) => {
let (previous_task, _) = o.get();
let new_template = task.original_task();
if new_template != previous_task.original_task() {
o.insert((task.clone(), *lru_score));
}
}
btree_map::Entry::Vacant(v) => {
v.insert((task.clone(), *lru_score));
}
hash_map::Entry::Vacant(v) => {
v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
true
}
tasks_by_label
},
);
}
})
.sorted_unstable_by(task_lru_comparator)
.map(|(kind, task, _)| (kind, task))
.collect::<Vec<_>>();
let resolved = tasks_by_label
.into_iter()
.map(|((kind, _), (task, lru_score))| (kind, task, lru_score))
.sorted_by(task_lru_comparator)
.filter_map(|(kind, task, lru_score)| {
if lru_score < not_used_score {
Some((kind, task))
} else {
None
}
})
.collect::<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(),
id_base: "global tasks.json".into(),
abs_path: paths::tasks_file().clone(),
},
common_name.to_string(),
),
(
TaskSourceKind::AbsPath {
id_base: "global tasks.json".into(),
abs_path: paths::tasks_file().clone(),
},
"static_source_1".to_string(),
),
(
TaskSourceKind::AbsPath {
id_base: "test source".into(),
abs_path: path_1.to_path_buf(),
},
common_name.to_string(),
),
(
TaskSourceKind::AbsPath {
id_base: "test source".into(),
abs_path: path_2.to_path_buf(),
},
common_name.to_string(),
),
(
TaskSourceKind::AbsPath {
id_base: "test source".into(),
abs_path: path_2.to_path_buf(),
id_base: "global tasks.json".into(),
abs_path: paths::tasks_file().clone(),
},
"static_source_2".to_string(),
),
(TaskSourceKind::UserInput, common_name.to_string()),
(TaskSourceKind::UserInput, "user_input".to_string()),
];
let worktree_1_tasks = [
(
TaskSourceKind::Worktree {
id: worktree_1,
abs_path: worktree_path_1.to_path_buf(),
id_base: "test_source".into(),
directory_in_worktree: PathBuf::from(".zed"),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
common_name.to_string(),
),
(
TaskSourceKind::Worktree {
id: worktree_1,
abs_path: worktree_path_1.to_path_buf(),
id_base: "test_source".into(),
directory_in_worktree: PathBuf::from(".zed"),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
"worktree_1".to_string(),
),
@ -945,36 +739,63 @@ mod tests {
(
TaskSourceKind::Worktree {
id: worktree_2,
abs_path: worktree_path_2.to_path_buf(),
id_base: "test_source".into(),
directory_in_worktree: PathBuf::from(".zed"),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
common_name.to_string(),
),
(
TaskSourceKind::Worktree {
id: worktree_2,
abs_path: worktree_path_2.to_path_buf(),
id_base: "test_source".into(),
directory_in_worktree: PathBuf::from(".zed"),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
"worktree_2".to_string(),
),
];
let all_tasks = worktree_1_tasks
.iter()
.chain(worktree_2_tasks.iter())
// worktree-less tasks come later in the list
.chain(worktree_independent_tasks.iter())
.cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<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()
}
}

View file

@ -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)
}

View file

@ -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;

View file

@ -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,

View file

@ -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,
}

View file

@ -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)]

View file

@ -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(())
}

View file

@ -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,

View file

@ -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
})
}

View file

@ -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 (used, current) =
task_inventory.read(cx).used_and_current_resolved_tasks(
worktree,
location,
&picker.delegate.task_context,
cx,
);
picker.delegate.last_used_candidate_index = if used.is_empty() {
None
} else {
Some(used.len() - 1)
};
let mut new_candidates = used;
new_candidates.extend(current);
let match_candidates =
string_match_candidates(new_candidates.iter());
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
})
})
let mut new_candidates = used;
new_candidates.extend(current);
let match_candidates = string_match_candidates(new_candidates.iter());
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
}
}
})
@ -276,11 +251,6 @@ impl PickerDelegate for TasksModalDelegate {
else {
return;
};
let Some(candidates): Option<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);

View file

@ -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)));

View file

@ -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 =