tasks: Add experimental support for user-defined task variables (#13699)

Context:
@bennetbo spotted a regression in handling of `cargo run` task in zed
repo following a merge of #13658. We've started invoking `cargo run`
from the folder of an active file whereas previously we did it from the
workspace root. We brainstormed few solutions that involved adding a
separate task that gets invoked at a workspace level, but I realized
that a cleaner solution may be to finally add user-configured task
variables. This way, we can choose which crate to run by default at a
workspace level.

This has been originally brought up in the context of javascript tasks
in
https://github.com/zed-industries/zed/pull/12118#issuecomment-2129232114

Note that this is intended for internal use only for the time being.
/cc @RemcoSmitsDev we should be unblocked on having runner-dependant
tasks now.

Release notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2024-07-01 15:59:19 +02:00 committed by GitHub
parent 065ab93ca7
commit bac6e2fee7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 108 additions and 25 deletions

View File

@ -19,6 +19,13 @@
"JavaScript": { "JavaScript": {
"tab_size": 2, "tab_size": 2,
"formatter": "prettier" "formatter": "prettier"
},
"Rust": {
"tasks": {
"variables": {
"RUST_DEFAULT_PACKAGE_RUN": "zed"
}
}
} }
}, },
"formatter": "auto", "formatter": "auto",

View File

@ -128,7 +128,14 @@
// The default number of lines to expand excerpts in the multibuffer by. // The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3, "expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private. // Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"], "private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
// Whether to use additional LSP queries to format (and amend) the code after // Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities. // every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true, "use_on_type_format": true,
@ -666,6 +673,10 @@
// "max_scroll_history_lines": 10000, // "max_scroll_history_lines": 10000,
}, },
"code_actions_on_format": {}, "code_actions_on_format": {},
/// Settings related to running tasks.
"tasks": {
"variables": {}
},
// An object whose keys are language names, and whose values // An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should // are arrays of filenames or extensions of files that should
// use those languages. // use those languages.

View File

@ -8469,13 +8469,14 @@ impl Editor {
runnable: &mut Runnable, runnable: &mut Runnable,
cx: &WindowContext<'_>, cx: &WindowContext<'_>,
) -> Vec<(TaskSourceKind, TaskTemplate)> { ) -> Vec<(TaskSourceKind, TaskTemplate)> {
let (inventory, worktree_id) = project.read_with(cx, |project, cx| { let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
let worktree_id = project let (worktree_id, file) = project
.buffer_for_id(runnable.buffer) .buffer_for_id(runnable.buffer)
.and_then(|buffer| buffer.read(cx).file()) .and_then(|buffer| buffer.read(cx).file())
.map(|file| WorktreeId::from_usize(file.worktree_id())); .map(|file| (WorktreeId::from_usize(file.worktree_id()), file.clone()))
.unzip();
(project.task_inventory().clone(), worktree_id) (project.task_inventory().clone(), worktree_id, file)
}); });
let inventory = inventory.read(cx); let inventory = inventory.read(cx);
@ -8485,7 +8486,12 @@ impl Editor {
.flat_map(|tag| { .flat_map(|tag| {
let tag = tag.0.clone(); let tag = tag.0.clone();
inventory inventory
.list_tasks(Some(runnable.language.clone()), worktree_id) .list_tasks(
file.clone(),
Some(runnable.language.clone()),
worktree_id,
cx,
)
.into_iter() .into_iter()
.filter(move |(_, template)| { .filter(move |(_, template)| {
template.tags.iter().any(|source_tag| source_tag == &tag) template.tags.iter().any(|source_tag| source_tag == &tag)

View File

@ -120,6 +120,8 @@ pub struct LanguageSettings {
pub code_actions_on_format: HashMap<String, bool>, pub code_actions_on_format: HashMap<String, bool>,
/// Whether to perform linked edits /// Whether to perform linked edits
pub linked_edits: bool, pub linked_edits: bool,
/// Task configuration for this language.
pub tasks: LanguageTaskConfig,
} }
impl LanguageSettings { impl LanguageSettings {
@ -340,6 +342,10 @@ pub struct LanguageSettingsContent {
/// ///
/// Default: true /// Default: true
pub linked_edits: Option<bool>, pub linked_edits: Option<bool>,
/// Task configuration for this language.
///
/// Default: {}
pub tasks: Option<LanguageTaskConfig>,
} }
/// The contents of the inline completion settings. /// The contents of the inline completion settings.
@ -546,6 +552,13 @@ fn scroll_debounce_ms() -> u64 {
50 50
} }
/// The task settings for a particular language.
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct LanguageTaskConfig {
/// Extra task variables to set for a particular language.
pub variables: HashMap<String, String>,
}
impl InlayHintSettings { impl InlayHintSettings {
/// Returns the kinds of inlay hints that are enabled based on the settings. /// Returns the kinds of inlay hints that are enabled based on the settings.
pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> { pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
@ -823,6 +836,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.code_actions_on_format.clone(), src.code_actions_on_format.clone(),
); );
merge(&mut settings.linked_edits, src.linked_edits); merge(&mut settings.linked_edits, src.linked_edits);
merge(&mut settings.tasks, src.tasks.clone());
merge( merge(
&mut settings.preferred_line_length, &mut settings.preferred_line_length,

View File

@ -1,4 +1,4 @@
use std::ops::Range; use std::{ops::Range, sync::Arc};
use crate::{Location, Runnable}; use crate::{Location, Runnable};
@ -31,7 +31,11 @@ pub trait ContextProvider: Send + Sync {
} }
/// Provides all tasks, associated with the current language. /// Provides all tasks, associated with the current language.
fn associated_tasks(&self) -> Option<TaskTemplates> { fn associated_tasks(
&self,
_: Option<Arc<dyn crate::File>>,
_cx: &AppContext,
) -> Option<TaskTemplates> {
None None
} }
} }

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
use gpui::{AsyncAppContext, Task}; use gpui::{AppContext, AsyncAppContext, Task};
use http::github::latest_github_release; use http::github::latest_github_release;
pub use language::*; pub use language::*;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -501,7 +501,11 @@ impl ContextProvider for GoContextProvider {
)) ))
} }
fn associated_tasks(&self) -> Option<TaskTemplates> { fn associated_tasks(
&self,
_: Option<Arc<dyn language::File>>,
_: &AppContext,
) -> Option<TaskTemplates> {
Some(TaskTemplates(vec![ Some(TaskTemplates(vec![
TaskTemplate { TaskTemplate {
label: format!( label: format!(

View File

@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use gpui::AppContext;
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate}; use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary; use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
@ -220,7 +221,11 @@ impl ContextProvider for PythonContextProvider {
Ok(task::TaskVariables::from_iter([unittest_target])) Ok(task::TaskVariables::from_iter([unittest_target]))
} }
fn associated_tasks(&self) -> Option<TaskTemplates> { fn associated_tasks(
&self,
_: Option<Arc<dyn language::File>>,
_: &AppContext,
) -> Option<TaskTemplates> {
Some(TaskTemplates(vec![ Some(TaskTemplates(vec![
TaskTemplate { TaskTemplate {
label: "execute selection".to_owned(), label: "execute selection".to_owned(),

View File

@ -2,9 +2,10 @@ use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_trait::async_trait; use async_trait::async_trait;
use futures::{io::BufReader, StreamExt}; use futures::{io::BufReader, StreamExt};
use gpui::AsyncAppContext; use gpui::{AppContext, AsyncAppContext};
use http::github::{latest_github_release, GitHubLspBinaryVersion}; use http::github::{latest_github_release, GitHubLspBinaryVersion};
pub use language::*; pub use language::*;
use language_settings::all_language_settings;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use lsp::LanguageServerBinary; use lsp::LanguageServerBinary;
use project::project_settings::{BinarySettings, ProjectSettings}; use project::project_settings::{BinarySettings, ProjectSettings};
@ -407,7 +408,22 @@ impl ContextProvider for RustContextProvider {
Ok(TaskVariables::default()) Ok(TaskVariables::default())
} }
fn associated_tasks(&self) -> Option<TaskTemplates> { fn associated_tasks(
&self,
file: Option<Arc<dyn language::File>>,
cx: &AppContext,
) -> Option<TaskTemplates> {
const DEFAULT_RUN_NAME_STR: &'static str = "RUST_DEFAULT_PACKAGE_RUN";
let package_to_run = all_language_settings(file.as_ref(), cx)
.language(Some("Rust"))
.tasks
.variables
.get(DEFAULT_RUN_NAME_STR);
let run_task_args = if let Some(package_to_run) = package_to_run {
vec!["run".into(), "-p".into(), package_to_run.clone()]
} else {
vec!["run".into()]
};
Some(TaskTemplates(vec![ Some(TaskTemplates(vec![
TaskTemplate { TaskTemplate {
label: format!( label: format!(
@ -501,7 +517,7 @@ impl ContextProvider for RustContextProvider {
TaskTemplate { TaskTemplate {
label: "cargo run".into(), label: "cargo run".into(),
command: "cargo".into(), command: "cargo".into(),
args: vec!["run".into()], args: run_task_args,
cwd: Some("$ZED_DIRNAME".to_owned()), cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default() ..TaskTemplate::default()
}, },

View File

@ -10861,12 +10861,19 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>> { ) -> Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>> {
if self.is_local() { if self.is_local() {
let language = location let (file, language) = location
.and_then(|location| location.buffer.read(cx).language_at(location.range.start)); .map(|location| {
let buffer = location.buffer.read(cx);
(
buffer.file().cloned(),
buffer.language_at(location.range.start),
)
})
.unwrap_or_default();
Task::ready(Ok(self Task::ready(Ok(self
.task_inventory() .task_inventory()
.read(cx) .read(cx)
.list_tasks(language, worktree))) .list_tasks(file, language, worktree, cx)))
} else if let Some(project_id) = self } else if let Some(project_id) = self
.remote_id() .remote_id()
.filter(|_| self.ssh_connection_string(cx).is_some()) .filter(|_| self.ssh_connection_string(cx).is_some())

View File

@ -15,7 +15,7 @@ use futures::{
}; };
use gpui::{AppContext, Context, Model, ModelContext, Task}; use gpui::{AppContext, Context, Model, ModelContext, Task};
use itertools::Itertools; use itertools::Itertools;
use language::{ContextProvider, Language, Location}; use language::{ContextProvider, File, Language, Location};
use task::{ use task::{
static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
TaskVariables, VariableName, TaskVariables, VariableName,
@ -155,14 +155,16 @@ impl Inventory {
/// returns all task templates with their source kinds, in no specific order. /// returns all task templates with their source kinds, in no specific order.
pub fn list_tasks( pub fn list_tasks(
&self, &self,
file: Option<Arc<dyn File>>,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
cx: &AppContext,
) -> Vec<(TaskSourceKind, TaskTemplate)> { ) -> Vec<(TaskSourceKind, TaskTemplate)> {
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(),
}); });
let language_tasks = language let language_tasks = language
.and_then(|language| language.context_provider()?.associated_tasks()) .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter() .into_iter()
.flat_map(|tasks| tasks.0.into_iter()) .flat_map(|tasks| tasks.0.into_iter())
.flat_map(|task| Some((task_source_kind.as_ref()?, task))); .flat_map(|task| Some((task_source_kind.as_ref()?, task)));
@ -207,8 +209,11 @@ impl Inventory {
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(),
}); });
let file = location
.as_ref()
.and_then(|location| location.buffer.read(cx).file().cloned());
let language_tasks = language let language_tasks = language
.and_then(|language| language.context_provider()?.associated_tasks()) .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter() .into_iter()
.flat_map(|tasks| tasks.0.into_iter()) .flat_map(|tasks| tasks.0.into_iter())
.flat_map(|task| Some((task_source_kind.as_ref()?, task))); .flat_map(|task| Some((task_source_kind.as_ref()?, task)));
@ -471,9 +476,9 @@ mod test_inventory {
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Vec<String> { ) -> Vec<String> {
inventory.update(cx, |inventory, _| { inventory.update(cx, |inventory, cx| {
inventory inventory
.list_tasks(None, worktree) .list_tasks(None, None, worktree, cx)
.into_iter() .into_iter()
.map(|(_, task)| task.label) .map(|(_, task)| task.label)
.sorted() .sorted()
@ -486,9 +491,9 @@ mod test_inventory {
task_name: &str, task_name: &str,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) {
inventory.update(cx, |inventory, _| { inventory.update(cx, |inventory, cx| {
let (task_source_kind, task) = inventory let (task_source_kind, task) = inventory
.list_tasks(None, None) .list_tasks(None, None, None, cx)
.into_iter() .into_iter()
.find(|(_, task)| task.label == task_name) .find(|(_, task)| task.label == task_name)
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}")); .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
@ -639,7 +644,11 @@ impl ContextProviderWithTasks {
} }
impl ContextProvider for ContextProviderWithTasks { impl ContextProvider for ContextProviderWithTasks {
fn associated_tasks(&self) -> Option<TaskTemplates> { fn associated_tasks(
&self,
_: Option<Arc<dyn language::File>>,
_: &AppContext,
) -> Option<TaskTemplates> {
Some(self.templates.clone()) Some(self.templates.clone())
} }
} }