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, + ); }) }); }