From 4dc61f7ccd6ac77a6f03d90d7ee80e855b0f32b9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:18:33 +0100 Subject: [PATCH] Extensions registering tasks (#9572) This PR also introduces built-in tasks for Rust and Elixir. Note that this is not a precedent for future PRs to include tasks for more languages; we simply want to find the rough edges with tasks & language integrations before proceeding to task contexts provided by extensions. As is, we'll load tasks for all loaded languages, so in order to get Elixir tasks, you have to open an Elixir buffer first. I think it sort of makes sense (though it's not ideal), as in the future where extensions do provide their own tasks.json, we'd like to limit the # of tasks surfaced to the user to make them as relevant to the project at hand as possible. Release Notes: - Added built-in tasks for Rust and Elixir files. --- Cargo.lock | 3 + crates/extension/Cargo.toml | 2 + crates/extension/src/extension_store.rs | 13 ++- crates/language/Cargo.toml | 1 + crates/language/src/language.rs | 53 ++--------- crates/language/src/language_registry.rs | 35 +++++--- crates/language/src/task_context.rs | 109 +++++++++++++++++++++++ crates/languages/src/elixir.rs | 43 +++++++++ crates/languages/src/json.rs | 2 +- crates/languages/src/lib.rs | 41 ++++++--- crates/languages/src/rust.rs | 95 ++++++++++++++------ crates/project/src/task_inventory.rs | 4 +- crates/task/src/lib.rs | 15 +++- crates/task/src/oneshot_source.rs | 7 +- crates/task/src/static_source.rs | 57 ++++++++---- crates/task/src/vscode_format.rs | 10 +-- crates/tasks_ui/src/lib.rs | 41 ++++----- crates/tasks_ui/src/modal.rs | 47 +++++----- crates/zed/src/zed.rs | 7 ++ 19 files changed, 416 insertions(+), 169 deletions(-) create mode 100644 crates/language/src/task_context.rs diff --git a/Cargo.lock b/Cargo.lock index 033c99d1cd..57a19d8a9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3486,7 +3486,9 @@ dependencies = [ "schemars", "serde", "serde_json", + "serde_json_lenient", "settings", + "task", "theme", "toml 0.8.10", "url", @@ -5176,6 +5178,7 @@ dependencies = [ "smallvec", "smol", "sum_tree", + "task", "text", "theme", "tree-sitter", diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 9282836cf5..4c982ad0fe 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -44,6 +44,8 @@ wasmtime.workspace = true wasmtime-wasi.workspace = true wasmparser.workspace = true wit-component.workspace = true +task.workspace = true +serde_json_lenient.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 7fe17402bc..3a1c971c38 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -23,7 +23,8 @@ use futures::{ }; use gpui::{actions, AppContext, Context, EventEmitter, Global, Model, ModelContext, Task}; use language::{ - LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES, + ContextProviderWithTasks, LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, + QUERY_FILENAME_PREFIXES, }; use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; @@ -835,12 +836,18 @@ impl ExtensionStore { language_name.clone(), language.grammar.clone(), language.matcher.clone(), - None, move || { let config = std::fs::read_to_string(language_path.join("config.toml"))?; let config: LanguageConfig = ::toml::from_str(&config)?; let queries = load_plugin_queries(&language_path); - Ok((config, queries)) + let tasks = std::fs::read_to_string(language_path.join("tasks.json")) + .ok() + .and_then(|contents| { + let definitions = serde_json_lenient::from_str(&contents).log_err()?; + Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>) + }); + + Ok((config, queries, tasks)) }, ); } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index e6f2a45ea5..b513fbb255 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -58,6 +58,7 @@ pulldown-cmark.workspace = true tree-sitter.workspace = true unicase = "2.6" util.workspace = true +task.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8f2f4bce6e..7578393478 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -14,6 +14,7 @@ pub mod language_settings; mod outline; pub mod proto; mod syntax_map; +mod task_context; #[cfg(test)] mod buffer_tests; @@ -54,6 +55,9 @@ use std::{ }, }; use syntax_map::SyntaxSnapshot; +pub use task_context::{ + ContextProvider, ContextProviderWithTasks, LanguageSource, SymbolContextProvider, +}; use theme::SyntaxTheme; use tree_sitter::{self, wasmtime, Query, WasmStore}; use util::http::HttpClient; @@ -120,46 +124,6 @@ pub struct Location { pub range: Range, } -pub struct LanguageContext { - pub package: Option, - pub symbol: Option, -} - -pub trait LanguageContextProvider: Send + Sync { - fn build_context(&self, location: Location, cx: &mut AppContext) -> Result; -} - -/// A context provider that fills out LanguageContext without inspecting the contents. -pub struct DefaultContextProvider; - -impl LanguageContextProvider for DefaultContextProvider { - fn build_context( - &self, - location: Location, - cx: &mut AppContext, - ) -> gpui::Result { - let symbols = location - .buffer - .read(cx) - .snapshot() - .symbols_containing(location.range.start, None); - let symbol = symbols.and_then(|symbols| { - symbols.last().map(|symbol| { - let range = symbol - .name_ranges - .last() - .cloned() - .unwrap_or(0..symbol.text.len()); - symbol.text[range].to_string() - }) - }); - Ok(LanguageContext { - package: None, - symbol, - }) - } -} - /// Represents a Language Server, with certain cached sync properties. /// Uses [`LspAdapter`] under the hood, but calls all 'static' methods /// once at startup, and caches the results. @@ -777,7 +741,7 @@ pub struct Language { pub(crate) id: LanguageId, pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) context_provider: Option>, + pub(crate) context_provider: Option>, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -892,10 +856,7 @@ impl Language { } } - pub fn with_context_provider( - mut self, - provider: Option>, - ) -> Self { + pub fn with_context_provider(mut self, provider: Option>) -> Self { self.context_provider = provider; self } @@ -1220,7 +1181,7 @@ impl Language { self.config.name.clone() } - pub fn context_provider(&self) -> Option> { + pub fn context_provider(&self) -> Option> { self.context_provider.clone() } diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index ab4189aed6..183c715538 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,6 +1,6 @@ use crate::{ - language_settings::all_language_settings, CachedLspAdapter, File, Language, LanguageConfig, - LanguageContextProvider, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter, + language_settings::all_language_settings, task_context::ContextProvider, CachedLspAdapter, + File, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT, }; use anyhow::{anyhow, Context as _, Result}; @@ -73,9 +73,17 @@ struct AvailableLanguage { name: Arc, grammar: Option>, matcher: LanguageMatcher, - load: Arc Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync>, + load: Arc< + dyn Fn() -> Result<( + LanguageConfig, + LanguageQueries, + Option>, + )> + + 'static + + Send + + Sync, + >, loaded: bool, - context_provider: Option>, } enum AvailableGrammar { @@ -195,8 +203,7 @@ impl LanguageRegistry { config.name.clone(), config.grammar.clone(), config.matcher.clone(), - None, - move || Ok((config.clone(), Default::default())), + move || Ok((config.clone(), Default::default(), None)), ) } @@ -245,8 +252,14 @@ impl LanguageRegistry { name: Arc, grammar_name: Option>, matcher: LanguageMatcher, - context_provider: Option>, - load: impl Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync, + load: impl Fn() -> Result<( + LanguageConfig, + LanguageQueries, + Option>, + )> + + 'static + + Send + + Sync, ) { let load = Arc::new(load); let state = &mut *self.state.write(); @@ -266,8 +279,6 @@ impl LanguageRegistry { grammar: grammar_name, matcher, load, - - context_provider, loaded: false, }); state.version += 1; @@ -333,7 +344,6 @@ impl LanguageRegistry { matcher: language.config.matcher.clone(), load: Arc::new(|| Err(anyhow!("already loaded"))), loaded: true, - context_provider: language.context_provider.clone(), }); state.add(language); } @@ -507,9 +517,8 @@ impl LanguageRegistry { .spawn(async move { let id = language.id; let name = language.name.clone(); - let provider = language.context_provider.clone(); let language = async { - let (config, queries) = (language.load)()?; + let (config, queries, provider) = (language.load)()?; if let Some(grammar) = config.grammar.clone() { let grammar = Some(this.get_or_load_grammar(grammar).await?); diff --git a/crates/language/src/task_context.rs b/crates/language/src/task_context.rs new file mode 100644 index 0000000000..47252e3975 --- /dev/null +++ b/crates/language/src/task_context.rs @@ -0,0 +1,109 @@ +use crate::{LanguageRegistry, Location}; + +use anyhow::Result; +use gpui::{AppContext, Context, Model}; +use std::sync::Arc; +use task::{static_source::tasks_for, static_source::TaskDefinitions, TaskSource, TaskVariables}; + +/// Language Contexts are used by Zed tasks to extract information about source file. +pub trait ContextProvider: Send + Sync { + fn build_context(&self, _: Location, _: &mut AppContext) -> Result { + Ok(TaskVariables::default()) + } + fn associated_tasks(&self) -> Option { + None + } +} + +/// A context provider that finds out what symbol is currently focused in the buffer. +pub struct SymbolContextProvider; + +impl ContextProvider for SymbolContextProvider { + fn build_context( + &self, + location: Location, + cx: &mut AppContext, + ) -> gpui::Result { + let symbols = location + .buffer + .read(cx) + .snapshot() + .symbols_containing(location.range.start, None); + let symbol = symbols.and_then(|symbols| { + symbols.last().map(|symbol| { + let range = symbol + .name_ranges + .last() + .cloned() + .unwrap_or(0..symbol.text.len()); + symbol.text[range].to_string() + }) + }); + Ok(TaskVariables::from_iter( + symbol.map(|symbol| ("ZED_SYMBOL".to_string(), symbol)), + )) + } +} + +/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks. +pub struct ContextProviderWithTasks { + definitions: TaskDefinitions, +} + +impl ContextProviderWithTasks { + pub fn new(definitions: TaskDefinitions) -> Self { + Self { definitions } + } +} + +impl ContextProvider for ContextProviderWithTasks { + fn associated_tasks(&self) -> Option { + Some(self.definitions.clone()) + } + + fn build_context(&self, location: Location, cx: &mut AppContext) -> Result { + SymbolContextProvider.build_context(location, cx) + } +} + +/// A source that pulls in the tasks from language registry. +pub struct LanguageSource { + languages: Arc, +} + +impl LanguageSource { + pub fn new( + languages: Arc, + cx: &mut AppContext, + ) -> Model> { + cx.new_model(|_| Box::new(Self { languages }) as Box<_>) + } +} + +impl TaskSource for LanguageSource { + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn tasks_for_path( + &mut self, + _: Option<&std::path::Path>, + _: &mut gpui::ModelContext>, + ) -> Vec> { + self.languages + .to_vec() + .into_iter() + .filter_map(|language| { + language + .context_provider()? + .associated_tasks() + .map(|tasks| (tasks, language)) + }) + .flat_map(|(tasks, language)| { + let language_name = language.name(); + let id_base = format!("buffer_source_{language_name}"); + tasks_for(tasks, &id_base) + }) + .collect() + } +} diff --git a/crates/languages/src/elixir.rs b/crates/languages/src/elixir.rs index b4479c0fd8..7e122bbb4e 100644 --- a/crates/languages/src/elixir.rs +++ b/crates/languages/src/elixir.rs @@ -18,6 +18,7 @@ use std::{ Arc, }, }; +use task::static_source::{Definition, TaskDefinitions}; use util::{ async_maybe, fs::remove_matching, @@ -535,3 +536,45 @@ fn label_for_symbol_elixir( filter_range: 0..name.len(), }) } + +pub(super) fn elixir_task_context() -> ContextProviderWithTasks { + // Taken from https://gist.github.com/josevalim/2e4f60a14ccd52728e3256571259d493#gistcomment-4995881 + ContextProviderWithTasks::new(TaskDefinitions(vec![ + Definition { + label: "Elixir: test suite".to_owned(), + command: "mix".to_owned(), + args: vec!["test".to_owned()], + ..Default::default() + }, + Definition { + label: "Elixir: failed tests suite".to_owned(), + command: "mix".to_owned(), + args: vec!["test".to_owned(), "--failed".to_owned()], + ..Default::default() + }, + Definition { + label: "Elixir: test file".to_owned(), + command: "mix".to_owned(), + args: vec!["test".to_owned(), "$ZED_FILE".to_owned()], + ..Default::default() + }, + Definition { + label: "Elixir: test at current line".to_owned(), + command: "mix".to_owned(), + args: vec!["test".to_owned(), "$ZED_FILE:$ZED_ROW".to_owned()], + ..Default::default() + }, + Definition { + label: "Elixir: break line".to_owned(), + command: "iex".to_owned(), + args: vec![ + "-S".to_owned(), + "mix".to_owned(), + "test".to_owned(), + "-b".to_owned(), + "$ZED_FILE:$ZED_ROW".to_owned(), + ], + ..Default::default() + }, + ])) +} diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 4eb9560fe9..fab6c3470e 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -52,7 +52,7 @@ impl JsonLspAdapter { }, cx, ); - let tasks_schema = task::static_source::DefinitionProvider::generate_json_schema(); + let tasks_schema = task::static_source::TaskDefinitions::generate_json_schema(); serde_json::json!({ "json": { "format": { diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 88b292617f..2030f602c8 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -7,7 +7,7 @@ use settings::Settings; use std::{str, sync::Arc}; use util::asset_str; -use crate::rust::RustContextProvider; +use crate::{elixir::elixir_task_context, rust::RustContextProvider}; use self::{deno::DenoSettings, elixir::ElixirSettings}; @@ -130,8 +130,13 @@ pub fn init( config.name.clone(), config.grammar.clone(), config.matcher.clone(), - Some(Arc::new(language::DefaultContextProvider)), - move || Ok((config.clone(), load_queries($name))), + move || { + Ok(( + config.clone(), + load_queries($name), + Some(Arc::new(language::SymbolContextProvider)), + )) + }, ); }; ($name:literal, $adapters:expr) => { @@ -145,8 +150,13 @@ pub fn init( config.name.clone(), config.grammar.clone(), config.matcher.clone(), - Some(Arc::new(language::DefaultContextProvider)), - move || Ok((config.clone(), load_queries($name))), + move || { + Ok(( + config.clone(), + load_queries($name), + Some(Arc::new(language::SymbolContextProvider)), + )) + }, ); }; ($name:literal, $adapters:expr, $context_provider:expr) => { @@ -160,8 +170,13 @@ pub fn init( config.name.clone(), config.grammar.clone(), config.matcher.clone(), - Some(Arc::new($context_provider)), - move || Ok((config.clone(), load_queries($name))), + move || { + Ok(( + config.clone(), + load_queries($name), + Some(Arc::new($context_provider)), + )) + }, ); }; } @@ -199,11 +214,16 @@ pub fn init( vec![ Arc::new(elixir::ElixirLspAdapter), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] + ], + elixir_task_context() ); } elixir::ElixirLspSetting::NextLs => { - language!("elixir", vec![Arc::new(elixir::NextLspAdapter)]); + language!( + "elixir", + vec![Arc::new(elixir::NextLspAdapter)], + elixir_task_context() + ); } elixir::ElixirLspSetting::Local { path, arguments } => { language!( @@ -211,7 +231,8 @@ pub fn init( vec![Arc::new(elixir::LocalLspAdapter { path: path.clone(), arguments: arguments.clone(), - })] + })], + elixir_task_context() ); } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 383ebbde53..91785459cf 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -11,6 +11,10 @@ use regex::Regex; use settings::Settings; use smol::fs::{self, File}; use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, sync::Arc}; +use task::{ + static_source::{Definition, TaskDefinitions}, + TaskVariables, +}; use util::{ async_maybe, fs::remove_matching, @@ -319,44 +323,77 @@ impl LspAdapter for RustLspAdapter { pub(crate) struct RustContextProvider; -impl LanguageContextProvider for RustContextProvider { +impl ContextProvider for RustContextProvider { fn build_context( &self, location: Location, cx: &mut gpui::AppContext, - ) -> Result { - let mut context = DefaultContextProvider.build_context(location.clone(), cx)?; - if context.package.is_none() { - if let Some(path) = location.buffer.read(cx).file().and_then(|file| { - let local_file = file.as_local()?.abs_path(cx); - local_file.parent().map(PathBuf::from) - }) { - // src/ - // main.rs - // lib.rs - // foo/ - // bar/ - // baz.rs <|> - // /bin/ - // bin_1.rs - // - let Some(pkgid) = std::process::Command::new("cargo") - .current_dir(path) - .arg("pkgid") - .output() - .log_err() - else { - return Ok(context); - }; - let package_name = String::from_utf8(pkgid.stdout) - .map(|name| name.trim().to_owned()) - .ok(); + ) -> Result { + let mut context = SymbolContextProvider.build_context(location.clone(), cx)?; - context.package = package_name; + if let Some(path) = location.buffer.read(cx).file().and_then(|file| { + let local_file = file.as_local()?.abs_path(cx); + local_file.parent().map(PathBuf::from) + }) { + let Some(pkgid) = std::process::Command::new("cargo") + .current_dir(path) + .arg("pkgid") + .output() + .log_err() + else { + return Ok(context); + }; + let package_name = String::from_utf8(pkgid.stdout) + .map(|name| name.trim().to_owned()) + .ok(); + + if let Some(package_name) = package_name { + context.0.insert("ZED_PACKAGE".to_owned(), package_name); } } + Ok(context) } + fn associated_tasks(&self) -> Option { + Some(TaskDefinitions(vec![ + Definition { + label: "Rust: Test current crate".to_owned(), + command: "cargo".into(), + args: vec!["test".into(), "-p".into(), "$ZED_PACKAGE".into()], + ..Default::default() + }, + Definition { + label: "Rust: Test current function".to_owned(), + command: "cargo".into(), + args: vec![ + "test".into(), + "-p".into(), + "$ZED_PACKAGE".into(), + "--".into(), + "$ZED_SYMBOL".into(), + ], + ..Default::default() + }, + Definition { + label: "Rust: cargo run".into(), + command: "cargo".into(), + args: vec!["run".into()], + ..Default::default() + }, + Definition { + label: "Rust: cargo check current crate".into(), + command: "cargo".into(), + args: vec!["check".into(), "-p".into(), "$ZED_PACKAGE".into()], + ..Default::default() + }, + Definition { + label: "Rust: cargo check workspace".into(), + command: "cargo".into(), + args: vec!["check".into(), "--workspace".into()], + ..Default::default() + }, + ])) + } } async fn get_cached_server_binary(container_dir: PathBuf) -> Option { diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index f157c78756..9501d37a38 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -35,13 +35,15 @@ pub enum TaskSourceKind { AbsPath(PathBuf), /// Worktree-specific task definitions, e.g. dynamic tasks from open worktree file, or tasks from the worktree's .zed/task.json Worktree { id: WorktreeId, abs_path: PathBuf }, + /// Buffer-specific task definitions, originating in e.g. language extension. + Buffer, } impl TaskSourceKind { fn abs_path(&self) -> Option<&Path> { match self { Self::AbsPath(abs_path) | Self::Worktree { abs_path, .. } => Some(abs_path), - Self::UserInput => None, + Self::UserInput | Self::Buffer => None, } } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 44d04ad03e..12b00addf5 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -41,13 +41,26 @@ pub struct SpawnInTerminal { pub reveal: RevealStrategy, } +type VariableName = String; +type VariableValue = String; + +/// Container for predefined environment variables that describe state of Zed at the time the task was spawned. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TaskVariables(pub HashMap); + +impl FromIterator<(String, String)> for TaskVariables { + fn from_iter>(iter: T) -> Self { + Self(HashMap::from_iter(iter)) + } +} + /// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function) #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TaskContext { /// A path to a directory in which the task should be executed. pub cwd: Option, /// Additional environment variables associated with a given task. - pub env: HashMap, + pub task_variables: TaskVariables, } /// Represents a short lived recipe of a task, whose main purpose diff --git a/crates/task/src/oneshot_source.rs b/crates/task/src/oneshot_source.rs index 5e62b0ba75..6f7bb39a88 100644 --- a/crates/task/src/oneshot_source.rs +++ b/crates/task/src/oneshot_source.rs @@ -40,14 +40,17 @@ impl Task for OneshotTask { if self.id().0.is_empty() { return None; } - let TaskContext { cwd, env } = cx; + let TaskContext { + cwd, + task_variables, + } = cx; Some(SpawnInTerminal { id: self.id().clone(), label: self.name().to_owned(), command: self.id().0.clone(), args: vec![], cwd, - env, + env: task_variables.0, use_new_terminal: Default::default(), allow_concurrent_runs: Default::default(), reveal: RevealStrategy::default(), diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 6a02cb5d97..0728d125b4 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -19,17 +19,46 @@ struct StaticTask { definition: Definition, } +impl StaticTask { + fn new(definition: Definition, (id_base, index_in_file): (&str, usize)) -> Arc { + Arc::new(Self { + id: TaskId(format!( + "static_{id_base}_{index_in_file}_{}", + definition.label + )), + definition, + }) + } +} + +/// TODO: doc +pub fn tasks_for(tasks: TaskDefinitions, id_base: &str) -> Vec> { + tasks + .0 + .into_iter() + .enumerate() + .map(|(index, task)| StaticTask::new(task, (id_base, index)) as Arc<_>) + .collect() +} + impl Task for StaticTask { fn exec(&self, cx: TaskContext) -> Option { - let TaskContext { cwd, env } = cx; + let TaskContext { + cwd, + task_variables, + } = cx; let cwd = self .definition .cwd .clone() - .and_then(|path| subst::substitute(&path, &env).map(Into::into).ok()) + .and_then(|path| { + subst::substitute(&path, &task_variables.0) + .map(Into::into) + .ok() + }) .or(cwd); let mut definition_env = self.definition.env.clone(); - definition_env.extend(env); + definition_env.extend(task_variables.0); Some(SpawnInTerminal { id: self.id.clone(), cwd, @@ -58,15 +87,15 @@ impl Task for StaticTask { /// The source of tasks defined in a tasks config file. pub struct StaticSource { - tasks: Vec, - _definitions: Model>, + tasks: Vec>, + _definitions: Model>, _subscription: Subscription, } /// Static task definition from the tasks config file. #[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] -pub(crate) struct Definition { +pub struct Definition { /// Human readable name of the task to display in the UI. pub label: String, /// Executable command to spawn. @@ -106,9 +135,9 @@ pub enum RevealStrategy { /// A group of Tasks defined in a JSON file. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct DefinitionProvider(pub(crate) Vec); +pub struct TaskDefinitions(pub Vec); -impl DefinitionProvider { +impl TaskDefinitions { /// Generates JSON schema of Tasks JSON definition format. pub fn generate_json_schema() -> serde_json_lenient::Value { let schema = SchemaSettings::draft07() @@ -206,7 +235,7 @@ impl StaticSource { /// Initializes the static source, reacting on tasks config changes. pub fn new( id_base: impl Into>, - definitions: Model>, + definitions: Model>, cx: &mut AppContext, ) -> Model> { cx.new_model(|cx| { @@ -222,10 +251,7 @@ impl StaticSource { .clone() .into_iter() .enumerate() - .map(|(i, definition)| StaticTask { - id: TaskId(format!("static_{id_base}_{i}_{}", definition.label)), - definition, - }) + .map(|(i, definition)| StaticTask::new(definition, (&id_base, i))) .collect(); cx.notify(); } @@ -247,9 +273,8 @@ impl TaskSource for StaticSource { _: &mut ModelContext>, ) -> Vec> { self.tasks - .clone() - .into_iter() - .map(|task| Arc::new(task) as Arc) + .iter() + .map(|task| task.clone() as Arc) .collect() } diff --git a/crates/task/src/vscode_format.rs b/crates/task/src/vscode_format.rs index dc6e9657db..ce9632db40 100644 --- a/crates/task/src/vscode_format.rs +++ b/crates/task/src/vscode_format.rs @@ -3,7 +3,7 @@ use collections::HashMap; use serde::Deserialize; use util::ResultExt; -use crate::static_source::{Definition, DefinitionProvider}; +use crate::static_source::{Definition, TaskDefinitions}; #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -124,7 +124,7 @@ pub struct VsCodeTaskFile { tasks: Vec, } -impl TryFrom for DefinitionProvider { +impl TryFrom for TaskDefinitions { type Error = anyhow::Error; fn try_from(value: VsCodeTaskFile) -> Result { @@ -148,7 +148,7 @@ mod tests { use std::collections::HashMap; use crate::{ - static_source::{Definition, DefinitionProvider}, + static_source::{Definition, TaskDefinitions}, vscode_format::{Command, VsCodeTaskDefinition}, VsCodeTaskFile, }; @@ -279,7 +279,7 @@ mod tests { }, ]; - let tasks: DefinitionProvider = vscode_definitions.try_into().unwrap(); + let tasks: TaskDefinitions = vscode_definitions.try_into().unwrap(); assert_eq!(tasks.0, expected); } @@ -380,7 +380,7 @@ mod tests { ..Default::default() }, ]; - let tasks: DefinitionProvider = vscode_definitions.try_into().unwrap(); + let tasks: TaskDefinitions = vscode_definitions.try_into().unwrap(); assert_eq!(tasks.0, expected); } } diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 80861f5657..a6f26d7bc9 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::path::PathBuf; use editor::Editor; use gpui::{AppContext, ViewContext, WindowContext}; use language::Point; use modal::{Spawn, TasksModal}; use project::{Location, WorktreeId}; -use task::{Task, TaskContext}; +use task::{Task, TaskContext, TaskVariables}; use util::ResultExt; use workspace::Workspace; @@ -156,40 +156,37 @@ fn task_context( let selected_text = buffer.read(cx).chars_for_range(selection_range).collect(); - let mut env = HashMap::from_iter([ + let mut task_variables = TaskVariables::from_iter([ ("ZED_ROW".into(), row.to_string()), ("ZED_COLUMN".into(), column.to_string()), ("ZED_SELECTED_TEXT".into(), selected_text), ]); if let Some(path) = current_file { - env.insert("ZED_FILE".into(), path); + task_variables.0.insert("ZED_FILE".into(), path); } if let Some(worktree_path) = worktree_path { - env.insert("ZED_WORKTREE_ROOT".into(), worktree_path); + task_variables + .0 + .insert("ZED_WORKTREE_ROOT".into(), worktree_path); } if let Some(language_context) = context { - if let Some(symbol) = language_context.symbol { - env.insert("ZED_SYMBOL".into(), symbol); - } - if let Some(symbol) = language_context.package { - env.insert("ZED_PACKAGE".into(), symbol); - } + task_variables.0.extend(language_context.0); } Some(TaskContext { cwd: cwd.clone(), - env, + task_variables, }) }) })() .unwrap_or_else(|| TaskContext { cwd, - env: Default::default(), + task_variables: Default::default(), }) } else { TaskContext { cwd, - env: Default::default(), + task_variables: Default::default(), } } } @@ -248,14 +245,14 @@ fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result, - candidates: Vec<(TaskSourceKind, Arc)>, + candidates: Option)>>, matches: Vec, selected_index: usize, workspace: WeakView, @@ -62,7 +62,7 @@ impl TasksModalDelegate { Self { inventory, workspace, - candidates: Vec::new(), + candidates: None, matches: Vec::new(), selected_index: 0, prompt: String::default(), @@ -84,10 +84,10 @@ impl TasksModalDelegate { } fn active_item_path( - &mut self, + workspace: &WeakView, cx: &mut ViewContext<'_, Picker>, ) -> Option<(PathBuf, ProjectPath)> { - let workspace = self.workspace.upgrade()?.read(cx); + let workspace = workspace.upgrade()?.read(cx); let project = workspace.project().read(cx); let active_item = workspace.active_item(cx)?; active_item.project_path(cx).and_then(|project_path| { @@ -183,19 +183,20 @@ impl PickerDelegate for TasksModalDelegate { cx.spawn(move |picker, mut cx| async move { let Some(candidates) = picker .update(&mut cx, |picker, cx| { - let (path, worktree) = match picker.delegate.active_item_path(cx) { - Some((abs_path, project_path)) => { - (Some(abs_path), Some(project_path.worktree_id)) - } - None => (None, None), - }; - picker.delegate.candidates = + let candidates = picker.delegate.candidates.get_or_insert_with(|| { + let (path, worktree) = + match Self::active_item_path(&picker.delegate.workspace, cx) { + Some((abs_path, project_path)) => { + (Some(abs_path), Some(project_path.worktree_id)) + } + None => (None, None), + }; picker.delegate.inventory.update(cx, |inventory, cx| { inventory.list_tasks(path.as_deref(), worktree, true, cx) - }); - picker - .delegate - .candidates + }) + }); + + candidates .iter() .enumerate() .map(|(index, (_, candidate))| StringMatchCandidate { @@ -244,10 +245,14 @@ impl PickerDelegate for TasksModalDelegate { None } } else { - self.matches.get(current_match_index).map(|current_match| { - let ix = current_match.candidate_id; - self.candidates[ix].1.clone() - }) + self.matches + .get(current_match_index) + .and_then(|current_match| { + let ix = current_match.candidate_id; + self.candidates + .as_ref() + .map(|candidates| candidates[ix].1.clone()) + }) }; let Some(task) = task else { @@ -272,10 +277,12 @@ impl PickerDelegate for TasksModalDelegate { selected: bool, cx: &mut ViewContext>, ) -> Option { + let candidates = self.candidates.as_ref()?; let hit = &self.matches[ix]; - let (source_kind, _) = &self.candidates[hit.candidate_id]; + let (source_kind, _) = &candidates[hit.candidate_id]; let details = match source_kind { TaskSourceKind::UserInput => "user input".to_string(), + TaskSourceKind::Buffer => "language extension".to_string(), TaskSourceKind::Worktree { abs_path, .. } | TaskSourceKind::AbsPath(abs_path) => { abs_path.compact().to_string_lossy().to_string() } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9eb221e450..10440242c0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -18,6 +18,7 @@ pub use open_listener::*; use anyhow::Context as _; use assets::Assets; use futures::{channel::mpsc, select_biased, StreamExt}; +use language::LanguageSource; use project::TaskSourceKind; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; @@ -33,6 +34,7 @@ use task::{ oneshot_source::OneshotSource, static_source::{StaticSource, TrackedFile}, }; + use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{ asset_str, @@ -177,6 +179,11 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }, cx, ); + inventory.add_source( + TaskSourceKind::Buffer, + |cx| LanguageSource::new(app_state.languages.clone(), cx), + cx, + ); }) }); }