Allow clients to run Zed tasks on remote projects (#12199)

Release Notes:

- Enabled Zed tasks on remote projects with ssh connection string
specified

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
This commit is contained in:
Kirill Bulatov 2024-05-24 22:26:57 +03:00 committed by GitHub
parent df35fd0026
commit 055a13a9b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1250 additions and 600 deletions

1
Cargo.lock generated
View File

@ -10161,6 +10161,7 @@ dependencies = [
name = "tasks_ui" name = "tasks_ui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"editor", "editor",
"file_icons", "file_icons",
"fuzzy", "fuzzy",

View File

@ -1101,6 +1101,36 @@ impl Database {
.map(|guard| guard.into_inner()) .map(|guard| guard.into_inner())
} }
/// Returns the host connection for a request to join a shared project.
pub async fn host_for_owner_project_request(
&self,
project_id: ProjectId,
_connection_id: ConnectionId,
user_id: UserId,
) -> Result<ConnectionId> {
self.project_transaction(project_id, |tx| async move {
let (project, dev_server_project) = project::Entity::find_by_id(project_id)
.find_also_related(dev_server_project::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let Some(dev_server_project) = dev_server_project else {
return Err(anyhow!("not a dev server project"))?;
};
let dev_server = dev_server::Entity::find_by_id(dev_server_project.dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such dev server"))?;
if dev_server.user_id != user_id {
return Err(anyhow!("not your project"))?;
}
project.host_connection()
})
.await
.map(|guard| guard.into_inner())
}
pub async fn connections_for_buffer_update( pub async fn connections_for_buffer_update(
&self, &self,
project_id: ProjectId, project_id: ProjectId,

View File

@ -446,6 +446,12 @@ impl Server {
.add_message_handler(update_language_server) .add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary) .add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings) .add_message_handler(update_worktree_settings)
.add_request_handler(user_handler(
forward_project_request_for_owner::<proto::TaskContextForLocation>,
))
.add_request_handler(user_handler(
forward_project_request_for_owner::<proto::TaskTemplates>,
))
.add_request_handler(user_handler( .add_request_handler(user_handler(
forward_read_only_project_request::<proto::GetHover>, forward_read_only_project_request::<proto::GetHover>,
)) ))
@ -2889,6 +2895,31 @@ where
Ok(()) Ok(())
} }
/// forward a project request to the dev server. Only allowed
/// if it's your dev server.
async fn forward_project_request_for_owner<T>(
request: T,
response: Response<T>,
session: UserSession,
) -> Result<()>
where
T: EntityMessage + RequestMessage,
{
let project_id = ProjectId::from_proto(request.remote_entity_id());
let host_connection_id = session
.db()
.await
.host_for_owner_project_request(project_id, session.connection_id, session.user_id())
.await?;
let payload = session
.peer
.forward_request(session.connection_id, host_connection_id, request)
.await?;
response.send(payload)?;
Ok(())
}
/// forward a project request to the host. These requests are disallowed /// forward a project request to the host. These requests are disallowed
/// for guests. /// for guests.
async fn forward_mutating_project_request<T>( async fn forward_mutating_project_request<T>(

View File

@ -4011,28 +4011,29 @@ impl Editor {
let deployed_from_indicator = action.deployed_from_indicator; let deployed_from_indicator = action.deployed_from_indicator;
let mut task = self.code_actions_task.take(); let mut task = self.code_actions_task.take();
let action = action.clone(); let action = action.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|editor, mut cx| async move {
while let Some(prev_task) = task { while let Some(prev_task) = task {
prev_task.await; prev_task.await;
task = this.update(&mut cx, |this, _| this.code_actions_task.take())?; task = editor.update(&mut cx, |this, _| this.code_actions_task.take())?;
} }
let spawned_test_task = this.update(&mut cx, |this, cx| { let spawned_test_task = editor.update(&mut cx, |editor, cx| {
if this.focus_handle.is_focused(cx) { if editor.focus_handle.is_focused(cx) {
let multibuffer_point = action let multibuffer_point = action
.deployed_from_indicator .deployed_from_indicator
.map(|row| DisplayPoint::new(row, 0).to_point(&snapshot)) .map(|row| DisplayPoint::new(row, 0).to_point(&snapshot))
.unwrap_or_else(|| this.selections.newest::<Point>(cx).head()); .unwrap_or_else(|| editor.selections.newest::<Point>(cx).head());
let (buffer, buffer_row) = snapshot let (buffer, buffer_row) = snapshot
.buffer_snapshot .buffer_snapshot
.buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) .buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
.and_then(|(buffer_snapshot, range)| { .and_then(|(buffer_snapshot, range)| {
this.buffer editor
.buffer
.read(cx) .read(cx)
.buffer(buffer_snapshot.remote_id()) .buffer(buffer_snapshot.remote_id())
.map(|buffer| (buffer, range.start.row)) .map(|buffer| (buffer, range.start.row))
})?; })?;
let (_, code_actions) = this let (_, code_actions) = editor
.available_code_actions .available_code_actions
.clone() .clone()
.and_then(|(location, code_actions)| { .and_then(|(location, code_actions)| {
@ -4047,7 +4048,7 @@ impl Editor {
}) })
.unzip(); .unzip();
let buffer_id = buffer.read(cx).remote_id(); let buffer_id = buffer.read(cx).remote_id();
let tasks = this let tasks = editor
.tasks .tasks
.get(&(buffer_id, buffer_row)) .get(&(buffer_id, buffer_row))
.map(|t| Arc::new(t.to_owned())); .map(|t| Arc::new(t.to_owned()));
@ -4055,10 +4056,13 @@ impl Editor {
return None; return None;
} }
this.completion_tasks.clear(); editor.completion_tasks.clear();
this.discard_inline_completion(false, cx); editor.discard_inline_completion(false, cx);
let tasks = tasks.as_ref().zip(this.workspace.clone()).and_then( let task_context =
|(tasks, (workspace, _))| { tasks
.as_ref()
.zip(editor.project.clone())
.map(|(tasks, project)| {
let position = Point::new(buffer_row, tasks.column); let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right); let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location { let location = Location {
@ -4073,19 +4077,22 @@ impl Editor {
value.clone(), value.clone(),
); );
} }
project.update(cx, |project, cx| {
workspace project.task_context_for_location(
.update(cx, |workspace, cx| {
tasks::task_context_for_location(
captured_task_variables, captured_task_variables,
workspace,
location, location,
cx, cx,
) )
}) })
.ok() });
.flatten()
.map(|task_context| { Some(cx.spawn(|editor, mut cx| async move {
let task_context = match task_context {
Some(task_context) => task_context.await,
None => None,
};
let resolved_tasks =
tasks.zip(task_context).map(|(tasks, task_context)| {
Arc::new(ResolvedTasks { Arc::new(ResolvedTasks {
templates: tasks templates: tasks
.templates .templates
@ -4096,23 +4103,25 @@ impl Editor {
.map(|task| (kind.clone(), task)) .map(|task| (kind.clone(), task))
}) })
.collect(), .collect(),
position: snapshot.buffer_snapshot.anchor_before( position: snapshot.buffer_snapshot.anchor_before(Point::new(
Point::new(multibuffer_point.row, tasks.column), multibuffer_point.row,
), tasks.column,
)),
}) })
}) });
}, let spawn_straight_away = resolved_tasks
);
let spawn_straight_away = tasks
.as_ref() .as_ref()
.map_or(false, |tasks| tasks.templates.len() == 1) .map_or(false, |tasks| tasks.templates.len() == 1)
&& code_actions && code_actions
.as_ref() .as_ref()
.map_or(true, |actions| actions.is_empty()); .map_or(true, |actions| actions.is_empty());
*this.context_menu.write() = Some(ContextMenu::CodeActions(CodeActionsMenu { if let Some(task) = editor
.update(&mut cx, |editor, cx| {
*editor.context_menu.write() =
Some(ContextMenu::CodeActions(CodeActionsMenu {
buffer, buffer,
actions: CodeActionContents { actions: CodeActionContents {
tasks, tasks: resolved_tasks,
actions: code_actions, actions: code_actions,
}, },
selected_item: Default::default(), selected_item: Default::default(),
@ -4120,16 +4129,27 @@ impl Editor {
deployed_from_indicator, deployed_from_indicator,
})); }));
if spawn_straight_away { if spawn_straight_away {
if let Some(task) = if let Some(task) = editor.confirm_code_action(
this.confirm_code_action(&ConfirmCodeAction { item_ix: Some(0) }, cx) &ConfirmCodeAction { item_ix: Some(0) },
cx,
) {
cx.notify();
return task;
}
}
cx.notify();
Task::ready(Ok(()))
})
.ok()
{ {
cx.notify(); task.await
return Some(task); } else {
} Ok(())
}
cx.notify();
} }
}))
} else {
Some(Task::ready(Ok(()))) Some(Task::ready(Ok(())))
}
})?; })?;
if let Some(task) = spawned_test_task { if let Some(task) = spawned_test_task {
task.await?; task.await?;
@ -7897,11 +7917,14 @@ impl Editor {
let Some(project) = project else { let Some(project) = project else {
return; return;
}; };
if project
.update(&mut cx, |this, _| this.is_remote()) let hide_runnables = project
.unwrap_or(true) .update(&mut cx, |project, cx| {
{ // Do not display any test indicators in non-dev server remote projects.
// Do not display any test indicators in remote projects. project.is_remote() && project.ssh_connection_string(cx).is_none()
})
.unwrap_or(true);
if hide_runnables {
return; return;
} }
let new_rows = let new_rows =
@ -7940,10 +7963,8 @@ impl Editor {
runnable_ranges runnable_ranges
.into_iter() .into_iter()
.filter_map(|mut runnable| { .filter_map(|mut runnable| {
let (tasks, _) = cx let tasks = cx
.update(|cx| { .update(|cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
Self::resolve_runnable(project.clone(), &mut runnable.runnable, cx)
})
.ok()?; .ok()?;
if tasks.is_empty() { if tasks.is_empty() {
return None; return None;
@ -7974,11 +7995,11 @@ impl Editor {
.collect() .collect()
} }
fn resolve_runnable( fn templates_with_tags(
project: Model<Project>, project: &Model<Project>,
runnable: &mut Runnable, runnable: &mut Runnable,
cx: &WindowContext<'_>, cx: &WindowContext<'_>,
) -> (Vec<(TaskSourceKind, TaskTemplate)>, Option<WorktreeId>) { ) -> Vec<(TaskSourceKind, TaskTemplate)> {
let (inventory, worktree_id) = project.read_with(cx, |project, cx| { let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
let worktree_id = project let worktree_id = project
.buffer_for_id(runnable.buffer) .buffer_for_id(runnable.buffer)
@ -8015,7 +8036,7 @@ impl Editor {
} }
} }
(tags, worktree_id) tags
} }
pub fn move_to_enclosing_bracket( pub fn move_to_enclosing_bracket(

View File

@ -1,58 +1,34 @@
use crate::Editor; use crate::Editor;
use anyhow::Context; use gpui::{Task as AsyncTask, WindowContext};
use gpui::{Model, WindowContext}; use project::Location;
use language::ContextProvider;
use project::{BasicContextProvider, Location, Project};
use task::{TaskContext, TaskVariables, VariableName}; use task::{TaskContext, TaskVariables, VariableName};
use text::{Point, ToOffset, ToPoint}; use text::{Point, ToOffset, ToPoint};
use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
pub(crate) fn task_context_for_location(
captured_variables: TaskVariables,
workspace: &Workspace,
location: Location,
cx: &mut WindowContext<'_>,
) -> Option<TaskContext> {
let cwd = workspace::tasks::task_cwd(workspace, cx)
.log_err()
.flatten();
let mut task_variables = combine_task_variables(
captured_variables,
location,
workspace.project().clone(),
cx,
)
.log_err()?;
// Remove all custom entries starting with _, as they're not intended for use by the end user.
task_variables.sweep();
Some(TaskContext {
cwd,
task_variables,
})
}
fn task_context_with_editor( fn task_context_with_editor(
workspace: &Workspace,
editor: &mut Editor, editor: &mut Editor,
cx: &mut WindowContext<'_>, cx: &mut WindowContext<'_>,
) -> Option<TaskContext> { ) -> AsyncTask<Option<TaskContext>> {
let Some(project) = editor.project.clone() else {
return AsyncTask::ready(None);
};
let (selection, buffer, editor_snapshot) = { let (selection, buffer, editor_snapshot) = {
let mut selection = editor.selections.newest::<Point>(cx); let mut selection = editor.selections.newest::<Point>(cx);
if editor.selections.line_mode { if editor.selections.line_mode {
selection.start = Point::new(selection.start.row, 0); selection.start = Point::new(selection.start.row, 0);
selection.end = Point::new(selection.end.row + 1, 0); selection.end = Point::new(selection.end.row + 1, 0);
} }
let (buffer, _, _) = editor let Some((buffer, _, _)) = editor
.buffer() .buffer()
.read(cx) .read(cx)
.point_to_buffer_offset(selection.start, cx)?; .point_to_buffer_offset(selection.start, cx)
else {
return AsyncTask::ready(None);
};
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
Some((selection, buffer, snapshot)) (selection, buffer, snapshot)
}?; };
let selection_range = selection.range(); let selection_range = selection.range();
let start = editor_snapshot let start = editor_snapshot
.display_snapshot .display_snapshot
@ -94,42 +70,23 @@ fn task_context_with_editor(
} }
variables variables
}; };
task_context_for_location(captured_variables, workspace, location.clone(), cx)
let context_task = project.update(cx, |project, cx| {
project.task_context_for_location(captured_variables, location.clone(), cx)
});
cx.spawn(|_| context_task)
} }
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext { pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> AsyncTask<TaskContext> {
let Some(editor) = workspace let Some(editor) = workspace
.active_item(cx) .active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx)) .and_then(|item| item.act_as::<Editor>(cx))
else { else {
return Default::default(); return AsyncTask::ready(TaskContext::default());
}; };
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
task_context_with_editor(workspace, editor, cx).unwrap_or_default() let context_task = task_context_with_editor(editor, cx);
cx.background_executor()
.spawn(async move { context_task.await.unwrap_or_default() })
}) })
} }
fn combine_task_variables(
mut captured_variables: TaskVariables,
location: Location,
project: Model<Project>,
cx: &mut WindowContext<'_>,
) -> anyhow::Result<TaskVariables> {
let language_context_provider = location
.buffer
.read(cx)
.language()
.and_then(|language| language.context_provider());
let baseline = BasicContextProvider::new(project)
.build_context(&captured_variables, &location, 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, cx)
.context("building provider context ")?,
);
}
Ok(captured_variables)
}

View File

@ -36,7 +36,7 @@ use git::{blame::Blame, repository::GitRepository};
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{ use gpui::{
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity, AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
EventEmitter, Model, ModelContext, PromptLevel, Task, WeakModel, EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
@ -47,10 +47,10 @@ use language::{
serialize_version, split_operations, serialize_version, split_operations,
}, },
range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel, range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel,
Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, Event as BufferEvent, ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, LspAdapterDelegate, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
ToPointUtf16, Transaction, Unclipped, ToOffset, ToPointUtf16, Transaction, Unclipped,
}; };
use log::error; use log::error;
use lsp::{ use lsp::{
@ -80,6 +80,7 @@ use similar::{ChangeTag, TextDiff};
use smol::channel::{Receiver, Sender}; use smol::channel::{Receiver, Sender};
use smol::lock::Semaphore; use smol::lock::Semaphore;
use std::{ use std::{
borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},
convert::TryInto, convert::TryInto,
env, env,
@ -97,7 +98,10 @@ use std::{
}, },
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use task::static_source::{StaticSource, TrackedFile}; use task::{
static_source::{StaticSource, TrackedFile},
RevealStrategy, TaskContext, TaskTemplate, TaskVariables, VariableName,
};
use terminals::Terminals; use terminals::Terminals;
use text::{Anchor, BufferId, LineEnding}; use text::{Anchor, BufferId, LineEnding};
use util::{ use util::{
@ -676,6 +680,8 @@ impl Project {
client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>); client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
client.add_model_request_handler(Self::handle_blame_buffer); client.add_model_request_handler(Self::handle_blame_buffer);
client.add_model_request_handler(Self::handle_multi_lsp_query); client.add_model_request_handler(Self::handle_multi_lsp_query);
client.add_model_request_handler(Self::handle_task_context_for_location);
client.add_model_request_handler(Self::handle_task_templates);
} }
pub fn local( pub fn local(
@ -1257,6 +1263,19 @@ impl Project {
self.dev_server_project_id self.dev_server_project_id
} }
pub fn ssh_connection_string(&self, cx: &ModelContext<Self>) -> Option<SharedString> {
if self.is_local() {
return None;
}
let dev_server_id = self.dev_server_project_id()?;
dev_server_projects::Store::global(cx)
.read(cx)
.dev_server_for_project(dev_server_id)?
.ssh_connection_string
.clone()
}
pub fn replica_id(&self) -> ReplicaId { pub fn replica_id(&self) -> ReplicaId {
match self.client_state { match self.client_state {
ProjectClientState::Remote { replica_id, .. } => replica_id, ProjectClientState::Remote { replica_id, .. } => replica_id,
@ -7892,7 +7911,7 @@ impl Project {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: remote_worktree_id, id: remote_worktree_id,
abs_path, abs_path,
id_base: "local_tasks_for_worktree", id_base: "local_tasks_for_worktree".into(),
}, },
|tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)), |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),
cx, cx,
@ -7912,7 +7931,7 @@ impl Project {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: remote_worktree_id, id: remote_worktree_id,
abs_path, abs_path,
id_base: "local_vscode_tasks_for_worktree", id_base: "local_vscode_tasks_for_worktree".into(),
}, },
|tx, cx| { |tx, cx| {
StaticSource::new(TrackedFile::new_convertible::< StaticSource::new(TrackedFile::new_convertible::<
@ -9424,6 +9443,122 @@ impl Project {
}) })
} }
async fn handle_task_context_for_location(
project: Model<Self>,
envelope: TypedEnvelope<proto::TaskContextForLocation>,
_: Arc<Client>,
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 {
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>,
_: Arc<Client>,
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::Always as i32,
RevealStrategy::Never => proto::RevealStrategy::Never as i32,
},
tags: template.tags,
});
proto::TemplatePair { kind, template }
})
.collect();
Ok(proto::TaskTemplatesResponse { templates })
}
async fn try_resolve_code_action( async fn try_resolve_code_action(
lang_server: &LanguageServer, lang_server: &LanguageServer,
action: &mut CodeAction, action: &mut CodeAction,
@ -10410,6 +10545,223 @@ impl Project {
Vec::new() Vec::new()
} }
} }
pub fn task_context_for_location(
&self,
captured_variables: TaskVariables,
location: Location,
cx: &mut ModelContext<'_, Project>,
) -> Task<Option<TaskContext>> {
if self.is_local() {
let cwd = self.task_cwd(cx).log_err().flatten();
cx.spawn(|project, cx| async move {
let mut task_variables = cx
.update(|cx| {
combine_task_variables(
captured_variables,
location,
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 {
cwd,
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 {
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 language = location
.and_then(|location| location.buffer.read(cx).language_at(location.range.start));
Task::ready(Ok(self
.task_inventory()
.read(cx)
.list_tasks(language, worktree)))
} 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::Always)
{
proto::RevealStrategy::Always => RevealStrategy::Always,
proto::RevealStrategy::Never => RevealStrategy::Never,
};
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,
tags: proto_template.tags,
};
Some((task_source_kind, task_template))
})
.collect())
})
}
fn task_cwd(&self, cx: &AppContext) -> anyhow::Result<Option<PathBuf>> {
let available_worktrees = self
.worktrees()
.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<_>>();
let cwd = match available_worktrees.len() {
0 => None,
1 => Some(available_worktrees[0].read(cx).abs_path()),
_ => {
let cwd_for_active_entry = self.active_entry().and_then(|entry_id| {
available_worktrees.into_iter().find_map(|worktree| {
let worktree = worktree.read(cx);
if worktree.contains_entry(entry_id) {
Some(worktree.abs_path())
} else {
None
}
})
});
anyhow::ensure!(
cwd_for_active_entry.is_some(),
"Cannot determine task cwd for multiple worktrees"
);
cwd_for_active_entry
}
};
Ok(cwd.map(|path| path.to_path_buf()))
}
}
fn combine_task_variables(
mut captured_variables: TaskVariables,
location: Location,
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, 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, cx)
.context("building provider context")?,
);
}
Ok(captured_variables)
} }
async fn populate_labels_for_symbols( async fn populate_labels_for_symbols(
@ -11238,3 +11590,40 @@ 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,
})
})
}

