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:
parent
f1053ff525
commit
49c75eb062
18 changed files with 1262 additions and 1366 deletions
|
@ -469,9 +469,6 @@ impl Server {
|
||||||
.add_request_handler(user_handler(
|
.add_request_handler(user_handler(
|
||||||
forward_project_request_for_owner::<proto::TaskContextForLocation>,
|
forward_project_request_for_owner::<proto::TaskContextForLocation>,
|
||||||
))
|
))
|
||||||
.add_request_handler(user_handler(
|
|
||||||
forward_project_request_for_owner::<proto::TaskTemplates>,
|
|
||||||
))
|
|
||||||
.add_request_handler(user_handler(
|
.add_request_handler(user_handler(
|
||||||
forward_read_only_project_request::<proto::GetHover>,
|
forward_read_only_project_request::<proto::GetHover>,
|
||||||
))
|
))
|
||||||
|
|
|
@ -1879,10 +1879,17 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
let task_inventory = project.read(cx).task_inventory().clone();
|
if let Some(task_inventory) = project
|
||||||
project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| {
|
.read(cx)
|
||||||
editor.tasks_update_task = Some(editor.refresh_runnables(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.update(cx, |project, cx| {
|
||||||
project.task_context_for_location(
|
project.task_store().update(cx, |task_store, cx| {
|
||||||
captured_task_variables,
|
task_store.task_context_for_location(
|
||||||
location,
|
captured_task_variables,
|
||||||
cx,
|
location,
|
||||||
)
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9134,23 +9143,29 @@ impl Editor {
|
||||||
.map(|file| (file.worktree_id(cx), file.clone()))
|
.map(|file| (file.worktree_id(cx), file.clone()))
|
||||||
.unzip();
|
.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 tags = mem::take(&mut runnable.tags);
|
||||||
let mut tags: Vec<_> = tags
|
let mut tags: Vec<_> = tags
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|tag| {
|
.flat_map(|tag| {
|
||||||
let tag = tag.0.clone();
|
let tag = tag.0.clone();
|
||||||
inventory
|
inventory
|
||||||
.list_tasks(
|
.as_ref()
|
||||||
file.clone(),
|
|
||||||
Some(runnable.language.clone()),
|
|
||||||
worktree_id,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.flat_map(|inventory| {
|
||||||
|
inventory.read(cx).list_tasks(
|
||||||
|
file.clone(),
|
||||||
|
Some(runnable.language.clone()),
|
||||||
|
worktree_id,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
.filter(move |(_, template)| {
|
.filter(move |(_, template)| {
|
||||||
template.tags.iter().any(|source_tag| source_tag == &tag)
|
template.tags.iter().any(|source_tag| source_tag == &tag)
|
||||||
})
|
})
|
||||||
|
|
|
@ -67,10 +67,11 @@ fn task_context_with_editor(
|
||||||
variables
|
variables
|
||||||
};
|
};
|
||||||
|
|
||||||
let context_task = project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
project.task_context_for_location(captured_variables, location.clone(), cx)
|
project.task_store().update(cx, |task_store, cx| {
|
||||||
});
|
task_store.task_context_for_location(captured_variables, location, cx)
|
||||||
cx.spawn(|_| context_task)
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> AsyncTask<TaskContext> {
|
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> AsyncTask<TaskContext> {
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub mod prettier_store;
|
||||||
pub mod project_settings;
|
pub mod project_settings;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
mod task_inventory;
|
mod task_inventory;
|
||||||
|
pub mod task_store;
|
||||||
pub mod terminals;
|
pub mod terminals;
|
||||||
pub mod worktree_store;
|
pub mod worktree_store;
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ pub use environment::EnvironmentErrorMessage;
|
||||||
pub mod search_history;
|
pub mod search_history;
|
||||||
mod yarn;
|
mod yarn;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use buffer_store::{BufferStore, BufferStoreEvent};
|
use buffer_store::{BufferStore, BufferStoreEvent};
|
||||||
use client::{
|
use client::{
|
||||||
proto, Client, Collaborator, DevServerProjectId, PendingEntitySubscription, ProjectId,
|
proto, Client, Collaborator, DevServerProjectId, PendingEntitySubscription, ProjectId,
|
||||||
|
@ -44,11 +45,10 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::InlayHintKind,
|
language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent,
|
||||||
proto::{deserialize_anchor, serialize_anchor, split_operations},
|
CachedLspAdapter, Capability, CodeLabel, DiagnosticEntry, Documentation, File as _, Language,
|
||||||
Buffer, BufferEvent, CachedLspAdapter, Capability, CodeLabel, ContextProvider, DiagnosticEntry,
|
LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
||||||
Documentation, File as _, Language, LanguageRegistry, LanguageServerName, PointUtf16, ToOffset,
|
Unclipped,
|
||||||
ToPointUtf16, Transaction, Unclipped,
|
|
||||||
};
|
};
|
||||||
use lsp::{
|
use lsp::{
|
||||||
CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||||
|
@ -56,16 +56,13 @@ use lsp::{
|
||||||
use lsp_command::*;
|
use lsp_command::*;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path};
|
|
||||||
pub use prettier_store::PrettierStore;
|
pub use prettier_store::PrettierStore;
|
||||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||||
use remote::SshRemoteClient;
|
use remote::SshRemoteClient;
|
||||||
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
|
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
|
||||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||||
use search_history::SearchHistory;
|
use search_history::SearchHistory;
|
||||||
use settings::{
|
use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore};
|
||||||
watch_config_file, InvalidSettingsError, Settings, SettingsLocation, SettingsStore,
|
|
||||||
};
|
|
||||||
use smol::channel::Receiver;
|
use smol::channel::Receiver;
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
use snippet_provider::SnippetProvider;
|
use snippet_provider::SnippetProvider;
|
||||||
|
@ -77,10 +74,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use task::{
|
use task_store::TaskStore;
|
||||||
static_source::{StaticSource, TrackedFile},
|
|
||||||
HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, TaskVariables, VariableName,
|
|
||||||
};
|
|
||||||
use terminals::Terminals;
|
use terminals::Terminals;
|
||||||
use text::{Anchor, BufferId};
|
use text::{Anchor, BufferId};
|
||||||
use util::{paths::compare_paths, ResultExt as _};
|
use util::{paths::compare_paths, ResultExt as _};
|
||||||
|
@ -141,6 +135,7 @@ pub struct Project {
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
client: Arc<client::Client>,
|
client: Arc<client::Client>,
|
||||||
join_project_response_message_id: u32,
|
join_project_response_message_id: u32,
|
||||||
|
task_store: Model<TaskStore>,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
ssh_client: Option<Model<SshRemoteClient>>,
|
ssh_client: Option<Model<SshRemoteClient>>,
|
||||||
|
@ -156,7 +151,6 @@ pub struct Project {
|
||||||
remotely_created_models: Arc<Mutex<RemotelyCreatedModels>>,
|
remotely_created_models: Arc<Mutex<RemotelyCreatedModels>>,
|
||||||
terminals: Terminals,
|
terminals: Terminals,
|
||||||
node: Option<NodeRuntime>,
|
node: Option<NodeRuntime>,
|
||||||
tasks: Model<Inventory>,
|
|
||||||
hosted_project_id: Option<ProjectId>,
|
hosted_project_id: Option<ProjectId>,
|
||||||
dev_server_project_id: Option<client::DevServerProjectId>,
|
dev_server_project_id: Option<client::DevServerProjectId>,
|
||||||
search_history: SearchHistory,
|
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_id);
|
||||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
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_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);
|
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
|
||||||
|
|
||||||
WorktreeStore::init(&client);
|
WorktreeStore::init(&client);
|
||||||
BufferStore::init(&client);
|
BufferStore::init(&client);
|
||||||
LspStore::init(&client);
|
LspStore::init(&client);
|
||||||
SettingsObserver::init(&client);
|
SettingsObserver::init(&client);
|
||||||
|
TaskStore::init(Some(&client));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
|
@ -590,7 +583,6 @@ impl Project {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
|
cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
|
||||||
.detach();
|
.detach();
|
||||||
let tasks = Inventory::new(cx);
|
|
||||||
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
|
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
|
||||||
let worktree_store = cx.new_model(|_| WorktreeStore::local(false, fs.clone()));
|
let worktree_store = cx.new_model(|_| WorktreeStore::local(false, fs.clone()));
|
||||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
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| {
|
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)
|
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let environment = ProjectEnvironment::new(&worktree_store, env, cx);
|
|
||||||
let lsp_store = cx.new_model(|cx| {
|
let lsp_store = cx.new_model(|cx| {
|
||||||
LspStore::new_local(
|
LspStore::new_local(
|
||||||
buffer_store.clone(),
|
buffer_store.clone(),
|
||||||
|
@ -645,6 +653,7 @@ impl Project {
|
||||||
snippets,
|
snippets,
|
||||||
languages,
|
languages,
|
||||||
client,
|
client,
|
||||||
|
task_store,
|
||||||
user_store,
|
user_store,
|
||||||
settings_observer,
|
settings_observer,
|
||||||
fs,
|
fs,
|
||||||
|
@ -655,7 +664,6 @@ impl Project {
|
||||||
local_handles: Vec::new(),
|
local_handles: Vec::new(),
|
||||||
},
|
},
|
||||||
node: Some(node),
|
node: Some(node),
|
||||||
tasks,
|
|
||||||
hosted_project_id: None,
|
hosted_project_id: None,
|
||||||
dev_server_project_id: None,
|
dev_server_project_id: None,
|
||||||
search_history: Self::new_search_history(),
|
search_history: Self::new_search_history(),
|
||||||
|
@ -681,7 +689,6 @@ impl Project {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
|
cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
|
||||||
.detach();
|
.detach();
|
||||||
let tasks = Inventory::new(cx);
|
|
||||||
let global_snippets_dir = paths::config_dir().join("snippets");
|
let global_snippets_dir = paths::config_dir().join("snippets");
|
||||||
let snippets =
|
let snippets =
|
||||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
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)
|
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||||
.detach();
|
.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| {
|
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)
|
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -748,6 +771,7 @@ impl Project {
|
||||||
snippets,
|
snippets,
|
||||||
languages,
|
languages,
|
||||||
client,
|
client,
|
||||||
|
task_store,
|
||||||
user_store,
|
user_store,
|
||||||
settings_observer,
|
settings_observer,
|
||||||
fs,
|
fs,
|
||||||
|
@ -758,7 +782,6 @@ impl Project {
|
||||||
local_handles: Vec::new(),
|
local_handles: Vec::new(),
|
||||||
},
|
},
|
||||||
node: Some(node),
|
node: Some(node),
|
||||||
tasks,
|
|
||||||
hosted_project_id: None,
|
hosted_project_id: None,
|
||||||
dev_server_project_id: None,
|
dev_server_project_id: None,
|
||||||
search_history: Self::new_search_history(),
|
search_history: Self::new_search_history(),
|
||||||
|
@ -783,6 +806,7 @@ impl Project {
|
||||||
BufferStore::init(&ssh_proto);
|
BufferStore::init(&ssh_proto);
|
||||||
LspStore::init(&ssh_proto);
|
LspStore::init(&ssh_proto);
|
||||||
SettingsObserver::init(&ssh_proto);
|
SettingsObserver::init(&ssh_proto);
|
||||||
|
TaskStore::init(Some(&ssh_proto));
|
||||||
|
|
||||||
this
|
this
|
||||||
})
|
})
|
||||||
|
@ -836,6 +860,7 @@ impl Project {
|
||||||
response,
|
response,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
client,
|
client,
|
||||||
|
false,
|
||||||
user_store,
|
user_store,
|
||||||
languages,
|
languages,
|
||||||
fs,
|
fs,
|
||||||
|
@ -844,10 +869,12 @@ impl Project {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn from_join_project_response(
|
async fn from_join_project_response(
|
||||||
response: TypedEnvelope<proto::JoinProjectResponse>,
|
response: TypedEnvelope<proto::JoinProjectResponse>,
|
||||||
subscriptions: [EntitySubscription; 5],
|
subscriptions: [EntitySubscription; 5],
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
|
run_tasks: bool,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
@ -884,12 +911,27 @@ impl Project {
|
||||||
lsp_store
|
lsp_store
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let settings_observer =
|
let task_store = cx.new_model(|cx| {
|
||||||
cx.new_model(|cx| SettingsObserver::new_remote(worktree_store.clone(), 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 this = cx.new_model(|cx| {
|
||||||
let replica_id = response.payload.replica_id as ReplicaId;
|
let replica_id = response.payload.replica_id as ReplicaId;
|
||||||
let tasks = Inventory::new(cx);
|
|
||||||
|
|
||||||
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), 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,
|
join_project_response_message_id: response.message_id,
|
||||||
languages,
|
languages,
|
||||||
user_store: user_store.clone(),
|
user_store: user_store.clone(),
|
||||||
|
task_store,
|
||||||
snippets,
|
snippets,
|
||||||
fs,
|
fs,
|
||||||
ssh_client: None,
|
ssh_client: None,
|
||||||
|
@ -943,7 +986,6 @@ impl Project {
|
||||||
local_handles: Vec::new(),
|
local_handles: Vec::new(),
|
||||||
},
|
},
|
||||||
node: None,
|
node: None,
|
||||||
tasks,
|
|
||||||
hosted_project_id: None,
|
hosted_project_id: None,
|
||||||
dev_server_project_id: response
|
dev_server_project_id: response
|
||||||
.payload
|
.payload
|
||||||
|
@ -1032,6 +1074,7 @@ impl Project {
|
||||||
response,
|
response,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
client,
|
client,
|
||||||
|
true,
|
||||||
user_store,
|
user_store,
|
||||||
languages,
|
languages,
|
||||||
fs,
|
fs,
|
||||||
|
@ -1283,8 +1326,8 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn task_inventory(&self) -> &Model<Inventory> {
|
pub fn task_store(&self) -> &Model<TaskStore> {
|
||||||
&self.tasks
|
&self.task_store
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snippets(&self) -> &Model<SnippetProvider> {
|
pub fn snippets(&self) -> &Model<SnippetProvider> {
|
||||||
|
@ -1505,6 +1548,9 @@ impl Project {
|
||||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||||
lsp_store.shared(project_id, self.client.clone().into(), 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| {
|
self.settings_observer.update(cx, |settings_observer, cx| {
|
||||||
settings_observer.shared(project_id, self.client.clone().into(), cx)
|
settings_observer.shared(project_id, self.client.clone().into(), cx)
|
||||||
});
|
});
|
||||||
|
@ -1593,9 +1639,13 @@ impl Project {
|
||||||
buffer_store.forget_shared_buffers();
|
buffer_store.forget_shared_buffers();
|
||||||
buffer_store.unshared(cx)
|
buffer_store.unshared(cx)
|
||||||
});
|
});
|
||||||
|
self.task_store.update(cx, |task_store, cx| {
|
||||||
|
task_store.unshared(cx);
|
||||||
|
});
|
||||||
self.settings_observer.update(cx, |settings_observer, cx| {
|
self.settings_observer.update(cx, |settings_observer, cx| {
|
||||||
settings_observer.unshared(cx);
|
settings_observer.unshared(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client
|
self.client
|
||||||
.send(proto::UnshareProject {
|
.send(proto::UnshareProject {
|
||||||
project_id: remote_id,
|
project_id: remote_id,
|
||||||
|
@ -2105,29 +2155,23 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
||||||
cx.subscribe(worktree, |this, worktree, event, cx| {
|
cx.subscribe(worktree, |project, worktree, event, cx| match event {
|
||||||
let is_local = worktree.read(cx).is_local();
|
worktree::Event::UpdatedEntries(changes) => {
|
||||||
match event {
|
cx.emit(Event::WorktreeUpdatedEntries(
|
||||||
worktree::Event::UpdatedEntries(changes) => {
|
worktree.read(cx).id(),
|
||||||
if is_local {
|
changes.clone(),
|
||||||
this.update_local_worktree_settings(&worktree, changes, cx);
|
));
|
||||||
}
|
|
||||||
|
|
||||||
cx.emit(Event::WorktreeUpdatedEntries(
|
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
|
||||||
worktree.read(cx).id(),
|
project
|
||||||
changes.clone(),
|
.client()
|
||||||
));
|
.telemetry()
|
||||||
|
.report_discovered_project_events(worktree_id, changes);
|
||||||
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)),
|
|
||||||
}
|
}
|
||||||
|
worktree::Event::UpdatedGitRepositories(_) => {
|
||||||
|
cx.emit(Event::WorktreeUpdatedGitRepositories);
|
||||||
|
}
|
||||||
|
worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(*id)),
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -2157,10 +2201,6 @@ impl Project {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.task_inventory().update(cx, |inventory, _| {
|
|
||||||
inventory.remove_worktree_sources(id_to_remove);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.notify();
|
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>) {
|
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||||
let new_active_entry = entry.and_then(|project_path| {
|
let new_active_entry = entry.and_then(|project_path| {
|
||||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
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| {
|
let buffer_store = this.read_with(&cx, |this, cx| {
|
||||||
if let Some(ssh) = &this.ssh_client {
|
if let Some(ssh) = &this.ssh_client {
|
||||||
let mut payload = envelope.payload.clone();
|
let mut payload = envelope.payload.clone();
|
||||||
payload.project_id = 0;
|
payload.project_id = SSH_PROJECT_ID;
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.spawn(ssh.read(cx).to_proto_client().request(payload))
|
.spawn(ssh.read(cx).to_proto_client().request(payload))
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
@ -3578,137 +3547,6 @@ impl Project {
|
||||||
Ok(response)
|
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(
|
async fn handle_search_candidate_buffers(
|
||||||
this: Model<Self>,
|
this: Model<Self>,
|
||||||
envelope: TypedEnvelope<proto::FindSearchCandidates>,
|
envelope: TypedEnvelope<proto::FindSearchCandidates>,
|
||||||
|
@ -3996,267 +3834,6 @@ impl Project {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.language_server_for_buffer(buffer, server_id, 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> {
|
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 {}
|
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]) {
|
pub fn sort_worktree_entries(entries: &mut [Entry]) {
|
||||||
entries.sort_by(|entry_a, entry_b| {
|
entries.sort_by(|entry_a, entry_b| {
|
||||||
compare_paths(
|
compare_paths(
|
||||||
|
|
|
@ -3,20 +3,30 @@ use collections::HashMap;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext};
|
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext};
|
||||||
use language::LanguageServerName;
|
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 rpc::{proto, AnyProtoClient, TypedEnvelope};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
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::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use task::{TaskTemplates, VsCodeTaskFile};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
|
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)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct ProjectSettings {
|
pub struct ProjectSettings {
|
||||||
|
@ -202,12 +212,13 @@ pub struct SettingsObserver {
|
||||||
downstream_client: Option<AnyProtoClient>,
|
downstream_client: Option<AnyProtoClient>,
|
||||||
worktree_store: Model<WorktreeStore>,
|
worktree_store: Model<WorktreeStore>,
|
||||||
project_id: u64,
|
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
|
/// (or the equivalent protobuf messages from upstream) and updates local settings
|
||||||
/// and sends notifications downstream.
|
/// 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.
|
/// upstream.
|
||||||
impl SettingsObserver {
|
impl SettingsObserver {
|
||||||
pub fn init(client: &AnyProtoClient) {
|
pub fn init(client: &AnyProtoClient) {
|
||||||
|
@ -218,6 +229,7 @@ impl SettingsObserver {
|
||||||
pub fn new_local(
|
pub fn new_local(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
worktree_store: Model<WorktreeStore>,
|
worktree_store: Model<WorktreeStore>,
|
||||||
|
task_store: Model<TaskStore>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||||
|
@ -225,6 +237,7 @@ impl SettingsObserver {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
worktree_store,
|
worktree_store,
|
||||||
|
task_store,
|
||||||
mode: SettingsObserverMode::Local(fs),
|
mode: SettingsObserverMode::Local(fs),
|
||||||
downstream_client: None,
|
downstream_client: None,
|
||||||
project_id: 0,
|
project_id: 0,
|
||||||
|
@ -234,10 +247,12 @@ impl SettingsObserver {
|
||||||
pub fn new_ssh(
|
pub fn new_ssh(
|
||||||
client: AnyProtoClient,
|
client: AnyProtoClient,
|
||||||
worktree_store: Model<WorktreeStore>,
|
worktree_store: Model<WorktreeStore>,
|
||||||
|
task_store: Model<TaskStore>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let this = Self {
|
let this = Self {
|
||||||
worktree_store,
|
worktree_store,
|
||||||
|
task_store,
|
||||||
mode: SettingsObserverMode::Ssh(client.clone()),
|
mode: SettingsObserverMode::Ssh(client.clone()),
|
||||||
downstream_client: None,
|
downstream_client: None,
|
||||||
project_id: 0,
|
project_id: 0,
|
||||||
|
@ -246,9 +261,14 @@ impl SettingsObserver {
|
||||||
this
|
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 {
|
Self {
|
||||||
worktree_store,
|
worktree_store,
|
||||||
|
task_store,
|
||||||
mode: SettingsObserverMode::Remote,
|
mode: SettingsObserverMode::Remote,
|
||||||
downstream_client: None,
|
downstream_client: None,
|
||||||
project_id: 0,
|
project_id: 0,
|
||||||
|
@ -319,19 +339,32 @@ impl SettingsObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_update_user_settings(
|
pub async fn handle_update_user_settings(
|
||||||
_: Model<Self>,
|
settings_observer: Model<Self>,
|
||||||
envelope: TypedEnvelope<proto::UpdateUserSettings>,
|
envelope: TypedEnvelope<proto::UpdateUserSettings>,
|
||||||
cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
cx.update_global(move |settings_store: &mut SettingsStore, cx| {
|
match envelope.payload.kind() {
|
||||||
settings_store.set_user_settings(&envelope.payload.content, cx)
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
|
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() {
|
if let Some(content) = serde_json::to_string(&settings).log_err() {
|
||||||
ssh.send(proto::UpdateUserSettings {
|
ssh.send(proto::UpdateUserSettings {
|
||||||
project_id: 0,
|
project_id: 0,
|
||||||
|
@ -389,7 +422,43 @@ impl SettingsObserver {
|
||||||
|
|
||||||
let mut settings_contents = Vec::new();
|
let mut settings_contents = Vec::new();
|
||||||
for (path, _, change) in changes.iter() {
|
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 removed = change == &PathChange::Removed;
|
||||||
|
let fs = fs.clone();
|
||||||
let abs_path = match worktree.read(cx).absolutize(path) {
|
let abs_path = match worktree.read(cx).absolutize(path) {
|
||||||
Ok(abs_path) => abs_path,
|
Ok(abs_path) => abs_path,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -397,26 +466,42 @@ impl SettingsObserver {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
settings_contents.push(async move {
|
||||||
if path.ends_with(local_settings_file_relative_path()) {
|
(
|
||||||
let settings_dir = Arc::from(
|
settings_dir,
|
||||||
path.ancestors()
|
kind,
|
||||||
.nth(local_settings_file_relative_path().components().count())
|
if removed {
|
||||||
.unwrap(),
|
None
|
||||||
);
|
} else {
|
||||||
let fs = fs.clone();
|
Some(
|
||||||
settings_contents.push(async move {
|
async move {
|
||||||
(
|
let content = fs.load(&abs_path).await?;
|
||||||
settings_dir,
|
if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
|
||||||
LocalSettingsKind::Settings,
|
let vscode_tasks =
|
||||||
if removed {
|
parse_json_with_comments::<VsCodeTaskFile>(&content)
|
||||||
None
|
.with_context(|| {
|
||||||
} else {
|
format!("parsing VSCode tasks, file {abs_path:?}")
|
||||||
Some(async move { fs.load(&abs_path).await }.await)
|
})?;
|
||||||
},
|
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() {
|
if settings_contents.is_empty() {
|
||||||
|
@ -450,47 +535,64 @@ impl SettingsObserver {
|
||||||
) {
|
) {
|
||||||
let worktree_id = worktree.read(cx).id();
|
let worktree_id = worktree.read(cx).id();
|
||||||
let remote_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 {
|
||||||
for (directory, kind, file_content) in settings_contents {
|
let result = match kind {
|
||||||
store.set_local_settings(
|
LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
|
||||||
worktree_id,
|
.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
|
||||||
directory.clone(),
|
store.set_local_settings(
|
||||||
kind,
|
worktree_id,
|
||||||
file_content.as_deref(),
|
directory.clone(),
|
||||||
cx,
|
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 {
|
if let Some(downstream_client) = &self.downstream_client {
|
||||||
downstream_client
|
downstream_client
|
||||||
.send(proto::UpdateWorktreeSettings {
|
.send(proto::UpdateWorktreeSettings {
|
||||||
project_id: self.project_id,
|
project_id: self.project_id,
|
||||||
worktree_id: remote_worktree_id.to_proto(),
|
worktree_id: remote_worktree_id.to_proto(),
|
||||||
path: directory.to_string_lossy().into_owned(),
|
path: directory.to_string_lossy().into_owned(),
|
||||||
content: file_content,
|
content: file_content,
|
||||||
kind: Some(local_settings_kind_to_proto(kind).into()),
|
kind: Some(local_settings_kind_to_proto(kind).into()),
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
anyhow::Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if let Ok(error) = error.downcast::<InvalidSettingsError>() {
|
if let Ok(error) = error.downcast::<InvalidSettingsError>() {
|
||||||
if let InvalidSettingsError::LocalSettings {
|
if let InvalidSettingsError::LocalSettings {
|
||||||
ref path,
|
ref path,
|
||||||
ref message,
|
ref message,
|
||||||
} = error
|
} = error
|
||||||
{
|
{
|
||||||
log::error!("Failed to set local settings in {:?}: {:?}", path, message);
|
log::error!(
|
||||||
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error)));
|
"Failed to set local settings in {:?}: {:?}",
|
||||||
|
path,
|
||||||
|
message
|
||||||
|
);
|
||||||
|
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Ok(()) => {
|
||||||
Ok(()) => {
|
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
|
||||||
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use serde_json::json;
|
||||||
use std::os;
|
use std::os;
|
||||||
|
|
||||||
use std::{mem, ops::Range, task::Poll};
|
use std::{mem, ops::Range, task::Poll};
|
||||||
use task::{ResolvedTask, TaskContext, TaskTemplate, TaskTemplates};
|
use task::{ResolvedTask, TaskContext};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt 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]
|
#[gpui::test]
|
||||||
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
|
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
TaskStore::init(None);
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
|
@ -102,7 +103,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
".zed": {
|
".zed": {
|
||||||
"settings.json": r#"{ "tab_size": 8 }"#,
|
"settings.json": r#"{ "tab_size": 8 }"#,
|
||||||
"tasks.json": r#"[{
|
"tasks.json": r#"[{
|
||||||
"label": "cargo check",
|
"label": "cargo check all",
|
||||||
"command": "cargo",
|
"command": "cargo",
|
||||||
"args": ["check", "--all"]
|
"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()
|
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,
|
id: worktree_id,
|
||||||
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
|
directory_in_worktree: PathBuf::from(".zed"),
|
||||||
id_base: "local_tasks_for_worktree".into(),
|
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let all_tasks = cx
|
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)
|
get_all_tasks(&project, Some(worktree_id), &task_context, cx)
|
||||||
})
|
})
|
||||||
.await
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(source_kind, task)| {
|
.map(|(source_kind, task)| {
|
||||||
let resolved = task.resolved.unwrap();
|
let resolved = task.resolved.unwrap();
|
||||||
|
@ -186,71 +186,65 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
all_tasks,
|
all_tasks,
|
||||||
vec![
|
vec![
|
||||||
(
|
|
||||||
global_task_source_kind.clone(),
|
|
||||||
"cargo check".to_string(),
|
|
||||||
vec!["check".to_string(), "--all".to_string()],
|
|
||||||
HashMap::default(),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: worktree_id,
|
id: worktree_id,
|
||||||
abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
|
directory_in_worktree: PathBuf::from("b/.zed"),
|
||||||
id_base: "local_tasks_for_worktree".into(),
|
id_base: "local worktree tasks from directory \"b/.zed\"".into(),
|
||||||
},
|
},
|
||||||
"cargo check".to_string(),
|
"cargo check".to_string(),
|
||||||
vec!["check".to_string()],
|
vec!["check".to_string()],
|
||||||
HashMap::default(),
|
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
|
let (_, resolved_task) = cx
|
||||||
.update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
|
.update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
|
||||||
.await
|
|
||||||
.into_iter()
|
.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");
|
.expect("should have one global task");
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, _| {
|
let task_inventory = project
|
||||||
inventory.task_scheduled(global_task_source_kind.clone(), resolved_task);
|
.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();
|
cx.run_until_parked();
|
||||||
|
|
||||||
let all_tasks = cx
|
let all_tasks = cx
|
||||||
.update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
|
.update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
|
||||||
.await
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(source_kind, task)| {
|
.map(|(source_kind, task)| {
|
||||||
let resolved = task.resolved.unwrap();
|
let resolved = task.resolved.unwrap();
|
||||||
|
@ -265,33 +259,38 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
all_tasks,
|
all_tasks,
|
||||||
vec![
|
vec![
|
||||||
|
(
|
||||||
|
topmost_local_task_source_kind.clone(),
|
||||||
|
"cargo check all".to_string(),
|
||||||
|
vec!["check".to_string(), "--all".to_string()],
|
||||||
|
HashMap::default(),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: worktree_id,
|
id: worktree_id,
|
||||||
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
|
directory_in_worktree: PathBuf::from("b/.zed"),
|
||||||
id_base: "local_tasks_for_worktree".into(),
|
id_base: "local worktree tasks from directory \"b/.zed\"".into(),
|
||||||
},
|
},
|
||||||
"cargo check".to_string(),
|
"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![
|
vec![
|
||||||
"check".to_string(),
|
"check".to_string(),
|
||||||
"--all".to_string(),
|
"--all".to_string(),
|
||||||
"--all-targets".to_string()
|
"--all-targets".to_string(),
|
||||||
],
|
],
|
||||||
HashMap::from_iter(Some((
|
HashMap::from_iter(Some((
|
||||||
"RUSTFLAGS".to_string(),
|
"RUSTFLAGS".to_string(),
|
||||||
"-Zunstable-options".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>,
|
worktree_id: Option<WorktreeId>,
|
||||||
task_context: &TaskContext,
|
task_context: &TaskContext,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Vec<(TaskSourceKind, ResolvedTask)>> {
|
) -> Vec<(TaskSourceKind, ResolvedTask)> {
|
||||||
let resolved_tasks = project.update(cx, |project, cx| {
|
let (mut old, new) = project.update(cx, |project, cx| {
|
||||||
project
|
project
|
||||||
.task_inventory()
|
.task_store
|
||||||
.read(cx)
|
.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)
|
||||||
});
|
});
|
||||||
|
old.extend(new);
|
||||||
cx.spawn(|_| async move {
|
old
|
||||||
let (mut old, new) = resolved_tasks.await;
|
|
||||||
old.extend(new);
|
|
||||||
old
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,40 +3,37 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cmp::{self, Reverse},
|
cmp::{self, Reverse},
|
||||||
|
collections::hash_map,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use collections::{btree_map, BTreeMap, HashMap, VecDeque};
|
use collections::{HashMap, HashSet, VecDeque};
|
||||||
use futures::{
|
use gpui::{AppContext, Context as _, Model};
|
||||||
channel::mpsc::{unbounded, UnboundedSender},
|
|
||||||
StreamExt,
|
|
||||||
};
|
|
||||||
use gpui::{AppContext, Context, Model, ModelContext, Task};
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{ContextProvider, File, Language, Location};
|
use language::{ContextProvider, File, Language, Location};
|
||||||
|
use settings::{parse_json_with_comments, SettingsLocation};
|
||||||
use task::{
|
use task::{
|
||||||
static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
|
ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName,
|
||||||
TaskVariables, VariableName,
|
|
||||||
};
|
};
|
||||||
use text::{Point, ToPoint};
|
use text::{Point, ToPoint};
|
||||||
use util::{post_inc, NumericPrefixWithSuffix, ResultExt};
|
use util::{post_inc, NumericPrefixWithSuffix, ResultExt as _};
|
||||||
use worktree::WorktreeId;
|
use worktree::WorktreeId;
|
||||||
|
|
||||||
use crate::Project;
|
use crate::worktree_store::WorktreeStore;
|
||||||
|
|
||||||
/// Inventory tracks available tasks for a given project.
|
/// Inventory tracks available tasks for a given project.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
pub struct Inventory {
|
pub struct Inventory {
|
||||||
sources: Vec<SourceInInventory>,
|
|
||||||
last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
|
last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
|
||||||
update_sender: UnboundedSender<()>,
|
templates_from_settings: ParsedTemplates,
|
||||||
_update_pooler: Task<anyhow::Result<()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SourceInInventory {
|
#[derive(Debug, Default)]
|
||||||
source: StaticSource,
|
struct ParsedTemplates {
|
||||||
kind: TaskSourceKind,
|
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.
|
/// 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
|
/// Tasks from the worktree's .zed/task.json
|
||||||
Worktree {
|
Worktree {
|
||||||
id: WorktreeId,
|
id: WorktreeId,
|
||||||
abs_path: PathBuf,
|
directory_in_worktree: PathBuf,
|
||||||
id_base: Cow<'static, str>,
|
id_base: Cow<'static, str>,
|
||||||
},
|
},
|
||||||
/// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
|
/// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
|
||||||
|
@ -60,20 +57,6 @@ pub enum TaskSourceKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn to_id_base(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
TaskSourceKind::UserInput => "oneshot".to_string(),
|
TaskSourceKind::UserInput => "oneshot".to_string(),
|
||||||
|
@ -83,9 +66,9 @@ impl TaskSourceKind {
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id,
|
id,
|
||||||
id_base,
|
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}"),
|
TaskSourceKind::Language { name } => format!("language_{name}"),
|
||||||
}
|
}
|
||||||
|
@ -94,61 +77,7 @@ impl TaskSourceKind {
|
||||||
|
|
||||||
impl Inventory {
|
impl Inventory {
|
||||||
pub fn new(cx: &mut AppContext) -> Model<Self> {
|
pub fn new(cx: &mut AppContext) -> Model<Self> {
|
||||||
cx.new_model(|cx| {
|
cx.new_model(|_| Self::default())
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pulls its task sources relevant to the worktree and the language given,
|
/// 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))
|
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|tasks| tasks.0.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
|
self.templates_from_settings(worktree)
|
||||||
.iter()
|
|
||||||
.filter(|source| {
|
|
||||||
let source_worktree = source.kind.worktree();
|
|
||||||
worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
|
|
||||||
})
|
|
||||||
.flat_map(|source| {
|
|
||||||
source
|
|
||||||
.source
|
|
||||||
.tasks_to_schedule()
|
|
||||||
.0
|
|
||||||
.into_iter()
|
|
||||||
.map(|task| (&source.kind, task))
|
|
||||||
})
|
|
||||||
.chain(language_tasks)
|
.chain(language_tasks)
|
||||||
.map(|(task_source_kind, task)| (task_source_kind.clone(), task))
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given.
|
/// 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,
|
/// 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.
|
/// 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(
|
pub fn used_and_current_resolved_tasks(
|
||||||
&self,
|
&self,
|
||||||
remote_templates_task: Option<Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>>>,
|
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
location: Option<Location>,
|
location: Option<Location>,
|
||||||
task_context: &TaskContext,
|
task_context: &TaskContext,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Task<(
|
) -> (
|
||||||
Vec<(TaskSourceKind, ResolvedTask)>,
|
Vec<(TaskSourceKind, ResolvedTask)>,
|
||||||
Vec<(TaskSourceKind, ResolvedTask)>,
|
Vec<(TaskSourceKind, ResolvedTask)>,
|
||||||
)> {
|
) {
|
||||||
let language = location
|
let language = location
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|location| location.buffer.read(cx).language_at(location.range.start));
|
.and_then(|location| location.buffer.read(cx).language_at(location.range.start));
|
||||||
|
@ -212,14 +126,10 @@ impl Inventory {
|
||||||
let file = location
|
let file = location
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|location| location.buffer.read(cx).file().cloned());
|
.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 lru_score = 0_u32;
|
||||||
let mut task_usage = self
|
let previously_spawned_tasks = self
|
||||||
.last_scheduled_tasks
|
.last_scheduled_tasks
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
|
@ -230,127 +140,64 @@ impl Inventory {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fold(
|
.filter(|(_, resolved_task)| {
|
||||||
BTreeMap::default(),
|
match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
|
||||||
|mut tasks, (task_source_kind, resolved_task)| {
|
hash_map::Entry::Occupied(mut o) => {
|
||||||
tasks.entry(&resolved_task.id).or_insert_with(|| {
|
o.get_mut().insert(resolved_task.id.clone());
|
||||||
(task_source_kind, resolved_task, post_inc(&mut lru_score))
|
// Neber allow duplicate reused tasks with the same labels
|
||||||
});
|
false
|
||||||
tasks
|
}
|
||||||
},
|
hash_map::Entry::Vacant(v) => {
|
||||||
);
|
v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
|
||||||
let not_used_score = post_inc(&mut lru_score);
|
true
|
||||||
let mut currently_resolved_tasks = self
|
}
|
||||||
.sources
|
}
|
||||||
.iter()
|
|
||||||
.filter(|source| {
|
|
||||||
let source_worktree = source.kind.worktree();
|
|
||||||
worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
|
|
||||||
})
|
})
|
||||||
.flat_map(|source| {
|
.map(|(task_source_kind, resolved_task)| {
|
||||||
source
|
(
|
||||||
.source
|
task_source_kind.clone(),
|
||||||
.tasks_to_schedule()
|
resolved_task.clone(),
|
||||||
.0
|
post_inc(&mut lru_score),
|
||||||
.into_iter()
|
)
|
||||||
.map(|task| (&source.kind, task))
|
|
||||||
})
|
})
|
||||||
.chain(language_tasks.filter(|_| remote_templates_task.is_none()))
|
.sorted_unstable_by(task_lru_comparator)
|
||||||
.filter_map(|(kind, task)| {
|
.map(|(kind, task, _)| (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))
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let task_context = task_context.clone();
|
let not_used_score = post_inc(&mut lru_score);
|
||||||
cx.spawn(move |_| async move {
|
let language_tasks = language
|
||||||
let remote_templates = match remote_templates_task {
|
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
|
||||||
Some(task) => match task.await.log_err() {
|
.into_iter()
|
||||||
Some(remote_templates) => remote_templates,
|
.flat_map(|tasks| tasks.0.into_iter())
|
||||||
None => return (Vec::new(), Vec::new()),
|
.flat_map(|task| Some((task_source_kind.clone()?, task)));
|
||||||
},
|
let new_resolved_tasks = self
|
||||||
None => Vec::new(),
|
.templates_from_settings(worktree)
|
||||||
};
|
.chain(language_tasks)
|
||||||
let remote_tasks = remote_templates.into_iter().filter_map(|(kind, task)| {
|
.filter_map(|(kind, task)| {
|
||||||
let id_base = kind.to_id_base();
|
let id_base = kind.to_id_base();
|
||||||
Some((
|
Some((
|
||||||
kind,
|
kind,
|
||||||
task.resolve_task(&id_base, &task_context)?,
|
task.resolve_task(&id_base, task_context)?,
|
||||||
not_used_score,
|
not_used_score,
|
||||||
))
|
))
|
||||||
});
|
})
|
||||||
currently_resolved_tasks.extend(remote_tasks);
|
.filter(|(_, resolved_task, _)| {
|
||||||
|
match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
|
||||||
let mut tasks_by_label = BTreeMap::default();
|
hash_map::Entry::Occupied(mut o) => {
|
||||||
tasks_by_label = previously_spawned_tasks.into_iter().fold(
|
// Allow new tasks with the same label, if their context is different
|
||||||
tasks_by_label,
|
o.get_mut().insert(resolved_task.id.clone())
|
||||||
|mut tasks_by_label, (source, task, lru_score)| {
|
|
||||||
match tasks_by_label.entry((source, task.resolved_label.clone())) {
|
|
||||||
btree_map::Entry::Occupied(mut o) => {
|
|
||||||
let (_, previous_lru_score) = o.get();
|
|
||||||
if previous_lru_score >= &lru_score {
|
|
||||||
o.insert((task, lru_score));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btree_map::Entry::Vacant(v) => {
|
|
||||||
v.insert((task, lru_score));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tasks_by_label
|
hash_map::Entry::Vacant(v) => {
|
||||||
},
|
v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
|
||||||
);
|
true
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tasks_by_label
|
}
|
||||||
},
|
})
|
||||||
);
|
.sorted_unstable_by(task_lru_comparator)
|
||||||
|
.map(|(kind, task, _)| (kind, task))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let resolved = tasks_by_label
|
(previously_spawned_tasks, new_resolved_tasks)
|
||||||
.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(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the last scheduled task by task_id if provided.
|
/// 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) {
|
pub fn delete_previously_used(&mut self, id: &TaskId) {
|
||||||
self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
|
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(
|
fn task_lru_comparator(
|
||||||
|
@ -432,39 +359,14 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_inventory {
|
mod test_inventory {
|
||||||
use gpui::{AppContext, Model, TestAppContext};
|
use gpui::{Model, TestAppContext};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use task::{
|
use task::TaskContext;
|
||||||
static_source::{StaticSource, TrackedFile},
|
|
||||||
TaskContext, TaskTemplate, TaskTemplates,
|
|
||||||
};
|
|
||||||
use worktree::WorktreeId;
|
use worktree::WorktreeId;
|
||||||
|
|
||||||
use crate::Inventory;
|
use crate::Inventory;
|
||||||
|
|
||||||
use super::{task_source_kind_preference, TaskSourceKind, UnboundedSender};
|
use super::{task_source_kind_preference, TaskSourceKind};
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn task_template_names(
|
pub(super) fn task_template_names(
|
||||||
inventory: &Model<Inventory>,
|
inventory: &Model<Inventory>,
|
||||||
|
@ -506,17 +408,9 @@ mod test_inventory {
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<(TaskSourceKind, String)> {
|
) -> Vec<(TaskSourceKind, String)> {
|
||||||
let (used, current) = inventory
|
let (used, current) = inventory.update(cx, |inventory, cx| {
|
||||||
.update(cx, |inventory, cx| {
|
inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
|
||||||
inventory.used_and_current_resolved_tasks(
|
});
|
||||||
None,
|
|
||||||
worktree,
|
|
||||||
None,
|
|
||||||
&TaskContext::default(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let mut all = used;
|
let mut all = used;
|
||||||
all.extend(current);
|
all.extend(current);
|
||||||
all.into_iter()
|
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.
|
/// 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.
|
/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
|
||||||
pub struct BasicContextProvider {
|
pub struct BasicContextProvider {
|
||||||
project: Model<Project>,
|
worktree_store: Model<WorktreeStore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BasicContextProvider {
|
impl BasicContextProvider {
|
||||||
pub fn new(project: Model<Project>) -> Self {
|
pub fn new(worktree_store: Model<WorktreeStore>) -> Self {
|
||||||
Self { project }
|
Self { worktree_store }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -585,7 +479,7 @@ impl ContextProvider for BasicContextProvider {
|
||||||
.file()
|
.file()
|
||||||
.map(|file| file.worktree_id(cx))
|
.map(|file| file.worktree_id(cx))
|
||||||
.and_then(|worktree_id| {
|
.and_then(|worktree_id| {
|
||||||
self.project
|
self.worktree_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.worktree_for_id(worktree_id, cx)
|
.worktree_for_id(worktree_id, cx)
|
||||||
.map(|worktree| worktree.read(cx).abs_path())
|
.map(|worktree| worktree.read(cx).abs_path())
|
||||||
|
@ -653,12 +547,17 @@ impl ContextProvider for ContextProviderWithTasks {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::task_store::TaskStore;
|
||||||
|
|
||||||
use super::test_inventory::*;
|
use super::test_inventory::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_task_list_sorting(cx: &mut TestAppContext) {
|
async fn test_task_list_sorting(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let inventory = cx.update(Inventory::new);
|
let inventory = cx.update(Inventory::new);
|
||||||
let initial_tasks = resolved_task_names(&inventory, None, cx).await;
|
let initial_tasks = resolved_task_names(&inventory, None, cx).await;
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -670,31 +569,6 @@ mod tests {
|
||||||
initial_tasks.is_empty(),
|
initial_tasks.is_empty(),
|
||||||
"No tasks expected for empty inventory, but got {initial_tasks:?}"
|
"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();
|
cx.run_until_parked();
|
||||||
let expected_initial_state = [
|
let expected_initial_state = [
|
||||||
"1_a_task".to_string(),
|
"1_a_task".to_string(),
|
||||||
|
@ -702,6 +576,17 @@ mod tests {
|
||||||
"2_task".to_string(),
|
"2_task".to_string(),
|
||||||
"3_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!(
|
assert_eq!(
|
||||||
task_template_names(&inventory, None, cx),
|
task_template_names(&inventory, None, cx),
|
||||||
&expected_initial_state,
|
&expected_initial_state,
|
||||||
|
@ -720,7 +605,6 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved_task_names(&inventory, None, cx).await,
|
resolved_task_names(&inventory, None, cx).await,
|
||||||
vec![
|
vec![
|
||||||
"2_task".to_string(),
|
|
||||||
"2_task".to_string(),
|
"2_task".to_string(),
|
||||||
"1_a_task".to_string(),
|
"1_a_task".to_string(),
|
||||||
"1_task".to_string(),
|
"1_task".to_string(),
|
||||||
|
@ -739,9 +623,6 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved_task_names(&inventory, None, cx).await,
|
resolved_task_names(&inventory, None, cx).await,
|
||||||
vec![
|
vec![
|
||||||
"3_task".to_string(),
|
|
||||||
"1_task".to_string(),
|
|
||||||
"2_task".to_string(),
|
|
||||||
"3_task".to_string(),
|
"3_task".to_string(),
|
||||||
"1_task".to_string(),
|
"1_task".to_string(),
|
||||||
"2_task".to_string(),
|
"2_task".to_string(),
|
||||||
|
@ -749,14 +630,17 @@ mod tests {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, _| {
|
||||||
inventory.add_source(
|
inventory
|
||||||
TaskSourceKind::UserInput,
|
.update_file_based_tasks(
|
||||||
|tx, cx| {
|
None,
|
||||||
static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], tx, cx)
|
Some(&mock_tasks_from_names(
|
||||||
},
|
["10_hello", "11_hello"]
|
||||||
cx,
|
.into_iter()
|
||||||
);
|
.chain(expected_initial_state.iter().map(|name| name.as_str())),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
let expected_updated_state = [
|
let expected_updated_state = [
|
||||||
|
@ -774,9 +658,6 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved_task_names(&inventory, None, cx).await,
|
resolved_task_names(&inventory, None, cx).await,
|
||||||
vec![
|
vec![
|
||||||
"3_task".to_string(),
|
|
||||||
"1_task".to_string(),
|
|
||||||
"2_task".to_string(),
|
|
||||||
"3_task".to_string(),
|
"3_task".to_string(),
|
||||||
"1_task".to_string(),
|
"1_task".to_string(),
|
||||||
"2_task".to_string(),
|
"2_task".to_string(),
|
||||||
|
@ -794,10 +675,6 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved_task_names(&inventory, None, cx).await,
|
resolved_task_names(&inventory, None, cx).await,
|
||||||
vec![
|
vec![
|
||||||
"11_hello".to_string(),
|
|
||||||
"3_task".to_string(),
|
|
||||||
"1_task".to_string(),
|
|
||||||
"2_task".to_string(),
|
|
||||||
"11_hello".to_string(),
|
"11_hello".to_string(),
|
||||||
"3_task".to_string(),
|
"3_task".to_string(),
|
||||||
"1_task".to_string(),
|
"1_task".to_string(),
|
||||||
|
@ -810,133 +687,50 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
|
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 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_1 = WorktreeId::from_usize(1);
|
||||||
let worktree_path_1 = Path::new("worktree_path_1");
|
|
||||||
let worktree_2 = WorktreeId::from_usize(2);
|
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();
|
cx.run_until_parked();
|
||||||
let worktree_independent_tasks = vec![
|
let worktree_independent_tasks = vec![
|
||||||
(
|
(
|
||||||
TaskSourceKind::AbsPath {
|
TaskSourceKind::AbsPath {
|
||||||
id_base: "test source".into(),
|
id_base: "global tasks.json".into(),
|
||||||
abs_path: path_1.to_path_buf(),
|
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(),
|
"static_source_1".to_string(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TaskSourceKind::AbsPath {
|
TaskSourceKind::AbsPath {
|
||||||
id_base: "test source".into(),
|
id_base: "global tasks.json".into(),
|
||||||
abs_path: path_1.to_path_buf(),
|
abs_path: paths::tasks_file().clone(),
|
||||||
},
|
|
||||||
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(),
|
|
||||||
},
|
},
|
||||||
"static_source_2".to_string(),
|
"static_source_2".to_string(),
|
||||||
),
|
),
|
||||||
(TaskSourceKind::UserInput, common_name.to_string()),
|
|
||||||
(TaskSourceKind::UserInput, "user_input".to_string()),
|
|
||||||
];
|
];
|
||||||
let worktree_1_tasks = [
|
let worktree_1_tasks = [
|
||||||
(
|
(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: worktree_1,
|
id: worktree_1,
|
||||||
abs_path: worktree_path_1.to_path_buf(),
|
directory_in_worktree: PathBuf::from(".zed"),
|
||||||
id_base: "test_source".into(),
|
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||||
},
|
},
|
||||||
common_name.to_string(),
|
common_name.to_string(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: worktree_1,
|
id: worktree_1,
|
||||||
abs_path: worktree_path_1.to_path_buf(),
|
directory_in_worktree: PathBuf::from(".zed"),
|
||||||
id_base: "test_source".into(),
|
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||||
},
|
},
|
||||||
"worktree_1".to_string(),
|
"worktree_1".to_string(),
|
||||||
),
|
),
|
||||||
|
@ -945,36 +739,63 @@ mod tests {
|
||||||
(
|
(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: worktree_2,
|
id: worktree_2,
|
||||||
abs_path: worktree_path_2.to_path_buf(),
|
directory_in_worktree: PathBuf::from(".zed"),
|
||||||
id_base: "test_source".into(),
|
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||||
},
|
},
|
||||||
common_name.to_string(),
|
common_name.to_string(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: worktree_2,
|
id: worktree_2,
|
||||||
abs_path: worktree_path_2.to_path_buf(),
|
directory_in_worktree: PathBuf::from(".zed"),
|
||||||
id_base: "test_source".into(),
|
id_base: "local worktree tasks from directory \".zed\"".into(),
|
||||||
},
|
},
|
||||||
"worktree_2".to_string(),
|
"worktree_2".to_string(),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let all_tasks = worktree_1_tasks
|
inventory.update(cx, |inventory, _| {
|
||||||
.iter()
|
inventory
|
||||||
.chain(worktree_2_tasks.iter())
|
.update_file_based_tasks(
|
||||||
// worktree-less tasks come later in the list
|
None,
|
||||||
.chain(worktree_independent_tasks.iter())
|
Some(&mock_tasks_from_names(
|
||||||
.cloned()
|
worktree_independent_tasks
|
||||||
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
|
.iter()
|
||||||
.collect::<Vec<_>>();
|
.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!(
|
assert_eq!(
|
||||||
list_tasks(&inventory_with_statics, None, cx).await,
|
list_tasks(&inventory, None, cx).await,
|
||||||
all_tasks
|
worktree_independent_tasks,
|
||||||
|
"Without a worktree, only worktree-independent tasks should be listed"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_tasks(&inventory_with_statics, Some(worktree_1), cx).await,
|
list_tasks(&inventory, Some(worktree_1), cx).await,
|
||||||
worktree_1_tasks
|
worktree_1_tasks
|
||||||
.iter()
|
.iter()
|
||||||
.chain(worktree_independent_tasks.iter())
|
.chain(worktree_independent_tasks.iter())
|
||||||
|
@ -983,7 +804,7 @@ mod tests {
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_tasks(&inventory_with_statics, Some(worktree_2), cx).await,
|
list_tasks(&inventory, Some(worktree_2), cx).await,
|
||||||
worktree_2_tasks
|
worktree_2_tasks
|
||||||
.iter()
|
.iter()
|
||||||
.chain(worktree_independent_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(
|
pub(super) async fn resolved_task_names(
|
||||||
inventory: &Model<Inventory>,
|
inventory: &Model<Inventory>,
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let (used, current) = inventory
|
let (used, current) = inventory.update(cx, |inventory, cx| {
|
||||||
.update(cx, |inventory, cx| {
|
inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
|
||||||
inventory.used_and_current_resolved_tasks(
|
});
|
||||||
None,
|
|
||||||
worktree,
|
|
||||||
None,
|
|
||||||
&TaskContext::default(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
used.into_iter()
|
used.into_iter()
|
||||||
.chain(current)
|
.chain(current)
|
||||||
.map(|(_, task)| task.original_task().label.clone())
|
.map(|(_, task)| task.original_task().label.clone())
|
||||||
.collect()
|
.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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
432
crates/project/src/task_store.rs
Normal file
432
crates/project/src/task_store.rs
Normal 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)
|
||||||
|
}
|
|
@ -245,8 +245,6 @@ message Envelope {
|
||||||
|
|
||||||
TaskContextForLocation task_context_for_location = 203;
|
TaskContextForLocation task_context_for_location = 203;
|
||||||
TaskContext task_context = 204;
|
TaskContext task_context = 204;
|
||||||
TaskTemplatesResponse task_templates_response = 205;
|
|
||||||
TaskTemplates task_templates = 206;
|
|
||||||
|
|
||||||
LinkedEditingRange linked_editing_range = 209;
|
LinkedEditingRange linked_editing_range = 209;
|
||||||
LinkedEditingRangeResponse linked_editing_range_response = 210;
|
LinkedEditingRangeResponse linked_editing_range_response = 210;
|
||||||
|
@ -290,6 +288,7 @@ message Envelope {
|
||||||
reserved 87 to 88;
|
reserved 87 to 88;
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
reserved 166 to 169;
|
reserved 166 to 169;
|
||||||
|
reserved 205 to 206;
|
||||||
reserved 224 to 229;
|
reserved 224 to 229;
|
||||||
reserved 247 to 254;
|
reserved 247 to 254;
|
||||||
}
|
}
|
||||||
|
@ -2260,6 +2259,7 @@ message GetSupermavenApiKeyResponse {
|
||||||
message TaskContextForLocation {
|
message TaskContextForLocation {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
Location location = 2;
|
Location location = 2;
|
||||||
|
map<string, string> task_variables = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TaskContext {
|
message TaskContext {
|
||||||
|
@ -2268,35 +2268,6 @@ message TaskContext {
|
||||||
map<string, string> project_env = 3;
|
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 Shell {
|
||||||
message WithArguments {
|
message WithArguments {
|
||||||
string program = 1;
|
string program = 1;
|
||||||
|
@ -2323,32 +2294,6 @@ enum HideStrategy {
|
||||||
HideOnSuccess = 2;
|
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 {
|
message ContextMessageStatus {
|
||||||
oneof variant {
|
oneof variant {
|
||||||
Done done = 1;
|
Done done = 1;
|
||||||
|
|
|
@ -290,8 +290,6 @@ messages!(
|
||||||
(SynchronizeBuffersResponse, Foreground),
|
(SynchronizeBuffersResponse, Foreground),
|
||||||
(TaskContextForLocation, Background),
|
(TaskContextForLocation, Background),
|
||||||
(TaskContext, Background),
|
(TaskContext, Background),
|
||||||
(TaskTemplates, Background),
|
|
||||||
(TaskTemplatesResponse, Background),
|
|
||||||
(Test, Foreground),
|
(Test, Foreground),
|
||||||
(Unfollow, Foreground),
|
(Unfollow, Foreground),
|
||||||
(UnshareProject, Foreground),
|
(UnshareProject, Foreground),
|
||||||
|
@ -460,7 +458,6 @@ request_messages!(
|
||||||
(ShareProject, ShareProjectResponse),
|
(ShareProject, ShareProjectResponse),
|
||||||
(SynchronizeBuffers, SynchronizeBuffersResponse),
|
(SynchronizeBuffers, SynchronizeBuffersResponse),
|
||||||
(TaskContextForLocation, TaskContext),
|
(TaskContextForLocation, TaskContext),
|
||||||
(TaskTemplates, TaskTemplatesResponse),
|
|
||||||
(Test, Test),
|
(Test, Test),
|
||||||
(UpdateBuffer, Ack),
|
(UpdateBuffer, Ack),
|
||||||
(UpdateParticipantLocation, Ack),
|
(UpdateParticipantLocation, Ack),
|
||||||
|
@ -543,7 +540,6 @@ entity_messages!(
|
||||||
StartLanguageServer,
|
StartLanguageServer,
|
||||||
SynchronizeBuffers,
|
SynchronizeBuffers,
|
||||||
TaskContextForLocation,
|
TaskContextForLocation,
|
||||||
TaskTemplates,
|
|
||||||
UnshareProject,
|
UnshareProject,
|
||||||
UpdateBuffer,
|
UpdateBuffer,
|
||||||
UpdateBufferFile,
|
UpdateBufferFile,
|
||||||
|
|
|
@ -7,6 +7,7 @@ use project::{
|
||||||
buffer_store::{BufferStore, BufferStoreEvent},
|
buffer_store::{BufferStore, BufferStoreEvent},
|
||||||
project_settings::SettingsObserver,
|
project_settings::SettingsObserver,
|
||||||
search::SearchQuery,
|
search::SearchQuery,
|
||||||
|
task_store::TaskStore,
|
||||||
worktree_store::WorktreeStore,
|
worktree_store::WorktreeStore,
|
||||||
LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId,
|
LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId,
|
||||||
};
|
};
|
||||||
|
@ -29,6 +30,7 @@ pub struct HeadlessProject {
|
||||||
pub worktree_store: Model<WorktreeStore>,
|
pub worktree_store: Model<WorktreeStore>,
|
||||||
pub buffer_store: Model<BufferStore>,
|
pub buffer_store: Model<BufferStore>,
|
||||||
pub lsp_store: Model<LspStore>,
|
pub lsp_store: Model<LspStore>,
|
||||||
|
pub task_store: Model<TaskStore>,
|
||||||
pub settings_observer: Model<SettingsObserver>,
|
pub settings_observer: Model<SettingsObserver>,
|
||||||
pub next_entry_id: Arc<AtomicUsize>,
|
pub next_entry_id: Arc<AtomicUsize>,
|
||||||
pub languages: Arc<LanguageRegistry>,
|
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 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.shared(SSH_PROJECT_ID, session.clone().into(), cx);
|
||||||
observer
|
observer
|
||||||
});
|
});
|
||||||
let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
|
|
||||||
let lsp_store = cx.new_model(|cx| {
|
let lsp_store = cx.new_model(|cx| {
|
||||||
let mut lsp_store = LspStore::new_local(
|
let mut lsp_store = LspStore::new_local(
|
||||||
buffer_store.clone(),
|
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, &buffer_store);
|
||||||
session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
|
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, &lsp_store);
|
||||||
|
session.subscribe_to_entity(SSH_PROJECT_ID, &task_store);
|
||||||
session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
|
session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
|
||||||
|
|
||||||
client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
|
client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
|
||||||
|
@ -126,6 +145,7 @@ impl HeadlessProject {
|
||||||
WorktreeStore::init(&client);
|
WorktreeStore::init(&client);
|
||||||
SettingsObserver::init(&client);
|
SettingsObserver::init(&client);
|
||||||
LspStore::init(&client);
|
LspStore::init(&client);
|
||||||
|
TaskStore::init(Some(&client));
|
||||||
|
|
||||||
HeadlessProject {
|
HeadlessProject {
|
||||||
session: client,
|
session: client,
|
||||||
|
@ -134,6 +154,7 @@ impl HeadlessProject {
|
||||||
worktree_store,
|
worktree_store,
|
||||||
buffer_store,
|
buffer_store,
|
||||||
lsp_store,
|
lsp_store,
|
||||||
|
task_store,
|
||||||
next_entry_id: Default::default(),
|
next_entry_id: Default::default(),
|
||||||
languages,
|
languages,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ pub use json_schema::*;
|
||||||
pub use keymap_file::KeymapFile;
|
pub use keymap_file::KeymapFile;
|
||||||
pub use settings_file::*;
|
pub use settings_file::*;
|
||||||
pub use settings_store::{
|
pub use settings_store::{
|
||||||
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
|
parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
|
||||||
SettingsStore,
|
SettingsSources, SettingsStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||||
|
|
|
@ -515,13 +515,11 @@ impl SettingsStore {
|
||||||
} else {
|
} else {
|
||||||
parse_json_with_comments(user_settings_content)?
|
parse_json_with_comments(user_settings_content)?
|
||||||
};
|
};
|
||||||
if settings.is_object() {
|
|
||||||
self.raw_user_settings = settings;
|
anyhow::ensure!(settings.is_object(), "settings must be an object");
|
||||||
self.recompute_values(None, cx)?;
|
self.raw_user_settings = settings;
|
||||||
Ok(())
|
self.recompute_values(None, cx)?;
|
||||||
} else {
|
Ok(())
|
||||||
Err(anyhow!("settings must be an object"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add or remove a set of local settings via a JSON string.
|
/// Add or remove a set of local settings via a JSON string.
|
||||||
|
@ -533,16 +531,29 @@ impl SettingsStore {
|
||||||
settings_content: Option<&str>,
|
settings_content: Option<&str>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
anyhow::ensure!(
|
||||||
|
kind != LocalSettingsKind::Tasks,
|
||||||
|
"Attempted to submit tasks into the settings store"
|
||||||
|
);
|
||||||
|
|
||||||
let raw_local_settings = self
|
let raw_local_settings = self
|
||||||
.raw_local_settings
|
.raw_local_settings
|
||||||
.entry((root_id, directory_path.clone()))
|
.entry((root_id, directory_path.clone()))
|
||||||
.or_default();
|
.or_default();
|
||||||
if settings_content.is_some_and(|content| !content.is_empty()) {
|
let changed = if settings_content.is_some_and(|content| !content.is_empty()) {
|
||||||
raw_local_settings.insert(kind, parse_json_with_comments(settings_content.unwrap())?);
|
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 {
|
} 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -140,8 +140,13 @@ impl FromStr for VariableName {
|
||||||
let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
|
let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
|
||||||
let value = match without_prefix {
|
let value = match without_prefix {
|
||||||
"FILE" => Self::File,
|
"FILE" => Self::File,
|
||||||
|
"FILENAME" => Self::Filename,
|
||||||
|
"RELATIVE_FILE" => Self::RelativeFile,
|
||||||
|
"DIRNAME" => Self::Dirname,
|
||||||
|
"STEM" => Self::Stem,
|
||||||
"WORKTREE_ROOT" => Self::WorktreeRoot,
|
"WORKTREE_ROOT" => Self::WorktreeRoot,
|
||||||
"SYMBOL" => Self::Symbol,
|
"SYMBOL" => Self::Symbol,
|
||||||
|
"RUNNABLE_SYMBOL" => Self::RunnableSymbol,
|
||||||
"SELECTED_TEXT" => Self::SelectedText,
|
"SELECTED_TEXT" => Self::SelectedText,
|
||||||
"ROW" => Self::Row,
|
"ROW" => Self::Row,
|
||||||
"COLUMN" => Self::Column,
|
"COLUMN" => Self::Column,
|
||||||
|
|
|
@ -18,10 +18,14 @@ pub fn init(cx: &mut AppContext) {
|
||||||
workspace
|
workspace
|
||||||
.register_action(spawn_task_or_modal)
|
.register_action(spawn_task_or_modal)
|
||||||
.register_action(move |workspace, action: &modal::Rerun, cx| {
|
.register_action(move |workspace, action: &modal::Rerun, cx| {
|
||||||
if let Some((task_source_kind, mut last_scheduled_task)) =
|
if let Some((task_source_kind, mut last_scheduled_task)) = workspace
|
||||||
workspace.project().update(cx, |project, cx| {
|
.project()
|
||||||
project
|
.read(cx)
|
||||||
.task_inventory()
|
.task_store()
|
||||||
|
.read(cx)
|
||||||
|
.task_inventory()
|
||||||
|
.and_then(|inventory| {
|
||||||
|
inventory
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.last_scheduled_task(action.task_id.as_ref())
|
.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<()> {
|
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 workspace_handle = workspace.weak_handle();
|
||||||
let context_task = task_context(workspace, cx);
|
let can_open_modal = workspace.project().update(cx, |project, cx| {
|
||||||
cx.spawn(|workspace, mut cx| async move {
|
project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh()
|
||||||
let task_context = context_task.await;
|
});
|
||||||
workspace
|
if can_open_modal {
|
||||||
.update(&mut cx, |workspace, cx| {
|
let context_task = task_context(workspace, cx);
|
||||||
if workspace.project().update(cx, |project, cx| {
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
project.is_local() || project.ssh_connection_string(cx).is_some()
|
let task_context = context_task.await;
|
||||||
}) {
|
workspace
|
||||||
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.toggle_modal(cx, |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(
|
fn spawn_task_with_name(
|
||||||
|
@ -113,14 +120,31 @@ fn spawn_task_with_name(
|
||||||
let context_task =
|
let context_task =
|
||||||
workspace.update(&mut cx, |workspace, cx| task_context(workspace, cx))?;
|
workspace.update(&mut cx, |workspace, cx| task_context(workspace, cx))?;
|
||||||
let task_context = context_task.await;
|
let task_context = context_task.await;
|
||||||
let tasks = workspace
|
let tasks = workspace.update(&mut cx, |workspace, cx| {
|
||||||
.update(&mut cx, |workspace, cx| {
|
let Some(task_inventory) = workspace
|
||||||
let (worktree, location) = active_item_selection_properties(workspace, cx);
|
.project()
|
||||||
workspace.project().update(cx, |project, cx| {
|
.read(cx)
|
||||||
project.task_templates(worktree, location, 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),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})?
|
.unwrap_or_default();
|
||||||
.await?;
|
task_inventory
|
||||||
|
.read(cx)
|
||||||
|
.list_tasks(file, language, worktree, cx)
|
||||||
|
})?;
|
||||||
|
|
||||||
let did_spawn = workspace
|
let did_spawn = workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
|
@ -185,7 +209,7 @@ mod tests {
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{Entity, TestAppContext};
|
use gpui::{Entity, TestAppContext};
|
||||||
use language::{Language, LanguageConfig};
|
use language::{Language, LanguageConfig};
|
||||||
use project::{BasicContextProvider, FakeFs, Project};
|
use project::{task_store::TaskStore, BasicContextProvider, FakeFs, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use task::{TaskContext, TaskVariables, VariableName};
|
use task::{TaskContext, TaskVariables, VariableName};
|
||||||
use ui::VisualContext;
|
use ui::VisualContext;
|
||||||
|
@ -223,6 +247,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let project = Project::test(fs, ["/dir".as_ref()], cx).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(
|
let rust_language = Arc::new(
|
||||||
Language::new(
|
Language::new(
|
||||||
LanguageConfig::default(),
|
LanguageConfig::default(),
|
||||||
|
@ -234,7 +259,9 @@ mod tests {
|
||||||
name: (_) @name) @item"#,
|
name: (_) @name) @item"#,
|
||||||
)
|
)
|
||||||
.unwrap()
|
.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(
|
let typescript_language = Arc::new(
|
||||||
|
@ -252,7 +279,9 @@ mod tests {
|
||||||
")" @context)) @item"#,
|
")" @context)) @item"#,
|
||||||
)
|
)
|
||||||
.unwrap()
|
.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| {
|
let worktree_id = project.update(cx, |project, cx| {
|
||||||
|
@ -373,6 +402,7 @@ mod tests {
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
workspace::init_settings(cx);
|
workspace::init_settings(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
TaskStore::init(None);
|
||||||
state
|
state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use gpui::{
|
||||||
View, ViewContext, VisualContext, WeakView,
|
View, ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
|
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 task::{ResolvedTask, TaskContext, TaskId, TaskTemplate};
|
||||||
use ui::{
|
use ui::{
|
||||||
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
|
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.
|
/// A modal used to spawn new tasks.
|
||||||
pub(crate) struct TasksModalDelegate {
|
pub(crate) struct TasksModalDelegate {
|
||||||
project: Model<Project>,
|
task_store: Model<TaskStore>,
|
||||||
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
|
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
|
||||||
last_used_candidate_index: Option<usize>,
|
last_used_candidate_index: Option<usize>,
|
||||||
divider_index: Option<usize>,
|
divider_index: Option<usize>,
|
||||||
|
@ -77,12 +77,12 @@ pub(crate) struct TasksModalDelegate {
|
||||||
|
|
||||||
impl TasksModalDelegate {
|
impl TasksModalDelegate {
|
||||||
fn new(
|
fn new(
|
||||||
project: Model<Project>,
|
task_store: Model<TaskStore>,
|
||||||
task_context: TaskContext,
|
task_context: TaskContext,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
project,
|
task_store,
|
||||||
workspace,
|
workspace,
|
||||||
candidates: None,
|
candidates: None,
|
||||||
matches: Vec::new(),
|
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
|
// 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.
|
// the original list without a removed entry.
|
||||||
candidates.remove(ix);
|
candidates.remove(ix);
|
||||||
self.project.update(cx, |project, cx| {
|
if let Some(inventory) = self.task_store.read(cx).task_inventory().cloned() {
|
||||||
project.task_inventory().update(cx, |inventory, _| {
|
inventory.update(cx, |inventory, _| {
|
||||||
inventory.delete_previously_used(&task.id);
|
inventory.delete_previously_used(&task.id);
|
||||||
})
|
})
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,14 +139,14 @@ pub(crate) struct TasksModal {
|
||||||
|
|
||||||
impl TasksModal {
|
impl TasksModal {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
project: Model<Project>,
|
task_store: Model<TaskStore>,
|
||||||
task_context: TaskContext,
|
task_context: TaskContext,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let picker = cx.new_view(|cx| {
|
let picker = cx.new_view(|cx| {
|
||||||
Picker::uniform_list(
|
Picker::uniform_list(
|
||||||
TasksModalDelegate::new(project, task_context, workspace),
|
TasksModalDelegate::new(task_store, task_context, workspace),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -204,71 +204,46 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
cx: &mut ViewContext<picker::Picker<Self>>,
|
cx: &mut ViewContext<picker::Picker<Self>>,
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
cx.spawn(move |picker, mut cx| async move {
|
cx.spawn(move |picker, mut cx| async move {
|
||||||
let Some(candidates_task) = picker
|
let Some(candidates) = picker
|
||||||
.update(&mut cx, |picker, cx| {
|
.update(&mut cx, |picker, cx| {
|
||||||
match &mut picker.delegate.candidates {
|
match &mut picker.delegate.candidates {
|
||||||
Some(candidates) => {
|
Some(candidates) => string_match_candidates(candidates.iter()),
|
||||||
Task::ready(Ok(string_match_candidates(candidates.iter())))
|
|
||||||
}
|
|
||||||
None => {
|
None => {
|
||||||
let Ok((worktree, location)) =
|
let Ok((worktree, location)) =
|
||||||
picker.delegate.workspace.update(cx, |workspace, cx| {
|
picker.delegate.workspace.update(cx, |workspace, cx| {
|
||||||
active_item_selection_properties(workspace, cx)
|
active_item_selection_properties(workspace, cx)
|
||||||
})
|
})
|
||||||
else {
|
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 =
|
let (used, current) =
|
||||||
picker.delegate.project.update(cx, |project, cx| {
|
task_inventory.read(cx).used_and_current_resolved_tasks(
|
||||||
let ssh_connection_string = project.ssh_connection_string(cx);
|
worktree,
|
||||||
if project.is_via_collab() && ssh_connection_string.is_none() {
|
location,
|
||||||
Task::ready((Vec::new(), Vec::new()))
|
&picker.delegate.task_context,
|
||||||
} else {
|
cx,
|
||||||
let remote_templates = if project.is_local() {
|
);
|
||||||
None
|
picker.delegate.last_used_candidate_index = if used.is_empty() {
|
||||||
} else {
|
None
|
||||||
project
|
} else {
|
||||||
.remote_id()
|
Some(used.len() - 1)
|
||||||
.filter(|_| ssh_connection_string.is_some())
|
};
|
||||||
.map(|project_id| {
|
|
||||||
project.query_remote_task_templates(
|
|
||||||
project_id,
|
|
||||||
worktree,
|
|
||||||
location.as_ref(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
project
|
|
||||||
.task_inventory()
|
|
||||||
.read(cx)
|
|
||||||
.used_and_current_resolved_tasks(
|
|
||||||
remote_templates,
|
|
||||||
worktree,
|
|
||||||
location,
|
|
||||||
&picker.delegate.task_context,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cx.spawn(|picker, mut cx| async move {
|
|
||||||
let (used, current) = resolved_task.await;
|
|
||||||
picker.update(&mut cx, |picker, _| {
|
|
||||||
picker.delegate.last_used_candidate_index = if used.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(used.len() - 1)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut new_candidates = used;
|
let mut new_candidates = used;
|
||||||
new_candidates.extend(current);
|
new_candidates.extend(current);
|
||||||
let match_candidates =
|
let match_candidates = string_match_candidates(new_candidates.iter());
|
||||||
string_match_candidates(new_candidates.iter());
|
let _ = picker.delegate.candidates.insert(new_candidates);
|
||||||
let _ = picker.delegate.candidates.insert(new_candidates);
|
match_candidates
|
||||||
match_candidates
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -276,11 +251,6 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(candidates): Option<Vec<StringMatchCandidate>> =
|
|
||||||
candidates_task.await.log_err()
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let matches = fuzzy::match_strings(
|
let matches = fuzzy::match_strings(
|
||||||
&candidates,
|
&candidates,
|
||||||
&query,
|
&query,
|
||||||
|
@ -492,9 +462,9 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
let is_recent_selected = self.divider_index >= Some(self.selected_index);
|
let is_recent_selected = self.divider_index >= Some(self.selected_index);
|
||||||
let current_modifiers = cx.modifiers();
|
let current_modifiers = cx.modifiers();
|
||||||
let left_button = if self
|
let left_button = if self
|
||||||
.project
|
.task_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.task_inventory()
|
.task_inventory()?
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.last_scheduled_task(None)
|
.last_scheduled_task(None)
|
||||||
.is_some()
|
.is_some()
|
||||||
|
@ -646,6 +616,20 @@ mod tests {
|
||||||
"",
|
"",
|
||||||
"Initial query should be empty"
|
"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!(
|
assert_eq!(
|
||||||
task_names(&tasks_picker, cx),
|
task_names(&tasks_picker, cx),
|
||||||
vec!["another one", "example task"],
|
vec!["another one", "example task"],
|
||||||
|
@ -951,8 +935,9 @@ mod tests {
|
||||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
task_names(&tasks_picker, cx),
|
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"],
|
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"
|
"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| {
|
tasks_picker.update(cx, |_, cx| {
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
|
@ -1035,10 +1020,12 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, _| {
|
if let Some(task_inventory) = project.task_store().read(cx).task_inventory().cloned() {
|
||||||
let (kind, task) = scheduled_task;
|
task_inventory.update(cx, |inventory, _| {
|
||||||
inventory.task_scheduled(kind, task);
|
let (kind, task) = scheduled_task;
|
||||||
})
|
inventory.task_scheduled(kind, task);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
tasks_picker.update(cx, |_, cx| {
|
tasks_picker.update(cx, |_, cx| {
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
|
|
|
@ -36,9 +36,13 @@ pub fn schedule_resolved_task(
|
||||||
if !omit_history {
|
if !omit_history {
|
||||||
resolved_task.resolved = Some(spawn_in_terminal.clone());
|
resolved_task.resolved = Some(spawn_in_terminal.clone());
|
||||||
workspace.project().update(cx, |project, cx| {
|
workspace.project().update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, _| {
|
if let Some(task_inventory) =
|
||||||
inventory.task_scheduled(task_source_kind, resolved_task);
|
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)));
|
cx.emit(crate::Event::SpawnTask(Box::new(spawn_in_terminal)));
|
||||||
|
|
|
@ -27,19 +27,17 @@ use anyhow::Context as _;
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use futures::{channel::mpsc, select_biased, StreamExt};
|
use futures::{channel::mpsc, select_biased, StreamExt};
|
||||||
use outline_panel::OutlinePanel;
|
use outline_panel::OutlinePanel;
|
||||||
use project::TaskSourceKind;
|
|
||||||
use project_panel::ProjectPanel;
|
use project_panel::ProjectPanel;
|
||||||
use quick_action_bar::QuickActionBar;
|
use quick_action_bar::QuickActionBar;
|
||||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use search::project_search::ProjectSearchBar;
|
use search::project_search::ProjectSearchBar;
|
||||||
use settings::{
|
use settings::{
|
||||||
initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings,
|
initial_local_settings_content, initial_tasks_content, KeymapFile, Settings, SettingsStore,
|
||||||
SettingsStore, DEFAULT_KEYMAP_PATH,
|
DEFAULT_KEYMAP_PATH,
|
||||||
};
|
};
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
||||||
use task::static_source::{StaticSource, TrackedFile};
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use workspace::notifications::NotificationId;
|
use workspace::notifications::NotificationId;
|
||||||
use workspace::CloseIntent;
|
use workspace::CloseIntent;
|
||||||
|
@ -229,27 +227,6 @@ pub fn initialize_workspace(
|
||||||
.unwrap_or(true)
|
.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();
|
let prompt_builder = prompt_builder.clone();
|
||||||
cx.spawn(|workspace_handle, mut cx| async move {
|
cx.spawn(|workspace_handle, mut cx| async move {
|
||||||
let assistant_panel =
|
let assistant_panel =
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue