From bac6e2fee77e612b38062ee2ebeed702447e397a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:59:19 +0200 Subject: [PATCH] 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 --- .zed/settings.json | 7 +++++++ assets/settings/default.json | 13 +++++++++++- crates/editor/src/editor.rs | 16 ++++++++++----- crates/language/src/language_settings.rs | 14 +++++++++++++ crates/language/src/task_context.rs | 8 ++++++-- crates/languages/src/go.rs | 8 ++++++-- crates/languages/src/python.rs | 7 ++++++- crates/languages/src/rust.rs | 22 ++++++++++++++++++--- crates/project/src/project.rs | 13 +++++++++--- crates/project/src/task_inventory.rs | 25 ++++++++++++++++-------- 10 files changed, 108 insertions(+), 25 deletions(-) diff --git a/.zed/settings.json b/.zed/settings.json index eedf2f3753..00c0f8e024 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -19,6 +19,13 @@ "JavaScript": { "tab_size": 2, "formatter": "prettier" + }, + "Rust": { + "tasks": { + "variables": { + "RUST_DEFAULT_PACKAGE_RUN": "zed" + } + } } }, "formatter": "auto", diff --git a/assets/settings/default.json b/assets/settings/default.json index 023fdec868..3078b5f28e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -128,7 +128,14 @@ // The default number of lines to expand excerpts in the multibuffer by. "expand_excerpt_lines": 3, // 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 // every "trigger" symbol input, defined by LSP server capabilities. "use_on_type_format": true, @@ -666,6 +673,10 @@ // "max_scroll_history_lines": 10000, }, "code_actions_on_format": {}, + /// Settings related to running tasks. + "tasks": { + "variables": {} + }, // An object whose keys are language names, and whose values // are arrays of filenames or extensions of files that should // use those languages. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7ca6af8008..e534df3d65 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8469,13 +8469,14 @@ impl Editor { runnable: &mut Runnable, cx: &WindowContext<'_>, ) -> Vec<(TaskSourceKind, TaskTemplate)> { - let (inventory, worktree_id) = project.read_with(cx, |project, cx| { - let worktree_id = project + let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { + let (worktree_id, file) = project .buffer_for_id(runnable.buffer) .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); @@ -8485,7 +8486,12 @@ impl Editor { .flat_map(|tag| { let tag = tag.0.clone(); inventory - .list_tasks(Some(runnable.language.clone()), worktree_id) + .list_tasks( + file.clone(), + Some(runnable.language.clone()), + worktree_id, + cx, + ) .into_iter() .filter(move |(_, template)| { template.tags.iter().any(|source_tag| source_tag == &tag) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index dff00d2b66..eacf8ac645 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -120,6 +120,8 @@ pub struct LanguageSettings { pub code_actions_on_format: HashMap, /// Whether to perform linked edits pub linked_edits: bool, + /// Task configuration for this language. + pub tasks: LanguageTaskConfig, } impl LanguageSettings { @@ -340,6 +342,10 @@ pub struct LanguageSettingsContent { /// /// Default: true pub linked_edits: Option, + /// Task configuration for this language. + /// + /// Default: {} + pub tasks: Option, } /// The contents of the inline completion settings. @@ -546,6 +552,13 @@ fn scroll_debounce_ms() -> u64 { 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, +} + impl InlayHintSettings { /// Returns the kinds of inlay hints that are enabled based on the settings. pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { @@ -823,6 +836,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent src.code_actions_on_format.clone(), ); merge(&mut settings.linked_edits, src.linked_edits); + merge(&mut settings.tasks, src.tasks.clone()); merge( &mut settings.preferred_line_length, diff --git a/crates/language/src/task_context.rs b/crates/language/src/task_context.rs index 743085f6ec..cc3f29558e 100644 --- a/crates/language/src/task_context.rs +++ b/crates/language/src/task_context.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{ops::Range, sync::Arc}; use crate::{Location, Runnable}; @@ -31,7 +31,11 @@ pub trait ContextProvider: Send + Sync { } /// Provides all tasks, associated with the current language. - fn associated_tasks(&self) -> Option { + fn associated_tasks( + &self, + _: Option>, + _cx: &AppContext, + ) -> Option { None } } diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 4b03117a0a..48641c5729 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; -use gpui::{AsyncAppContext, Task}; +use gpui::{AppContext, AsyncAppContext, Task}; use http::github::latest_github_release; pub use language::*; use lazy_static::lazy_static; @@ -501,7 +501,11 @@ impl ContextProvider for GoContextProvider { )) } - fn associated_tasks(&self) -> Option { + fn associated_tasks( + &self, + _: Option>, + _: &AppContext, + ) -> Option { Some(TaskTemplates(vec![ TaskTemplate { label: format!( diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a2ec1eef0e..4335ed5ba4 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1,5 +1,6 @@ use anyhow::Result; use async_trait::async_trait; +use gpui::AppContext; use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; @@ -220,7 +221,11 @@ impl ContextProvider for PythonContextProvider { Ok(task::TaskVariables::from_iter([unittest_target])) } - fn associated_tasks(&self) -> Option { + fn associated_tasks( + &self, + _: Option>, + _: &AppContext, + ) -> Option { Some(TaskTemplates(vec![ TaskTemplate { label: "execute selection".to_owned(), diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index f033e7936f..a92021287c 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -2,9 +2,10 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use futures::{io::BufReader, StreamExt}; -use gpui::AsyncAppContext; +use gpui::{AppContext, AsyncAppContext}; use http::github::{latest_github_release, GitHubLspBinaryVersion}; pub use language::*; +use language_settings::all_language_settings; use lazy_static::lazy_static; use lsp::LanguageServerBinary; use project::project_settings::{BinarySettings, ProjectSettings}; @@ -407,7 +408,22 @@ impl ContextProvider for RustContextProvider { Ok(TaskVariables::default()) } - fn associated_tasks(&self) -> Option { + fn associated_tasks( + &self, + file: Option>, + cx: &AppContext, + ) -> Option { + 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![ TaskTemplate { label: format!( @@ -501,7 +517,7 @@ impl ContextProvider for RustContextProvider { TaskTemplate { label: "cargo run".into(), command: "cargo".into(), - args: vec!["run".into()], + args: run_task_args, cwd: Some("$ZED_DIRNAME".to_owned()), ..TaskTemplate::default() }, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f4b60149a7..4185281bd5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -10861,12 +10861,19 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let language = location - .and_then(|location| location.buffer.read(cx).language_at(location.range.start)); + let (file, language) = location + .map(|location| { + let buffer = location.buffer.read(cx); + ( + buffer.file().cloned(), + buffer.language_at(location.range.start), + ) + }) + .unwrap_or_default(); Task::ready(Ok(self .task_inventory() .read(cx) - .list_tasks(language, worktree))) + .list_tasks(file, language, worktree, cx))) } else if let Some(project_id) = self .remote_id() .filter(|_| self.ssh_connection_string(cx).is_some()) diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 3f91bd16c7..ac3f93201c 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -15,7 +15,7 @@ use futures::{ }; use gpui::{AppContext, Context, Model, ModelContext, Task}; use itertools::Itertools; -use language::{ContextProvider, Language, Location}; +use language::{ContextProvider, File, Language, Location}; use task::{ static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName, @@ -155,14 +155,16 @@ impl Inventory { /// returns all task templates with their source kinds, in no specific order. pub fn list_tasks( &self, + file: Option>, language: Option>, worktree: Option, + cx: &AppContext, ) -> Vec<(TaskSourceKind, TaskTemplate)> { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name(), }); let language_tasks = language - .and_then(|language| language.context_provider()?.associated_tasks()) + .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) .flat_map(|task| Some((task_source_kind.as_ref()?, task))); @@ -207,8 +209,11 @@ impl Inventory { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name(), }); + let file = location + .as_ref() + .and_then(|location| location.buffer.read(cx).file().cloned()); let language_tasks = language - .and_then(|language| language.context_provider()?.associated_tasks()) + .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) .flat_map(|task| Some((task_source_kind.as_ref()?, task))); @@ -471,9 +476,9 @@ mod test_inventory { worktree: Option, cx: &mut TestAppContext, ) -> Vec { - inventory.update(cx, |inventory, _| { + inventory.update(cx, |inventory, cx| { inventory - .list_tasks(None, worktree) + .list_tasks(None, None, worktree, cx) .into_iter() .map(|(_, task)| task.label) .sorted() @@ -486,9 +491,9 @@ mod test_inventory { task_name: &str, cx: &mut TestAppContext, ) { - inventory.update(cx, |inventory, _| { + inventory.update(cx, |inventory, cx| { let (task_source_kind, task) = inventory - .list_tasks(None, None) + .list_tasks(None, None, None, cx) .into_iter() .find(|(_, task)| task.label == task_name) .unwrap_or_else(|| panic!("Failed to find task with name {task_name}")); @@ -639,7 +644,11 @@ impl ContextProviderWithTasks { } impl ContextProvider for ContextProviderWithTasks { - fn associated_tasks(&self) -> Option { + fn associated_tasks( + &self, + _: Option>, + _: &AppContext, + ) -> Option { Some(self.templates.clone()) } }