View File

@ -14,7 +14,7 @@ use serde_json::json;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::os; use std::os;
use std::task::Poll; use std::task::Poll;
use task::{TaskContext, TaskTemplate, TaskTemplates}; use task::{ResolvedTask, TaskContext, TaskTemplate, TaskTemplates};
use unindent::Unindent as _; use unindent::Unindent as _;
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree}; use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
use worktree::WorktreeModelHandle as _; use worktree::WorktreeModelHandle as _;
@ -129,17 +129,19 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
let task_context = TaskContext::default(); let task_context = TaskContext::default();
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let workree_id = cx.update(|cx| { let worktree_id = cx.update(|cx| {
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id() project.worktrees().next().unwrap().read(cx).id()
}) })
}); });
let global_task_source_kind = TaskSourceKind::Worktree { let global_task_source_kind = TaskSourceKind::Worktree {
id: workree_id, id: worktree_id,
abs_path: PathBuf::from("/the-root/.zed/tasks.json"), abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
id_base: "local_tasks_for_worktree", id_base: "local_tasks_for_worktree".into(),
}; };
cx.update(|cx| {
let all_tasks = cx
.update(|cx| {
let tree = worktree.read(cx); let tree = worktree.read(cx);
let settings_a = language_settings( let settings_a = language_settings(
@ -166,18 +168,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
assert_eq!(settings_a.tab_size.get(), 8); assert_eq!(settings_a.tab_size.get(), 8);
assert_eq!(settings_b.tab_size.get(), 2); assert_eq!(settings_b.tab_size.get(), 2);
let all_tasks = project get_all_tasks(&project, Some(worktree_id), &task_context, cx)
.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, _| {
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
);
old.extend(new);
old
})
}) })
.await
.into_iter() .into_iter()
.map(|(source_kind, task)| { .map(|(source_kind, task)| {
let resolved = task.resolved.unwrap(); let resolved = task.resolved.unwrap();
@ -200,9 +193,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
), ),
( (
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: workree_id, id: worktree_id,
abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
id_base: "local_tasks_for_worktree", id_base: "local_tasks_for_worktree".into(),
}, },
"cargo check".to_string(), "cargo check".to_string(),
vec!["check".to_string()], vec!["check".to_string()],
@ -210,20 +203,17 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
), ),
] ]
); );
});
project.update(cx, |project, cx| { let (_, resolved_task) = cx
let inventory = project.task_inventory(); .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
inventory.update(cx, |inventory, _| { .await
let (mut old, new) =
inventory.used_and_current_resolved_tasks(None, Some(workree_id), &task_context);
old.extend(new);
let (_, resolved_task) = old
.into_iter() .into_iter()
.find(|(source_kind, _)| source_kind == &global_task_source_kind) .find(|(source_kind, _)| source_kind == &global_task_source_kind)
.expect("should have one global task"); .expect("should have one global task");
project.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, _| {
inventory.task_scheduled(global_task_source_kind.clone(), resolved_task); inventory.task_scheduled(global_task_source_kind.clone(), resolved_task);
}) });
}); });
let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate { let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate {
@ -257,19 +247,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
tx.unbounded_send(tasks).unwrap(); tx.unbounded_send(tasks).unwrap();
cx.run_until_parked(); cx.run_until_parked();
cx.update(|cx| { let all_tasks = cx
let all_tasks = project .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
.update(cx, |project, cx| { .await
project.task_inventory().update(cx, |inventory, _| {
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
);
old.extend(new);
old
})
})
.into_iter() .into_iter()
.map(|(source_kind, task)| { .map(|(source_kind, task)| {
let resolved = task.resolved.unwrap(); let resolved = task.resolved.unwrap();
@ -286,9 +266,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
vec![ vec![
( (
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: workree_id, id: worktree_id,
abs_path: PathBuf::from("/the-root/.zed/tasks.json"), abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
id_base: "local_tasks_for_worktree", id_base: "local_tasks_for_worktree".into(),
}, },
"cargo check".to_string(), "cargo check".to_string(),
vec![ vec![
@ -303,9 +283,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
), ),
( (
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: workree_id, id: worktree_id,
abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
id_base: "local_tasks_for_worktree", id_base: "local_tasks_for_worktree".into(),
}, },
"cargo check".to_string(), "cargo check".to_string(),
vec!["check".to_string()], vec!["check".to_string()],
@ -313,7 +293,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
), ),
] ]
); );
});
} }
#[gpui::test] #[gpui::test]
@ -5225,3 +5204,23 @@ fn tsx_lang() -> Arc<Language> {
Some(tree_sitter_typescript::language_tsx()), Some(tree_sitter_typescript::language_tsx()),
)) ))
} }
fn get_all_tasks(
project: &Model<Project>,
worktree_id: Option<WorktreeId>,
task_context: &TaskContext,
cx: &mut AppContext,
) -> Task<Vec<(TaskSourceKind, ResolvedTask)>> {
let resolved_tasks = project.update(cx, |project, cx| {
project
.task_inventory()
.read(cx)
.used_and_current_resolved_tasks(None, worktree_id, None, task_context, cx)
});
cx.spawn(|_| async move {
let (mut old, new) = resolved_tasks.await;
old.extend(new);
old
})
}

View File

@ -1,6 +1,7 @@
//! Project-wide storage of the tasks available, capable of updating itself from the sources set. //! Project-wide storage of the tasks available, capable of updating itself from the sources set.
use std::{ use std::{
borrow::Cow,
cmp::{self, Reverse}, cmp::{self, Reverse},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@ -20,7 +21,7 @@ use task::{
TaskVariables, VariableName, TaskVariables, VariableName,
}; };
use text::{Point, ToPoint}; use text::{Point, ToPoint};
use util::{post_inc, NumericPrefixWithSuffix}; use util::{post_inc, NumericPrefixWithSuffix, ResultExt};
use worktree::WorktreeId; use worktree::WorktreeId;
use crate::Project; use crate::Project;
@ -47,11 +48,11 @@ pub enum TaskSourceKind {
Worktree { Worktree {
id: WorktreeId, id: WorktreeId,
abs_path: PathBuf, abs_path: PathBuf,
id_base: &'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
AbsPath { AbsPath {
id_base: &'static str, id_base: Cow<'static, str>,
abs_path: PathBuf, abs_path: PathBuf,
}, },
/// Languages-specific tasks coming from extensions. /// Languages-specific tasks coming from extensions.
@ -191,13 +192,18 @@ impl Inventory {
/// 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 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,
language: Option<Arc<Language>>, remote_templates_task: Option<Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>>>,
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
location: Option<Location>,
task_context: &TaskContext, task_context: &TaskContext,
) -> ( cx: &AppContext,
) -> Task<(
Vec<(TaskSourceKind, ResolvedTask)>, Vec<(TaskSourceKind, ResolvedTask)>,
Vec<(TaskSourceKind, ResolvedTask)>, Vec<(TaskSourceKind, ResolvedTask)>,
) { )> {
let language = location
.as_ref()
.and_then(|location| location.buffer.read(cx).language_at(location.range.start));
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
name: language.name(), name: language.name(),
}); });
@ -229,7 +235,7 @@ impl Inventory {
}, },
); );
let not_used_score = post_inc(&mut lru_score); let not_used_score = post_inc(&mut lru_score);
let currently_resolved_tasks = self let mut currently_resolved_tasks = self
.sources .sources
.iter() .iter()
.filter(|source| { .filter(|source| {
@ -244,7 +250,7 @@ impl Inventory {
.into_iter() .into_iter()
.map(|task| (&source.kind, task)) .map(|task| (&source.kind, task))
}) })
.chain(language_tasks) .chain(language_tasks.filter(|_| remote_templates_task.is_none()))
.filter_map(|(kind, task)| { .filter_map(|(kind, task)| {
let id_base = kind.to_id_base(); let id_base = kind.to_id_base();
Some((kind, task.resolve_task(&id_base, task_context)?)) Some((kind, task.resolve_task(&id_base, task_context)?))
@ -259,7 +265,27 @@ impl Inventory {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let previously_spawned_tasks = task_usage let previously_spawned_tasks = task_usage
.into_iter() .into_iter()
.map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score)); .map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score))
.collect::<Vec<_>>();
let task_context = task_context.clone();
cx.spawn(move |_| async move {
let remote_templates = match remote_templates_task {
Some(task) => match task.await.log_err() {
Some(remote_templates) => remote_templates,
None => return (Vec::new(), Vec::new()),
},
None => Vec::new(),
};
let remote_tasks = remote_templates.into_iter().filter_map(|(kind, task)| {
let id_base = kind.to_id_base();
Some((
kind,
task.resolve_task(&id_base, &task_context)?,
not_used_score,
))
});
currently_resolved_tasks.extend(remote_tasks);
let mut tasks_by_label = BTreeMap::default(); let mut tasks_by_label = BTreeMap::default();
tasks_by_label = previously_spawned_tasks.into_iter().fold( tasks_by_label = previously_spawned_tasks.into_iter().fold(
@ -309,7 +335,7 @@ impl Inventory {
None None
} }
}) })
.collect(); .collect::<Vec<_>>();
( (
resolved, resolved,
@ -319,6 +345,7 @@ impl Inventory {
.map(|(kind, task, _)| (kind, task)) .map(|(kind, task, _)| (kind, task))
.collect(), .collect(),
) )
})
} }
/// Returns the last scheduled task, if any of the sources contains one with the matching id. /// Returns the last scheduled task, if any of the sources contains one with the matching id.
@ -443,21 +470,6 @@ mod test_inventory {
}) })
} }
pub(super) fn resolved_task_names(
inventory: &Model<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<String> {
inventory.update(cx, |inventory, _| {
let (used, current) =
inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task().label.clone())
.collect()
})
}
pub(super) fn register_task_used( pub(super) fn register_task_used(
inventory: &Model<Inventory>, inventory: &Model<Inventory>,
task_name: &str, task_name: &str,
@ -478,21 +490,28 @@ mod test_inventory {
}); });
} }
pub(super) fn list_tasks( pub(super) async fn list_tasks(
inventory: &Model<Inventory>, inventory: &Model<Inventory>,
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> { ) -> Vec<(TaskSourceKind, String)> {
inventory.update(cx, |inventory, _| { let (used, current) = inventory
let (used, current) = .update(cx, |inventory, cx| {
inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default()); 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()
.map(|(source_kind, task)| (source_kind, task.resolved_label)) .map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone())) .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect() .collect()
})
} }
} }
@ -622,9 +641,9 @@ mod tests {
use super::*; use super::*;
#[gpui::test] #[gpui::test]
fn test_task_list_sorting(cx: &mut TestAppContext) { async fn test_task_list_sorting(cx: &mut TestAppContext) {
let inventory = cx.update(Inventory::new); let inventory = cx.update(Inventory::new);
let initial_tasks = resolved_task_names(&inventory, None, cx); let initial_tasks = resolved_task_names(&inventory, None, cx).await;
assert!( assert!(
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:?}"
@ -671,7 +690,7 @@ mod tests {
&expected_initial_state, &expected_initial_state,
); );
assert_eq!( assert_eq!(
resolved_task_names(&inventory, None, cx), resolved_task_names(&inventory, None, cx).await,
&expected_initial_state, &expected_initial_state,
"Tasks with equal amount of usages should be sorted alphanumerically" "Tasks with equal amount of usages should be sorted alphanumerically"
); );
@ -682,7 +701,7 @@ mod tests {
&expected_initial_state, &expected_initial_state,
); );
assert_eq!( assert_eq!(
resolved_task_names(&inventory, None, cx), resolved_task_names(&inventory, None, cx).await,
vec![ vec![
"2_task".to_string(), "2_task".to_string(),
"2_task".to_string(), "2_task".to_string(),
@ -701,7 +720,7 @@ mod tests {
&expected_initial_state, &expected_initial_state,
); );
assert_eq!( assert_eq!(
resolved_task_names(&inventory, None, cx), resolved_task_names(&inventory, None, cx).await,
vec![ vec![
"3_task".to_string(), "3_task".to_string(),
"1_task".to_string(), "1_task".to_string(),
@ -736,7 +755,7 @@ mod tests {
&expected_updated_state, &expected_updated_state,
); );
assert_eq!( assert_eq!(
resolved_task_names(&inventory, None, cx), resolved_task_names(&inventory, None, cx).await,
vec![ vec![
"3_task".to_string(), "3_task".to_string(),
"1_task".to_string(), "1_task".to_string(),
@ -756,7 +775,7 @@ mod tests {
&expected_updated_state, &expected_updated_state,
); );
assert_eq!( assert_eq!(
resolved_task_names(&inventory, None, cx), resolved_task_names(&inventory, None, cx).await,
vec![ vec![
"11_hello".to_string(), "11_hello".to_string(),
"3_task".to_string(), "3_task".to_string(),
@ -773,7 +792,7 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
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); let inventory_with_statics = 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_1 = Path::new("path_1");
@ -797,7 +816,7 @@ mod tests {
); );
inventory.add_source( inventory.add_source(
TaskSourceKind::AbsPath { TaskSourceKind::AbsPath {
id_base: "test source", id_base: "test source".into(),
abs_path: path_1.to_path_buf(), abs_path: path_1.to_path_buf(),
}, },
|tx, cx| { |tx, cx| {
@ -811,7 +830,7 @@ mod tests {
); );
inventory.add_source( inventory.add_source(
TaskSourceKind::AbsPath { TaskSourceKind::AbsPath {
id_base: "test source", id_base: "test source".into(),
abs_path: path_2.to_path_buf(), abs_path: path_2.to_path_buf(),
}, },
|tx, cx| { |tx, cx| {
@ -827,7 +846,7 @@ mod tests {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: worktree_1, id: worktree_1,
abs_path: worktree_path_1.to_path_buf(), abs_path: worktree_path_1.to_path_buf(),
id_base: "test_source", id_base: "test_source".into(),
}, },
|tx, cx| { |tx, cx| {
static_test_source( static_test_source(
@ -842,7 +861,7 @@ mod tests {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: worktree_2, id: worktree_2,
abs_path: worktree_path_2.to_path_buf(), abs_path: worktree_path_2.to_path_buf(),
id_base: "test_source", id_base: "test_source".into(),
}, },
|tx, cx| { |tx, cx| {
static_test_source( static_test_source(
@ -858,28 +877,28 @@ mod tests {
let worktree_independent_tasks = vec![ let worktree_independent_tasks = vec![
( (
TaskSourceKind::AbsPath { TaskSourceKind::AbsPath {
id_base: "test source", id_base: "test source".into(),
abs_path: path_1.to_path_buf(), abs_path: path_1.to_path_buf(),
}, },
"static_source_1".to_string(), "static_source_1".to_string(),
), ),
( (
TaskSourceKind::AbsPath { TaskSourceKind::AbsPath {
id_base: "test source", id_base: "test source".into(),
abs_path: path_1.to_path_buf(), abs_path: path_1.to_path_buf(),
}, },
common_name.to_string(), common_name.to_string(),
), ),
( (
TaskSourceKind::AbsPath { TaskSourceKind::AbsPath {
id_base: "test source", id_base: "test source".into(),
abs_path: path_2.to_path_buf(), abs_path: path_2.to_path_buf(),
}, },
common_name.to_string(), common_name.to_string(),
), ),
( (
TaskSourceKind::AbsPath { TaskSourceKind::AbsPath {
id_base: "test source", id_base: "test source".into(),
abs_path: path_2.to_path_buf(), abs_path: path_2.to_path_buf(),
}, },
"static_source_2".to_string(), "static_source_2".to_string(),
@ -892,7 +911,7 @@ mod tests {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: worktree_1, id: worktree_1,
abs_path: worktree_path_1.to_path_buf(), abs_path: worktree_path_1.to_path_buf(),
id_base: "test_source", id_base: "test_source".into(),
}, },
common_name.to_string(), common_name.to_string(),
), ),
@ -900,7 +919,7 @@ mod tests {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: worktree_1, id: worktree_1,
abs_path: worktree_path_1.to_path_buf(), abs_path: worktree_path_1.to_path_buf(),
id_base: "test_source", id_base: "test_source".into(),
}, },
"worktree_1".to_string(), "worktree_1".to_string(),
), ),
@ -910,7 +929,7 @@ mod tests {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: worktree_2, id: worktree_2,
abs_path: worktree_path_2.to_path_buf(), abs_path: worktree_path_2.to_path_buf(),
id_base: "test_source", id_base: "test_source".into(),
}, },
common_name.to_string(), common_name.to_string(),
), ),
@ -918,7 +937,7 @@ mod tests {
TaskSourceKind::Worktree { TaskSourceKind::Worktree {
id: worktree_2, id: worktree_2,
abs_path: worktree_path_2.to_path_buf(), abs_path: worktree_path_2.to_path_buf(),
id_base: "test_source", id_base: "test_source".into(),
}, },
"worktree_2".to_string(), "worktree_2".to_string(),
), ),
@ -933,9 +952,12 @@ mod tests {
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone())) .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(list_tasks(&inventory_with_statics, None, cx), all_tasks);
assert_eq!( assert_eq!(
list_tasks(&inventory_with_statics, Some(worktree_1), cx), list_tasks(&inventory_with_statics, None, cx).await,
all_tasks
);
assert_eq!(
list_tasks(&inventory_with_statics, Some(worktree_1), cx).await,
worktree_1_tasks worktree_1_tasks
.iter() .iter()
.chain(worktree_independent_tasks.iter()) .chain(worktree_independent_tasks.iter())
@ -944,7 +966,7 @@ mod tests {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
assert_eq!( assert_eq!(
list_tasks(&inventory_with_statics, Some(worktree_2), cx), list_tasks(&inventory_with_statics, Some(worktree_2), cx).await,
worktree_2_tasks worktree_2_tasks
.iter() .iter()
.chain(worktree_independent_tasks.iter()) .chain(worktree_independent_tasks.iter())
@ -953,4 +975,26 @@ mod tests {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }
pub(super) async fn resolved_task_names(
inventory: &Model<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<String> {
let (used, current) = inventory
.update(cx, |inventory, cx| {
inventory.used_and_current_resolved_tasks(
None,
worktree,
None,
&TaskContext::default(),
cx,
)
})
.await;
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task().label.clone())
.collect()
}
} }

View File

@ -245,7 +245,12 @@ message Envelope {
RegenerateDevServerToken regenerate_dev_server_token = 200; RegenerateDevServerToken regenerate_dev_server_token = 200;
RegenerateDevServerTokenResponse regenerate_dev_server_token_response = 201; RegenerateDevServerTokenResponse regenerate_dev_server_token_response = 201;
RenameDevServer rename_dev_server = 202; // Current max RenameDevServer rename_dev_server = 202;
TaskContextForLocation task_context_for_location = 203;
TaskContext task_context = 204;
TaskTemplatesResponse task_templates_response = 205;
TaskTemplates task_templates = 206; // Current max
} }
reserved 158 to 161; reserved 158 to 161;
@ -2118,3 +2123,71 @@ message GetSupermavenApiKey {}
message GetSupermavenApiKeyResponse { message GetSupermavenApiKeyResponse {
string api_key = 1; string api_key = 1;
} }
message TaskContextForLocation {
uint64 project_id = 1;
Location location = 2;
}
message TaskContext {
optional string cwd = 1;
map<string, string> task_variables = 2;
}
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;
repeated string tags = 9;
}
enum RevealStrategy {
Always = 0;
Never = 1;
}
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;
}
}

View File

@ -279,6 +279,10 @@ messages!(
(StartLanguageServer, Foreground), (StartLanguageServer, Foreground),
(SynchronizeBuffers, Foreground), (SynchronizeBuffers, Foreground),
(SynchronizeBuffersResponse, Foreground), (SynchronizeBuffersResponse, Foreground),
(TaskContextForLocation, Background),
(TaskContext, Background),
(TaskTemplates, Background),
(TaskTemplatesResponse, Background),
(Test, Foreground), (Test, Foreground),
(Unfollow, Foreground), (Unfollow, Foreground),
(UnshareProject, Foreground), (UnshareProject, Foreground),
@ -326,7 +330,7 @@ messages!(
(RegenerateDevServerToken, Foreground), (RegenerateDevServerToken, Foreground),
(RegenerateDevServerTokenResponse, Foreground), (RegenerateDevServerTokenResponse, Foreground),
(RenameDevServer, Foreground), (RenameDevServer, Foreground),
(OpenNewBuffer, Foreground) (OpenNewBuffer, Foreground),
); );
request_messages!( request_messages!(
@ -414,6 +418,8 @@ request_messages!(
(SetChannelVisibility, Ack), (SetChannelVisibility, Ack),
(ShareProject, ShareProjectResponse), (ShareProject, ShareProjectResponse),
(SynchronizeBuffers, SynchronizeBuffersResponse), (SynchronizeBuffers, SynchronizeBuffersResponse),
(TaskContextForLocation, TaskContext),
(TaskTemplates, TaskTemplatesResponse),
(Test, Test), (Test, Test),
(UpdateBuffer, Ack), (UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack), (UpdateParticipantLocation, Ack),
@ -481,6 +487,8 @@ entity_messages!(
SearchProject, SearchProject,
StartLanguageServer, StartLanguageServer,
SynchronizeBuffers, SynchronizeBuffers,
TaskContextForLocation,
TaskTemplates,
UnshareProject, UnshareProject,
UpdateBuffer, UpdateBuffer,
UpdateBufferFile, UpdateBufferFile,

View File

@ -5,10 +5,11 @@ pub mod static_source;
mod task_template; mod task_template;
mod vscode_format; mod vscode_format;
use collections::{HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use gpui::SharedString; use gpui::SharedString;
use serde::Serialize; use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
use std::{borrow::Cow, path::Path}; use std::{borrow::Cow, path::Path};
pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates}; pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates};
@ -161,8 +162,35 @@ impl VariableName {
} }
} }
impl FromStr for VariableName {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
let value = match without_prefix {
"FILE" => Self::File,
"WORKTREE_ROOT" => Self::WorktreeRoot,
"SYMBOL" => Self::Symbol,
"SELECTED_TEXT" => Self::SelectedText,
"ROW" => Self::Row,
"COLUMN" => Self::Column,
_ => {
if let Some(custom_name) =
without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX)
{
Self::Custom(Cow::Owned(custom_name.to_owned()))
} else {
return Err(());
}
}
};
Ok(value)
}
}
/// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts. /// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts.
pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_"; pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_";
const ZED_CUSTOM_VARIABLE_NAME_PREFIX: &str = "CUSTOM_";
impl std::fmt::Display for VariableName { impl std::fmt::Display for VariableName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
@ -178,7 +206,10 @@ impl std::fmt::Display for VariableName {
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"), Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"), Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"), Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"), Self::Custom(s) => write!(
f,
"{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}"
),
} }
} }
} }
@ -219,6 +250,16 @@ impl FromIterator<(VariableName, String)> for TaskVariables {
} }
} }
impl IntoIterator for TaskVariables {
type Item = (VariableName, String);
type IntoIter = hash_map::IntoIter<VariableName, String>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function). /// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
/// Keeps all Zed-related state inside, used to produce a resolved task out of its template. /// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]

View File

@ -9,6 +9,7 @@ license = "GPL-3.0-or-later"
workspace = true workspace = true
[dependencies] [dependencies]
anyhow.workspace = true
editor.workspace = true editor.workspace = true
file_icons.workspace = true file_icons.workspace = true
fuzzy.workspace = true fuzzy.workspace = true

View File

@ -1,11 +1,8 @@
use std::sync::Arc;
use ::settings::Settings; use ::settings::Settings;
use editor::{tasks::task_context, Editor}; use editor::{tasks::task_context, Editor};
use gpui::{AppContext, ViewContext, WindowContext}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
use language::Language;
use modal::TasksModal; use modal::TasksModal;
use project::WorktreeId; use project::{Location, WorktreeId};
use workspace::tasks::schedule_task; use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace}; use workspace::{tasks::schedule_resolved_task, Workspace};
@ -34,7 +31,11 @@ pub fn init(cx: &mut AppContext) {
if let Some(use_new_terminal) = action.use_new_terminal { if let Some(use_new_terminal) = action.use_new_terminal {
original_task.use_new_terminal = use_new_terminal; original_task.use_new_terminal = use_new_terminal;
} }
let task_context = task_context(workspace, cx); 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| {
schedule_task( schedule_task(
workspace, workspace,
task_source_kind, task_source_kind,
@ -43,6 +44,10 @@ pub fn init(cx: &mut AppContext) {
false, false,
cx, cx,
) )
})
.ok()
})
.detach()
} else { } else {
if let Some(resolved) = last_scheduled_task.resolved.as_mut() { if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
if let Some(allow_concurrent_runs) = action.allow_concurrent_runs { if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
@ -62,7 +67,7 @@ pub fn init(cx: &mut AppContext) {
); );
} }
} else { } else {
toggle_modal(workspace, cx); toggle_modal(workspace, cx).detach();
}; };
}); });
}, },
@ -72,33 +77,52 @@ pub fn init(cx: &mut AppContext) {
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) { fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
match &action.task_name { match &action.task_name {
Some(name) => spawn_task_with_name(name.clone(), cx), Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx),
None => toggle_modal(workspace, cx), None => toggle_modal(workspace, cx).detach(),
} }
} }
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) { fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
let inventory = workspace.project().read(cx).task_inventory().clone(); let project = workspace.project().clone();
let workspace_handle = workspace.weak_handle(); let workspace_handle = workspace.weak_handle();
let task_context = task_context(workspace, cx); 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()
}) {
workspace.toggle_modal(cx, |cx| { workspace.toggle_modal(cx, |cx| {
TasksModal::new(inventory, task_context, workspace_handle, cx) TasksModal::new(project, task_context, workspace_handle, cx)
})
}
})
.ok();
}) })
} }
fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) { fn spawn_task_with_name(
name: String,
cx: &mut ViewContext<Workspace>,
) -> AsyncTask<anyhow::Result<()>> {
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
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)
})
})?
.await?;
let did_spawn = workspace let did_spawn = workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
let (worktree, language) = active_item_selection_properties(workspace, cx);
let tasks = workspace.project().update(cx, |project, cx| {
project
.task_inventory()
.update(cx, |inventory, _| inventory.list_tasks(language, worktree))
});
let (task_source_kind, target_task) = let (task_source_kind, target_task) =
tasks.into_iter().find(|(_, task)| task.label == name)?; tasks.into_iter().find(|(_, task)| task.label == name)?;
let task_context = task_context(workspace, cx);
schedule_task( schedule_task(
workspace, workspace,
task_source_kind, task_source_kind,
@ -108,9 +132,7 @@ fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
cx, cx,
); );
Some(()) Some(())
}) })?
.ok()
.flatten()
.is_some(); .is_some();
if !did_spawn { if !did_spawn {
workspace workspace
@ -119,32 +141,38 @@ fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
}) })
.ok(); .ok();
} }
Ok(())
}) })
.detach();
} }
fn active_item_selection_properties( fn active_item_selection_properties(
workspace: &Workspace, workspace: &Workspace,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> (Option<WorktreeId>, Option<Arc<Language>>) { ) -> (Option<WorktreeId>, Option<Location>) {
let active_item = workspace.active_item(cx); let active_item = workspace.active_item(cx);
let worktree_id = active_item let worktree_id = active_item
.as_ref() .as_ref()
.and_then(|item| item.project_path(cx)) .and_then(|item| item.project_path(cx))
.map(|path| path.worktree_id); .map(|path| path.worktree_id);
let language = active_item let location = active_item
.and_then(|active_item| active_item.act_as::<Editor>(cx)) .and_then(|active_item| active_item.act_as::<Editor>(cx))
.and_then(|editor| { .and_then(|editor| {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let selection = editor.selections.newest::<usize>(cx); let selection = editor.selections.newest_anchor();
let (buffer, buffer_position, _) = editor let multi_buffer = editor.buffer().clone();
.buffer() let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
.read(cx) let (buffer_snapshot, buffer_offset) =
.point_to_buffer_offset(selection.start, cx)?; multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
buffer.read(cx).language_at(buffer_position) let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
Some(Location {
buffer,
range: buffer_anchor..buffer_anchor,
})
}) })
}); });
(worktree_id, language) (worktree_id, location)
} }
#[cfg(test)] #[cfg(test)]
@ -250,12 +278,20 @@ mod tests {
.unwrap(); .unwrap();
buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx)); buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx));
let editor2 = cx.new_view(|cx| Editor::for_buffer(buffer2, Some(project), cx)); let editor2 = cx.new_view(|cx| Editor::for_buffer(buffer2, Some(project), cx));
workspace.update(cx, |this, cx| {
this.add_item_to_center(Box::new(editor1.clone()), cx); let first_context = workspace
this.add_item_to_center(Box::new(editor2.clone()), cx); .update(cx, |workspace, cx| {
assert_eq!(this.active_item(cx).unwrap().item_id(), editor2.entity_id()); workspace.add_item_to_center(Box::new(editor1.clone()), cx);
workspace.add_item_to_center(Box::new(editor2.clone()), cx);
assert_eq!( assert_eq!(
task_context(this, cx), workspace.active_item(cx).unwrap().item_id(),
editor2.entity_id()
);
task_context(workspace, cx)
})
.await;
assert_eq!(
first_context,
TaskContext { TaskContext {
cwd: Some("/dir".into()), cwd: Some("/dir".into()),
task_variables: TaskVariables::from_iter([ task_variables: TaskVariables::from_iter([
@ -270,12 +306,16 @@ mod tests {
]) ])
} }
); );
// And now, let's select an identifier. // And now, let's select an identifier.
editor2.update(cx, |this, cx| { editor2.update(cx, |editor, cx| {
this.change_selections(None, cx, |selections| selections.select_ranges([14..18])) editor.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
}); });
assert_eq!( assert_eq!(
task_context(this, cx), workspace
.update(cx, |workspace, cx| { task_context(workspace, cx) })
.await,
TaskContext { TaskContext {
cwd: Some("/dir".into()), cwd: Some("/dir".into()),
task_variables: TaskVariables::from_iter([ task_variables: TaskVariables::from_iter([
@ -293,10 +333,14 @@ mod tests {
} }
); );
// Now, let's switch the active item to .ts file.
this.activate_item(&editor1, cx);
assert_eq!( assert_eq!(
task_context(this, cx), workspace
.update(cx, |workspace, cx| {
// Now, let's switch the active item to .ts file.
workspace.activate_item(&editor1, cx);
task_context(workspace, cx)
})
.await,
TaskContext { TaskContext {
cwd: Some("/dir".into()), cwd: Some("/dir".into()),
task_variables: TaskVariables::from_iter([ task_variables: TaskVariables::from_iter([
@ -312,7 +356,6 @@ mod tests {
]) ])
} }
); );
});
} }
pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> { pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {

View File

@ -4,11 +4,11 @@ use crate::active_item_selection_properties;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView,
InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, View, InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, Task,
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::{Inventory, TaskSourceKind}; use project::{Project, TaskSourceKind};
use task::{ResolvedTask, TaskContext, TaskTemplate}; use task::{ResolvedTask, TaskContext, 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,
@ -60,7 +60,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 {
inventory: Model<Inventory>, project: Model<Project>,
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>,
@ -74,12 +74,12 @@ pub(crate) struct TasksModalDelegate {
impl TasksModalDelegate { impl TasksModalDelegate {
fn new( fn new(
inventory: Model<Inventory>, project: Model<Project>,
task_context: TaskContext, task_context: TaskContext,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
) -> Self { ) -> Self {
Self { Self {
inventory, project,
workspace, workspace,
candidates: None, candidates: None,
matches: Vec::new(), matches: Vec::new(),
@ -121,8 +121,10 @@ 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.inventory.update(cx, |inventory, _| { self.project.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, _| {
inventory.delete_previously_used(&task.id); inventory.delete_previously_used(&task.id);
})
}); });
} }
} }
@ -134,14 +136,14 @@ pub(crate) struct TasksModal {
impl TasksModal { impl TasksModal {
pub(crate) fn new( pub(crate) fn new(
inventory: Model<Inventory>, project: Model<Project>,
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(inventory, task_context, workspace), TasksModalDelegate::new(project, task_context, workspace),
cx, cx,
) )
}); });
@ -197,28 +199,59 @@ impl PickerDelegate for TasksModalDelegate {
&mut self, &mut self,
query: String, query: String,
cx: &mut ViewContext<picker::Picker<Self>>, cx: &mut ViewContext<picker::Picker<Self>>,
) -> gpui::Task<()> { ) -> Task<()> {
cx.spawn(move |picker, mut cx| async move { cx.spawn(move |picker, mut cx| async move {
let Some(candidates) = picker let Some(candidates_task) = picker
.update(&mut cx, |picker, cx| { .update(&mut cx, |picker, cx| {
let candidates = match &mut picker.delegate.candidates { match &mut picker.delegate.candidates {
Some(candidates) => candidates, Some(candidates) => {
Task::ready(Ok(string_match_candidates(candidates.iter())))
}
None => { None => {
let Ok((worktree, language)) = 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 Vec::new(); return Task::ready(Ok(Vec::new()));
}; };
let (used, current) =
picker.delegate.inventory.update(cx, |inventory, _| { let resolved_task =
inventory.used_and_current_resolved_tasks( picker.delegate.project.update(cx, |project, cx| {
language, let ssh_connection_string = project.ssh_connection_string(cx);
if project.is_remote() && 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, worktree,
&picker.delegate.task_context, 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() { picker.delegate.last_used_candidate_index = if used.is_empty() {
None None
} else { } else {
@ -227,23 +260,24 @@ impl PickerDelegate for TasksModalDelegate {
let mut new_candidates = used; let mut new_candidates = used;
new_candidates.extend(current); new_candidates.extend(current);
picker.delegate.candidates.insert(new_candidates) let match_candidates =
} string_match_candidates(new_candidates.iter());
}; let _ = picker.delegate.candidates.insert(new_candidates);
candidates match_candidates
.iter()
.enumerate()
.map(|(index, (_, candidate))| StringMatchCandidate {
id: index,
char_bag: candidate.resolved_label.chars().collect(),
string: candidate.display_label().to_owned(),
}) })
.collect::<Vec<_>>() })
}
}
}) })
.ok() .ok()
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,
@ -534,6 +568,19 @@ impl PickerDelegate for TasksModalDelegate {
} }
} }
fn string_match_candidates<'a>(
candidates: impl Iterator<Item = &'a (TaskSourceKind, ResolvedTask)> + 'a,
) -> Vec<StringMatchCandidate> {
candidates
.enumerate()
.map(|(index, (_, candidate))| StringMatchCandidate {
id: index,
char_bag: candidate.resolved_label.chars().collect(),
string: candidate.display_label().to_owned(),
})
.collect()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};

View File

@ -1,46 +1,9 @@
use std::path::PathBuf;
use project::TaskSourceKind; use project::TaskSourceKind;
use task::{ResolvedTask, TaskContext, TaskTemplate}; use task::{ResolvedTask, TaskContext, TaskTemplate};
use ui::{ViewContext, WindowContext}; use ui::ViewContext;
use crate::Workspace; use crate::Workspace;
pub fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
let project = workspace.project().read(cx);
let available_worktrees = project
.worktrees()
.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<_>>();
let cwd = match available_worktrees.len() {
0 => None,
1 => Some(available_worktrees[0].read(cx).abs_path()),
_ => {
let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
available_worktrees.into_iter().find_map(|worktree| {
let worktree = worktree.read(cx);
if worktree.contains_entry(entry_id) {
Some(worktree.abs_path())
} else {
None
}
})
});
anyhow::ensure!(
cwd_for_active_entry.is_some(),
"Cannot determine task cwd for multiple worktrees"
);
cwd_for_active_entry
}
};
Ok(cwd.map(|path| path.to_path_buf()))
}
pub fn schedule_task( pub fn schedule_task(
workspace: &Workspace, workspace: &Workspace,
task_source_kind: TaskSourceKind, task_source_kind: TaskSourceKind,

View File

@ -163,7 +163,9 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
}); });
let project = workspace.project().clone(); let project = workspace.project().clone();
if project.read(cx).is_local() { if project.update(cx, |project, cx| {
project.is_local() || project.ssh_connection_string(cx).is_some()
}) {
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
let fs = app_state.fs.clone(); let fs = app_state.fs.clone();
project.task_inventory().update(cx, |inventory, cx| { project.task_inventory().update(cx, |inventory, cx| {
@ -171,7 +173,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
watch_config_file(&cx.background_executor(), fs, paths::TASKS.clone()); watch_config_file(&cx.background_executor(), fs, paths::TASKS.clone());
inventory.add_source( inventory.add_source(
TaskSourceKind::AbsPath { TaskSourceKind::AbsPath {
id_base: "global_tasks", id_base: "global_tasks".into(),
abs_path: paths::TASKS.clone(), abs_path: paths::TASKS.clone(),
}, },
|tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)), |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),