ZIm/crates/tasks_ui/src/lib.rs
Thorsten Ball 0360cda543
tasks: Use environment variables from project (#15266)
This fixes #12125 and addresses what's described in here:

-
https://github.com/zed-industries/zed/issues/4977#issuecomment-2162094388

Before the changes in this PR, when running tasks, they inherited the
Zed process environment, but that might not be the process environment
that you'd get if you `cd` into a project directory.

We already ran into that problem with language servers and we fixed it
by loading the shell environment in the context of a projects root
directory and then passing that to the language servers when starting
them (or when looking for their binaries).

What the change here does is to add the behavior for tasks too: we use
the project-environment as the base environment with which to spawn
tasks. Everything else still works the same, except that the base env is
different.

Release Notes:

- Improved the environment-variable detection when running tasks so that
tasks can now access environment variables as if the task had been
spawned in a terminal that `cd`ed into a project directory. That means
environment variables set by `direnv`/`asdf`/`mise` and other tools are
now picked up.
([#12125](https://github.com/zed-industries/zed/issues/12125)).

Demo:


https://github.com/user-attachments/assets/8bfcc98f-0f9b-4439-b0d9-298aef1a3efe
2024-07-26 18:19:53 +02:00

379 lines
15 KiB
Rust

use ::settings::Settings;
use editor::{tasks::task_context, Editor};
use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
use modal::TasksModal;
use project::{Location, WorktreeId};
use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace};
mod modal;
mod settings;
pub use modal::{Rerun, Spawn};
pub fn init(cx: &mut AppContext) {
settings::TaskSettings::register(cx);
cx.observe_new_views(
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
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()
.read(cx)
.last_scheduled_task(action.task_id.as_ref())
})
{
if action.reevaluate_context {
let mut original_task = last_scheduled_task.original_task().clone();
if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
original_task.allow_concurrent_runs = allow_concurrent_runs;
}
if let Some(use_new_terminal) = action.use_new_terminal {
original_task.use_new_terminal = use_new_terminal;
}
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(
workspace,
task_source_kind,
&original_task,
&task_context,
false,
cx,
)
})
.ok()
})
.detach()
} else {
if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
resolved.allow_concurrent_runs = allow_concurrent_runs;
}
if let Some(use_new_terminal) = action.use_new_terminal {
resolved.use_new_terminal = use_new_terminal;
}
}
schedule_resolved_task(
workspace,
task_source_kind,
last_scheduled_task,
false,
cx,
);
}
} else {
toggle_modal(workspace, cx).detach();
};
});
},
)
.detach();
}
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
match &action.task_name {
Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx),
None => toggle_modal(workspace, cx).detach(),
}
}
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
let project = workspace.project().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()
}) {
workspace.toggle_modal(cx, |cx| {
TasksModal::new(project, task_context, workspace_handle, cx)
})
}
})
.ok();
})
}
fn spawn_task_with_name(
name: String,
cx: &mut ViewContext<Workspace>,
) -> AsyncTask<anyhow::Result<()>> {
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
.update(&mut cx, |workspace, cx| {
let (task_source_kind, target_task) =
tasks.into_iter().find(|(_, task)| task.label == name)?;
schedule_task(
workspace,
task_source_kind,
&target_task,
&task_context,
false,
cx,
);
Some(())
})?
.is_some();
if !did_spawn {
workspace
.update(&mut cx, |workspace, cx| {
spawn_task_or_modal(workspace, &Spawn::default(), cx);
})
.ok();
}
Ok(())
})
}
fn active_item_selection_properties(
workspace: &Workspace,
cx: &mut WindowContext,
) -> (Option<WorktreeId>, Option<Location>) {
let active_item = workspace.active_item(cx);
let worktree_id = active_item
.as_ref()
.and_then(|item| item.project_path(cx))
.map(|path| path.worktree_id);
let location = active_item
.and_then(|active_item| active_item.act_as::<Editor>(cx))
.and_then(|editor| {
editor.update(cx, |editor, cx| {
let selection = editor.selections.newest_anchor();
let multi_buffer = editor.buffer().clone();
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
let (buffer_snapshot, buffer_offset) =
multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
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, location)
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, sync::Arc};
use editor::Editor;
use gpui::{Entity, TestAppContext};
use language::{Language, LanguageConfig};
use project::{BasicContextProvider, FakeFs, Project};
use serde_json::json;
use task::{TaskContext, TaskVariables, VariableName};
use ui::VisualContext;
use workspace::{AppState, Workspace};
use crate::task_context;
#[gpui::test]
async fn test_default_language_context(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
json!({
".zed": {
"tasks.json": r#"[
{
"label": "example task",
"command": "echo",
"args": ["4"]
},
{
"label": "another one",
"command": "echo",
"args": ["55"]
},
]"#,
},
"a.ts": "function this_is_a_test() { }",
"rust": {
"b.rs": "use std; fn this_is_a_rust_file() { }",
}
}),
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let rust_language = Arc::new(
Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::language()),
)
.with_outline_query(
r#"(function_item
"fn" @context
name: (_) @name) @item"#,
)
.unwrap()
.with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))),
);
let typescript_language = Arc::new(
Language::new(
LanguageConfig::default(),
Some(tree_sitter_typescript::language_typescript()),
)
.with_outline_query(
r#"(function_declaration
"async"? @context
"function" @context
name: (_) @name
parameters: (formal_parameters
"(" @context
")" @context)) @item"#,
)
.unwrap()
.with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))),
);
let worktree_id = project.update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let buffer1 = workspace
.update(cx, |this, cx| {
this.project()
.update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
})
.await
.unwrap();
buffer1.update(cx, |this, cx| {
this.set_language(Some(typescript_language), cx)
});
let editor1 = cx.new_view(|cx| Editor::for_buffer(buffer1, Some(project.clone()), cx));
let buffer2 = workspace
.update(cx, |this, cx| {
this.project().update(cx, |this, cx| {
this.open_buffer((worktree_id, "rust/b.rs"), cx)
})
})
.await
.unwrap();
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 first_context = workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_center(Box::new(editor1.clone()), cx);
workspace.add_item_to_center(Box::new(editor2.clone()), cx);
assert_eq!(
workspace.active_item(cx).unwrap().item_id(),
editor2.entity_id()
);
task_context(workspace, cx)
})
.await;
assert_eq!(
first_context,
TaskContext {
cwd: Some("/dir".into()),
task_variables: TaskVariables::from_iter([
(VariableName::File, "/dir/rust/b.rs".into()),
(VariableName::Filename, "b.rs".into()),
(VariableName::RelativeFile, "rust/b.rs".into()),
(VariableName::Dirname, "/dir/rust".into()),
(VariableName::Stem, "b".into()),
(VariableName::WorktreeRoot, "/dir".into()),
(VariableName::Row, "1".into()),
(VariableName::Column, "1".into()),
]),
project_env: HashMap::default(),
}
);
// And now, let's select an identifier.
editor2.update(cx, |editor, cx| {
editor.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
});
assert_eq!(
workspace
.update(cx, |workspace, cx| { task_context(workspace, cx) })
.await,
TaskContext {
cwd: Some("/dir".into()),
task_variables: TaskVariables::from_iter([
(VariableName::File, "/dir/rust/b.rs".into()),
(VariableName::Filename, "b.rs".into()),
(VariableName::RelativeFile, "rust/b.rs".into()),
(VariableName::Dirname, "/dir/rust".into()),
(VariableName::Stem, "b".into()),
(VariableName::WorktreeRoot, "/dir".into()),
(VariableName::Row, "1".into()),
(VariableName::Column, "15".into()),
(VariableName::SelectedText, "is_i".into()),
(VariableName::Symbol, "this_is_a_rust_file".into()),
]),
project_env: HashMap::default(),
}
);
assert_eq!(
workspace
.update(cx, |workspace, cx| {
// Now, let's switch the active item to .ts file.
workspace.activate_item(&editor1, true, true, cx);
task_context(workspace, cx)
})
.await,
TaskContext {
cwd: Some("/dir".into()),
task_variables: TaskVariables::from_iter([
(VariableName::File, "/dir/a.ts".into()),
(VariableName::Filename, "a.ts".into()),
(VariableName::RelativeFile, "a.ts".into()),
(VariableName::Dirname, "/dir".into()),
(VariableName::Stem, "a".into()),
(VariableName::WorktreeRoot, "/dir".into()),
(VariableName::Row, "1".into()),
(VariableName::Column, "1".into()),
(VariableName::Symbol, "this_is_a_test".into()),
]),
project_env: HashMap::default(),
}
);
}
pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
file_icons::init((), cx);
language::init(cx);
crate::init(cx);
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
state
})
}
}