Rework remote task synchronization (#18746)

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

Release Notes:

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

View file

@ -18,10 +18,14 @@ pub fn init(cx: &mut AppContext) {
workspace
.register_action(spawn_task_or_modal)
.register_action(move |workspace, action: &modal::Rerun, cx| {
if let Some((task_source_kind, mut last_scheduled_task)) =
workspace.project().update(cx, |project, cx| {
project
.task_inventory()
if let Some((task_source_kind, mut last_scheduled_task)) = workspace
.project()
.read(cx)
.task_store()
.read(cx)
.task_inventory()
.and_then(|inventory| {
inventory
.read(cx)
.last_scheduled_task(action.task_id.as_ref())
})
@ -86,23 +90,26 @@ fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewC
}
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
let project = workspace.project().clone();
let task_store = workspace.project().read(cx).task_store().clone();
let workspace_handle = workspace.weak_handle();
let context_task = task_context(workspace, cx);
cx.spawn(|workspace, mut cx| async move {
let task_context = context_task.await;
workspace
.update(&mut cx, |workspace, cx| {
if workspace.project().update(cx, |project, cx| {
project.is_local() || project.ssh_connection_string(cx).is_some()
}) {
let can_open_modal = workspace.project().update(cx, |project, cx| {
project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh()
});
if can_open_modal {
let context_task = task_context(workspace, cx);
cx.spawn(|workspace, mut cx| async move {
let task_context = context_task.await;
workspace
.update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| {
TasksModal::new(project, task_context, workspace_handle, cx)
TasksModal::new(task_store.clone(), task_context, workspace_handle, cx)
})
}
})
.ok();
})
})
.ok();
})
} else {
AsyncTask::ready(())
}
}
fn spawn_task_with_name(
@ -113,14 +120,31 @@ fn spawn_task_with_name(
let context_task =
workspace.update(&mut cx, |workspace, cx| task_context(workspace, cx))?;
let task_context = context_task.await;
let tasks = workspace
.update(&mut cx, |workspace, cx| {
let (worktree, location) = active_item_selection_properties(workspace, cx);
workspace.project().update(cx, |project, cx| {
project.task_templates(worktree, location, cx)
let tasks = workspace.update(&mut cx, |workspace, cx| {
let Some(task_inventory) = workspace
.project()
.read(cx)
.task_store()
.read(cx)
.task_inventory()
.cloned()
else {
return Vec::new();
};
let (worktree, location) = active_item_selection_properties(workspace, cx);
let (file, language) = location
.map(|location| {
let buffer = location.buffer.read(cx);
(
buffer.file().cloned(),
buffer.language_at(location.range.start),
)
})
})?
.await?;
.unwrap_or_default();
task_inventory
.read(cx)
.list_tasks(file, language, worktree, cx)
})?;
let did_spawn = workspace
.update(&mut cx, |workspace, cx| {
@ -185,7 +209,7 @@ mod tests {
use editor::Editor;
use gpui::{Entity, TestAppContext};
use language::{Language, LanguageConfig};
use project::{BasicContextProvider, FakeFs, Project};
use project::{task_store::TaskStore, BasicContextProvider, FakeFs, Project};
use serde_json::json;
use task::{TaskContext, TaskVariables, VariableName};
use ui::VisualContext;
@ -223,6 +247,7 @@ mod tests {
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let worktree_store = project.update(cx, |project, _| project.worktree_store().clone());
let rust_language = Arc::new(
Language::new(
LanguageConfig::default(),
@ -234,7 +259,9 @@ mod tests {
name: (_) @name) @item"#,
)
.unwrap()
.with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))),
.with_context_provider(Some(Arc::new(BasicContextProvider::new(
worktree_store.clone(),
)))),
);
let typescript_language = Arc::new(
@ -252,7 +279,9 @@ mod tests {
")" @context)) @item"#,
)
.unwrap()
.with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))),
.with_context_provider(Some(Arc::new(BasicContextProvider::new(
worktree_store.clone(),
)))),
);
let worktree_id = project.update(cx, |project, cx| {
@ -373,6 +402,7 @@ mod tests {
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
TaskStore::init(None);
state
})
}

View file

@ -8,7 +8,7 @@ use gpui::{
View, ViewContext, VisualContext, WeakView,
};
use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
use project::{Project, TaskSourceKind};
use project::{task_store::TaskStore, TaskSourceKind};
use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate};
use ui::{
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
@ -63,7 +63,7 @@ impl_actions!(task, [Rerun, Spawn]);
/// A modal used to spawn new tasks.
pub(crate) struct TasksModalDelegate {
project: Model<Project>,
task_store: Model<TaskStore>,
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
last_used_candidate_index: Option<usize>,
divider_index: Option<usize>,
@ -77,12 +77,12 @@ pub(crate) struct TasksModalDelegate {
impl TasksModalDelegate {
fn new(
project: Model<Project>,
task_store: Model<TaskStore>,
task_context: TaskContext,
workspace: WeakView<Workspace>,
) -> Self {
Self {
project,
task_store,
workspace,
candidates: None,
matches: Vec::new(),
@ -124,11 +124,11 @@ impl TasksModalDelegate {
// it doesn't make sense to requery the inventory for new candidates, as that's potentially costly and more often than not it should just return back
// the original list without a removed entry.
candidates.remove(ix);
self.project.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, _| {
if let Some(inventory) = self.task_store.read(cx).task_inventory().cloned() {
inventory.update(cx, |inventory, _| {
inventory.delete_previously_used(&task.id);
})
});
};
}
}
@ -139,14 +139,14 @@ pub(crate) struct TasksModal {
impl TasksModal {
pub(crate) fn new(
project: Model<Project>,
task_store: Model<TaskStore>,
task_context: TaskContext,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let picker = cx.new_view(|cx| {
Picker::uniform_list(
TasksModalDelegate::new(project, task_context, workspace),
TasksModalDelegate::new(task_store, task_context, workspace),
cx,
)
});
@ -204,71 +204,46 @@ impl PickerDelegate for TasksModalDelegate {
cx: &mut ViewContext<picker::Picker<Self>>,
) -> Task<()> {
cx.spawn(move |picker, mut cx| async move {
let Some(candidates_task) = picker
let Some(candidates) = picker
.update(&mut cx, |picker, cx| {
match &mut picker.delegate.candidates {
Some(candidates) => {
Task::ready(Ok(string_match_candidates(candidates.iter())))
}
Some(candidates) => string_match_candidates(candidates.iter()),
None => {
let Ok((worktree, location)) =
picker.delegate.workspace.update(cx, |workspace, cx| {
active_item_selection_properties(workspace, cx)
})
else {
return Task::ready(Ok(Vec::new()));
return Vec::new();
};
let Some(task_inventory) = picker
.delegate
.task_store
.read(cx)
.task_inventory()
.cloned()
else {
return Vec::new();
};
let resolved_task =
picker.delegate.project.update(cx, |project, cx| {
let ssh_connection_string = project.ssh_connection_string(cx);
if project.is_via_collab() && ssh_connection_string.is_none() {
Task::ready((Vec::new(), Vec::new()))
} else {
let remote_templates = if project.is_local() {
None
} else {
project
.remote_id()
.filter(|_| ssh_connection_string.is_some())
.map(|project_id| {
project.query_remote_task_templates(
project_id,
worktree,
location.as_ref(),
cx,
)
})
};
project
.task_inventory()
.read(cx)
.used_and_current_resolved_tasks(
remote_templates,
worktree,
location,
&picker.delegate.task_context,
cx,
)
}
});
cx.spawn(|picker, mut cx| async move {
let (used, current) = resolved_task.await;
picker.update(&mut cx, |picker, _| {
picker.delegate.last_used_candidate_index = if used.is_empty() {
None
} else {
Some(used.len() - 1)
};
let (used, current) =
task_inventory.read(cx).used_and_current_resolved_tasks(
worktree,
location,
&picker.delegate.task_context,
cx,
);
picker.delegate.last_used_candidate_index = if used.is_empty() {
None
} else {
Some(used.len() - 1)
};
let mut new_candidates = used;
new_candidates.extend(current);
let match_candidates =
string_match_candidates(new_candidates.iter());
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
})
})
let mut new_candidates = used;
new_candidates.extend(current);
let match_candidates = string_match_candidates(new_candidates.iter());
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
}
}
})
@ -276,11 +251,6 @@ impl PickerDelegate for TasksModalDelegate {
else {
return;
};
let Some(candidates): Option<Vec<StringMatchCandidate>> =
candidates_task.await.log_err()
else {
return;
};
let matches = fuzzy::match_strings(
&candidates,
&query,
@ -492,9 +462,9 @@ impl PickerDelegate for TasksModalDelegate {
let is_recent_selected = self.divider_index >= Some(self.selected_index);
let current_modifiers = cx.modifiers();
let left_button = if self
.project
.task_store
.read(cx)
.task_inventory()
.task_inventory()?
.read(cx)
.last_scheduled_task(None)
.is_some()
@ -646,6 +616,20 @@ mod tests {
"",
"Initial query should be empty"
);
assert_eq!(
task_names(&tasks_picker, cx),
Vec::<String>::new(),
"With no global tasks and no open item, no tasks should be listed"
);
drop(tasks_picker);
let _ = workspace
.update(cx, |workspace, cx| {
workspace.open_abs_path(PathBuf::from("/dir/a.ts"), true, cx)
})
.await
.unwrap();
let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!(
task_names(&tasks_picker, cx),
vec!["another one", "example task"],
@ -951,8 +935,9 @@ mod tests {
let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!(
task_names(&tasks_picker, cx),
vec!["TypeScript task from file /dir/a1.ts", "TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"],
"After spawning the task and getting it into the history, it should be up in the sort as recently used"
vec!["TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"],
"After spawning the task and getting it into the history, it should be up in the sort as recently used.
Tasks with the same labels and context are deduplicated."
);
tasks_picker.update(cx, |_, cx| {
cx.emit(DismissEvent);
@ -1035,10 +1020,12 @@ mod tests {
.unwrap()
});
project.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, _| {
let (kind, task) = scheduled_task;
inventory.task_scheduled(kind, task);
})
if let Some(task_inventory) = project.task_store().read(cx).task_inventory().cloned() {
task_inventory.update(cx, |inventory, _| {
let (kind, task) = scheduled_task;
inventory.task_scheduled(kind, task);
});
}
});
tasks_picker.update(cx, |_, cx| {
cx.emit(DismissEvent);