diff --git a/Cargo.lock b/Cargo.lock index 37684379a94d379f1badc20faa8642af96dcd86f..47cf6cfed628ececc6f2b40b185920e9c264b587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9467,6 +9467,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "subst" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca1318e5d6716d6541696727c88d9b8dfc8cfe6afd6908e186546fd4af7f5b98" +dependencies = [ + "memchr", + "unicode-width", +] + [[package]] name = "subtle" version = "2.5.0" @@ -9698,6 +9708,7 @@ dependencies = [ "schemars", "serde", "serde_json_lenient", + "subst", "util", ] @@ -9716,6 +9727,8 @@ dependencies = [ "serde", "serde_json", "task", + "tree-sitter-rust", + "tree-sitter-typescript", "ui", "util", "workspace", diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 55fd214e3e00ff49128faf40f7cab75f8c34fb5b..e80ac6f5a4a26a9efd216fe90ca30c176dcd6143 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -555,6 +555,7 @@ 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)?; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 38d64ccf0c190e7ee569f6ab83bf94502b93d89f..ee137abe4357fc715fa628af346eb032943ab2a1 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -120,6 +120,46 @@ 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. @@ -727,6 +767,7 @@ pub struct Language { pub(crate) id: LanguageId, pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, + pub(crate) context_provider: Option>, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -841,9 +882,18 @@ impl Language { highlight_map: Default::default(), }) }), + context_provider: None, } } + pub fn with_context_provider( + mut self, + provider: Option>, + ) -> Self { + self.context_provider = provider; + self + } + pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(query) = queries.highlights { self = self @@ -1139,6 +1189,10 @@ impl Language { self.config.name.clone() } + pub fn context_provider(&self) -> Option> { + self.context_provider.clone() + } + pub fn highlight_text<'a>( self: &'a Arc, text: &'a Rope, diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 8bc29f943f7fd82b21cd562c36e21a24652a4300..d32b0f3346d0cd6a140e1239f54a729cdbe9620b 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,6 +1,6 @@ use crate::{ - CachedLspAdapter, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, - LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT, + CachedLspAdapter, Language, LanguageConfig, LanguageContextProvider, LanguageId, + LanguageMatcher, LanguageServerName, LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT, }; use anyhow::{anyhow, Context as _, Result}; use collections::{hash_map, HashMap}; @@ -78,6 +78,7 @@ struct AvailableLanguage { matcher: LanguageMatcher, load: Arc Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync>, loaded: bool, + context_provider: Option>, } enum AvailableGrammar { @@ -188,6 +189,7 @@ impl LanguageRegistry { config.name.clone(), config.grammar.clone(), config.matcher.clone(), + None, move || Ok((config.clone(), Default::default())), ) } @@ -237,6 +239,7 @@ impl LanguageRegistry { name: Arc, grammar_name: Option>, matcher: LanguageMatcher, + context_provider: Option>, load: impl Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync, ) { let load = Arc::new(load); @@ -257,6 +260,8 @@ impl LanguageRegistry { grammar: grammar_name, matcher, load, + + context_provider, loaded: false, }); state.version += 1; @@ -422,6 +427,7 @@ 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)()?; @@ -431,7 +437,9 @@ impl LanguageRegistry { None }; - Language::new_with_id(id, config, grammar).with_queries(queries) + Language::new_with_id(id, config, grammar) + .with_context_provider(provider) + .with_queries(queries) } .await; diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index f47bf33214fe886ed70446a56a2c39425a08e8c4..6aef9f6d16ebcb9c2d90ba576d47fed3da927031 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -122,212 +122,245 @@ pub fn init( ("dart", tree_sitter_dart::language()), ]); - let language = |asset_dir_name: &'static str, adapters: Vec>| { - let config = load_config(asset_dir_name); - for adapter in adapters { - languages.register_lsp_adapter(config.name.clone(), adapter); - } - languages.register_language( - config.name.clone(), - config.grammar.clone(), - config.matcher.clone(), - move || Ok((config.clone(), load_queries(asset_dir_name))), - ); - }; - - language( + macro_rules! language { + ($name:literal) => { + let config = load_config($name); + languages.register_language( + config.name.clone(), + config.grammar.clone(), + config.matcher.clone(), + Some(Arc::new(language::DefaultContextProvider)), + move || Ok((config.clone(), load_queries($name))), + ); + }; + ($name:literal, $adapters:expr) => { + let config = load_config($name); + // typeck helper + let adapters: Vec> = $adapters; + for adapter in adapters { + languages.register_lsp_adapter(config.name.clone(), adapter); + } + languages.register_language( + config.name.clone(), + config.grammar.clone(), + config.matcher.clone(), + Some(Arc::new(language::DefaultContextProvider)), + move || Ok((config.clone(), load_queries($name))), + ); + }; + ($name:literal, $adapters:expr, $context_provider:expr) => { + let config = load_config($name); + // typeck helper + let adapters: Vec> = $adapters; + for adapter in $adapters { + languages.register_lsp_adapter(config.name.clone(), adapter); + } + languages.register_language( + config.name.clone(), + config.grammar.clone(), + config.matcher.clone(), + Some(Arc::new($context_provider)), + move || Ok((config.clone(), load_queries($name))), + ); + }; + } + language!( "astro", vec![ Arc::new(astro::AstroLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("bash", vec![]); - language("c", vec![Arc::new(c::CLspAdapter) as Arc]); - language("clojure", vec![Arc::new(clojure::ClojureLspAdapter)]); - language("cpp", vec![Arc::new(c::CLspAdapter)]); - language("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]); - language( + language!("bash"); + language!("c", vec![Arc::new(c::CLspAdapter) as Arc]); + language!("clojure", vec![Arc::new(clojure::ClojureLspAdapter)]); + language!("cpp", vec![Arc::new(c::CLspAdapter)]); + language!("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]); + language!( "css", vec![ Arc::new(css::CssLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "dockerfile", vec![Arc::new(dockerfile::DockerfileLspAdapter::new( node_runtime.clone(), - ))], + ))] ); match &ElixirSettings::get(None, cx).lsp { - elixir::ElixirLspSetting::ElixirLs => language( - "elixir", - vec![ - Arc::new(elixir::ElixirLspAdapter), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], - ), + elixir::ElixirLspSetting::ElixirLs => { + language!( + "elixir", + vec![ + Arc::new(elixir::ElixirLspAdapter), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ] + ); + } elixir::ElixirLspSetting::NextLs => { - language("elixir", vec![Arc::new(elixir::NextLspAdapter)]) + language!("elixir", vec![Arc::new(elixir::NextLspAdapter)]); + } + elixir::ElixirLspSetting::Local { path, arguments } => { + language!( + "elixir", + vec![Arc::new(elixir::LocalLspAdapter { + path: path.clone(), + arguments: arguments.clone(), + })] + ); } - elixir::ElixirLspSetting::Local { path, arguments } => language( - "elixir", - vec![Arc::new(elixir::LocalLspAdapter { - path: path.clone(), - arguments: arguments.clone(), - })], - ), } - language("gitcommit", vec![]); - language("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]); + language!("gitcommit"); + language!("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]); - language("gleam", vec![Arc::new(gleam::GleamLspAdapter)]); - language("go", vec![Arc::new(go::GoLspAdapter)]); - language("gomod", vec![]); - language("gowork", vec![]); - language("zig", vec![Arc::new(zig::ZlsAdapter)]); - language( + language!("gleam", vec![Arc::new(gleam::GleamLspAdapter)]); + language!("go", vec![Arc::new(go::GoLspAdapter)]); + language!("gomod"); + language!("gowork"); + language!("zig", vec![Arc::new(zig::ZlsAdapter)]); + language!( "heex", vec![ Arc::new(elixir::ElixirLspAdapter), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "json", vec![Arc::new(json::JsonLspAdapter::new( node_runtime.clone(), languages.clone(), - ))], + ))] ); - language("markdown", vec![]); - language( + language!("markdown"); + language!( "python", vec![Arc::new(python::PythonLspAdapter::new( node_runtime.clone(), - ))], + ))] ); - language("rust", vec![Arc::new(rust::RustLspAdapter)]); - language("toml", vec![Arc::new(toml::TaploLspAdapter)]); + language!("rust", vec![Arc::new(rust::RustLspAdapter)]); + language!("toml", vec![Arc::new(toml::TaploLspAdapter)]); match &DenoSettings::get(None, cx).enable { true => { - language( + language!( "tsx", vec![ Arc::new(deno::DenoLspAdapter::new()), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]); - language( + language!("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]); + language!( "javascript", vec![ Arc::new(deno::DenoLspAdapter::new()), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); } false => { - language( + language!( "tsx", vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "typescript", vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "javascript", vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); } } - language("haskell", vec![Arc::new(haskell::HaskellLanguageServer {})]); - language( + language!("haskell", vec![Arc::new(haskell::HaskellLanguageServer {})]); + language!( "html", vec![ Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); - language( + language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); + language!( "erb", vec![ Arc::new(ruby::RubyLanguageServer), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("scheme", vec![]); - language("racket", vec![]); - language("lua", vec![Arc::new(lua::LuaLspAdapter)]); - language( + language!("scheme"); + language!("racket"); + language!("lua", vec![Arc::new(lua::LuaLspAdapter)]); + language!( "yaml", - vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))], + vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))] ); - language( + language!( "svelte", vec![ Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "php", vec![ Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "purescript", vec![Arc::new(purescript::PurescriptLspAdapter::new( node_runtime.clone(), - ))], + ))] ); - language( + language!( "elm", - vec![Arc::new(elm::ElmLspAdapter::new(node_runtime.clone()))], + vec![Arc::new(elm::ElmLspAdapter::new(node_runtime.clone()))] ); - language("glsl", vec![]); - language("nix", vec![]); - language("nu", vec![Arc::new(nu::NuLanguageServer {})]); - language("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]); - language("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]); - language( + language!("glsl"); + language!("nix"); + language!("nu", vec![Arc::new(nu::NuLanguageServer {})]); + language!("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]); + language!("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]); + language!( "vue", - vec![Arc::new(vue::VueLspAdapter::new(node_runtime.clone()))], + vec![Arc::new(vue::VueLspAdapter::new(node_runtime.clone()))] ); - language("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]); - language("proto", vec![]); - language("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]); - language( + language!("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]); + language!("proto"); + language!("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]); + language!( "terraform-vars", - vec![Arc::new(terraform::TerraformLspAdapter)], + vec![Arc::new(terraform::TerraformLspAdapter)] ); - language("hcl", vec![]); - language( + language!("hcl", vec![]); + language!( "prisma", vec![Arc::new(prisma::PrismaLspAdapter::new( node_runtime.clone(), - ))], + ))] ); - language("dart", vec![Arc::new(dart::DartLanguageServer {})]); + language!("dart", vec![Arc::new(dart::DartLanguageServer {})]); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 54b3e80a8be3cf2d051f9e4d381da7d4930a72a5..b51cdf2ba498df537e287d96f2a68cd2a1a525d9 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -10,13 +10,13 @@ use collections::{HashMap, VecDeque}; use gpui::{AppContext, Context, Model, ModelContext, Subscription}; use itertools::Itertools; use project_core::worktree::WorktreeId; -use task::{Task, TaskId, TaskSource}; +use task::{Task, TaskContext, TaskId, TaskSource}; use util::{post_inc, NumericPrefixWithSuffix}; /// Inventory tracks available tasks for a given project. pub struct Inventory { sources: Vec, - last_scheduled_tasks: VecDeque, + last_scheduled_tasks: VecDeque<(TaskId, TaskContext)>, } struct SourceInInventory { @@ -133,17 +133,20 @@ impl Inventory { ) -> Vec<(TaskSourceKind, Arc)> { let mut lru_score = 0_u32; let tasks_by_usage = if lru { - self.last_scheduled_tasks - .iter() - .rev() - .fold(HashMap::default(), |mut tasks, id| { - tasks.entry(id).or_insert_with(|| post_inc(&mut lru_score)); + self.last_scheduled_tasks.iter().rev().fold( + HashMap::default(), + |mut tasks, (id, context)| { tasks - }) + .entry(id) + .or_insert_with(|| (post_inc(&mut lru_score), Some(context))); + tasks + }, + ) } else { HashMap::default() }; - let not_used_score = post_inc(&mut lru_score); + let not_used_task_context = None; + let not_used_score = (post_inc(&mut lru_score), not_used_task_context); self.sources .iter() .filter(|source| { @@ -171,7 +174,8 @@ impl Inventory { .sorted_unstable_by( |((kind_a, task_a), usages_a), ((kind_b, task_b), usages_b)| { usages_a - .cmp(usages_b) + .0 + .cmp(&usages_b.0) .then( kind_a .worktree() @@ -200,19 +204,21 @@ impl Inventory { } /// Returns the last scheduled task, if any of the sources contains one with the matching id. - pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option> { - self.last_scheduled_tasks.back().and_then(|id| { - // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future. - self.list_tasks(None, None, false, cx) - .into_iter() - .find(|(_, task)| task.id() == id) - .map(|(_, task)| task) - }) + pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<(Arc, TaskContext)> { + self.last_scheduled_tasks + .back() + .and_then(|(id, task_context)| { + // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future. + self.list_tasks(None, None, false, cx) + .into_iter() + .find(|(_, task)| task.id() == id) + .map(|(_, task)| (task, task_context.clone())) + }) } /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks. - pub fn task_scheduled(&mut self, id: TaskId) { - self.last_scheduled_tasks.push_back(id); + pub fn task_scheduled(&mut self, id: TaskId, task_context: TaskContext) { + self.last_scheduled_tasks.push_back((id, task_context)); if self.last_scheduled_tasks.len() > 5_000 { self.last_scheduled_tasks.pop_front(); } @@ -221,14 +227,11 @@ impl Inventory { #[cfg(any(test, feature = "test-support"))] pub mod test_inventory { - use std::{ - path::{Path, PathBuf}, - sync::Arc, - }; + use std::{path::Path, sync::Arc}; use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext}; use project_core::worktree::WorktreeId; - use task::{Task, TaskId, TaskSource}; + use task::{Task, TaskContext, TaskId, TaskSource}; use crate::Inventory; @@ -249,11 +252,11 @@ pub mod test_inventory { &self.name } - fn cwd(&self) -> Option<&Path> { + fn cwd(&self) -> Option<&str> { None } - fn exec(&self, _cwd: Option) -> Option { + fn exec(&self, _cwd: TaskContext) -> Option { None } } @@ -327,7 +330,7 @@ pub mod test_inventory { .into_iter() .find(|(_, task)| task.name() == task_name) .unwrap_or_else(|| panic!("Failed to find task with name {task_name}")); - inventory.task_scheduled(task.1.id().clone()); + inventory.task_scheduled(task.1.id().clone(), TaskContext::default()); }); } diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index aeafaba3f0ecbe601061bc5c40f80ae12bc10c69..47f3170c7436e5e002cca17c317c277395e0289d 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -13,6 +13,7 @@ gpui.workspace = true schemars.workspace = true serde.workspace = true serde_json_lenient.workspace = true +subst = "0.3.0" util.workspace = true [dev-dependencies] diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 66e4249c7e8e214dbe458a4fb76fccf02101e536..37107e2569a83f60d307c32252ad7ccf0e1abf05 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -36,6 +36,15 @@ pub struct SpawnInTerminal { pub allow_concurrent_runs: bool, } +/// 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, +} + /// Represents a short lived recipe of a task, whose main purpose /// is to get spawned. pub trait Task { @@ -44,10 +53,10 @@ pub trait Task { /// Human readable name of the task to display in the UI. fn name(&self) -> &str; /// Task's current working directory. If `None`, current project's root will be used. - fn cwd(&self) -> Option<&Path>; + fn cwd(&self) -> Option<&str>; /// Sets up everything needed to spawn the task in the given directory (`cwd`). /// If a task is intended to be spawned in the terminal, it should return the corresponding struct filled with the data necessary. - fn exec(&self, cwd: Option) -> Option; + fn exec(&self, cx: TaskContext) -> Option; } /// [`Source`] produces tasks that can be scheduled. diff --git a/crates/task/src/oneshot_source.rs b/crates/task/src/oneshot_source.rs index bda14f894e8f5fd2ef773cd45381c85a10bf5904..85257bee547f5824c7f96ba32b828a2ee764c890 100644 --- a/crates/task/src/oneshot_source.rs +++ b/crates/task/src/oneshot_source.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use crate::{SpawnInTerminal, Task, TaskId, TaskSource}; +use crate::{SpawnInTerminal, Task, TaskContext, TaskId, TaskSource}; use gpui::{AppContext, Context, Model}; /// A storage and source of tasks generated out of user command prompt inputs. @@ -30,21 +30,22 @@ impl Task for OneshotTask { &self.id.0 } - fn cwd(&self) -> Option<&std::path::Path> { + fn cwd(&self) -> Option<&str> { None } - fn exec(&self, cwd: Option) -> Option { + fn exec(&self, cx: TaskContext) -> Option { if self.id().0.is_empty() { return None; } + let TaskContext { cwd, env } = cx; Some(SpawnInTerminal { id: self.id().clone(), label: self.name().to_owned(), command: self.id().0.clone(), args: vec![], cwd, - env: Default::default(), + env, use_new_terminal: Default::default(), allow_concurrent_runs: Default::default(), }) diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 9b022fdb0f61c7a3766b3151473fe180421b44e9..dedf5a6384e0aca41ae997befde6e0f416f1503c 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -1,10 +1,6 @@ //! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file. -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{borrow::Cow, path::Path, sync::Arc}; use collections::HashMap; use futures::StreamExt; @@ -13,7 +9,7 @@ use schemars::{gen::SchemaSettings, JsonSchema}; use serde::{Deserialize, Serialize}; use util::ResultExt; -use crate::{SpawnInTerminal, Task, TaskId, TaskSource}; +use crate::{SpawnInTerminal, Task, TaskContext, TaskId, TaskSource}; use futures::channel::mpsc::UnboundedReceiver; /// A single config file entry with the deserialized task definition. @@ -24,7 +20,16 @@ struct StaticTask { } impl Task for StaticTask { - fn exec(&self, cwd: Option) -> Option { + fn exec(&self, cx: TaskContext) -> Option { + let TaskContext { cwd, env } = cx; + let cwd = self + .definition + .cwd + .clone() + .and_then(|path| subst::substitute(&path, &env).map(Into::into).ok()) + .or(cwd); + let mut definition_env = self.definition.env.clone(); + definition_env.extend(env); Some(SpawnInTerminal { id: self.id.clone(), cwd, @@ -33,7 +38,7 @@ impl Task for StaticTask { label: self.definition.label.clone(), command: self.definition.command.clone(), args: self.definition.args.clone(), - env: self.definition.env.clone(), + env: definition_env, }) } @@ -45,7 +50,7 @@ impl Task for StaticTask { &self.id } - fn cwd(&self) -> Option<&Path> { + fn cwd(&self) -> Option<&str> { self.definition.cwd.as_deref() } } @@ -72,7 +77,7 @@ pub(crate) struct Definition { pub env: HashMap, /// Current working directory to spawn the command into, defaults to current project root. #[serde(default)] - pub cwd: Option, + pub cwd: Option, /// Whether to use a new terminal tab or reuse the existing one to spawn the process. #[serde(default)] pub use_new_terminal: bool, diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index 6c350ff931241a8e3c51aa403e24e75f06e44279..cbf5280ef6ff631efd6ade072ae350c23f3f3068 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -7,6 +7,7 @@ license = "GPL-3.0-or-later" [dependencies] anyhow.workspace = true +editor.workspace = true fuzzy.workspace = true gpui.workspace = true menu.workspace = true @@ -17,10 +18,14 @@ serde.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +language.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } serde_json.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-typescript.workspace = true workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 5f517fdedf522fd5a2ad4bd1affb0cd2448d62fe..278d01ef398cced783d882a8f31ca8c038133179 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -1,8 +1,11 @@ -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; +use editor::Editor; use gpui::{AppContext, ViewContext, WindowContext}; +use language::Point; use modal::TasksModal; -use task::Task; +use project::{Location, WorktreeId}; +use task::{Task, TaskContext}; use util::ResultExt; use workspace::Workspace; @@ -15,16 +18,28 @@ pub fn init(cx: &mut AppContext) { .register_action(|workspace, _: &modal::Spawn, cx| { let inventory = workspace.project().read(cx).task_inventory().clone(); let workspace_handle = workspace.weak_handle(); - workspace - .toggle_modal(cx, |cx| TasksModal::new(inventory, workspace_handle, cx)) + let cwd = task_cwd(workspace, cx).log_err().flatten(); + let task_context = task_context(workspace, cwd, cx); + workspace.toggle_modal(cx, |cx| { + TasksModal::new(inventory, task_context, workspace_handle, cx) + }) }) - .register_action(move |workspace, _: &modal::Rerun, cx| { - if let Some(task) = workspace.project().update(cx, |project, cx| { - project - .task_inventory() - .update(cx, |inventory, cx| inventory.last_scheduled_task(cx)) - }) { - schedule_task(workspace, task.as_ref(), cx) + .register_action(move |workspace, action: &modal::Rerun, cx| { + if let Some((task, old_context)) = + workspace.project().update(cx, |project, cx| { + project + .task_inventory() + .update(cx, |inventory, cx| inventory.last_scheduled_task(cx)) + }) + { + let task_context = if action.reevaluate_context { + let cwd = task_cwd(workspace, cx).log_err().flatten(); + task_context(workspace, cwd, cx) + } else { + old_context + }; + + schedule_task(workspace, task.as_ref(), task_context, cx) }; }); }, @@ -32,16 +47,117 @@ pub fn init(cx: &mut AppContext) { .detach(); } -fn schedule_task(workspace: &Workspace, task: &dyn Task, cx: &mut ViewContext<'_, Workspace>) { - let cwd = match task.cwd() { - Some(cwd) => Some(cwd.to_path_buf()), - None => task_cwd(workspace, cx).log_err().flatten(), - }; - let spawn_in_terminal = task.exec(cwd); +fn task_context( + workspace: &Workspace, + cwd: Option, + cx: &mut WindowContext<'_>, +) -> TaskContext { + let current_editor = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + .clone(); + if let Some(current_editor) = current_editor { + (|| { + let editor = current_editor.read(cx); + let selection = editor.selections.newest::(cx); + let (buffer, _, _) = editor + .buffer() + .read(cx) + .point_to_buffer_offset(selection.start, cx)?; + + current_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let selection_range = selection.range(); + let start = snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(selection_range.start) + .text_anchor; + let end = snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(selection_range.end) + .text_anchor; + let Point { row, column } = snapshot + .display_snapshot + .buffer_snapshot + .offset_to_point(selection_range.start); + let row = row + 1; + let column = column + 1; + let location = Location { + buffer: buffer.clone(), + range: start..end, + }; + + let current_file = location + .buffer + .read(cx) + .file() + .map(|file| file.path().to_string_lossy().to_string()); + let worktree_id = location + .buffer + .read(cx) + .file() + .map(|file| WorktreeId::from_usize(file.worktree_id())); + let context = buffer + .read(cx) + .language() + .and_then(|language| language.context_provider()) + .and_then(|provider| provider.build_context(location, cx).ok()); + + let worktree_path = worktree_id.and_then(|worktree_id| { + workspace + .project() + .read(cx) + .worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string()) + }); + + let mut env = HashMap::from_iter([ + ("ZED_ROW".into(), row.to_string()), + ("ZED_COLUMN".into(), column.to_string()), + ]); + if let Some(path) = current_file { + env.insert("ZED_FILE".into(), path); + } + if let Some(worktree_path) = worktree_path { + env.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); + } + } + + Some(TaskContext { + cwd: cwd.clone(), + env, + }) + }) + })() + .unwrap_or_else(|| TaskContext { + cwd, + env: Default::default(), + }) + } else { + TaskContext { + cwd, + env: Default::default(), + } + } +} + +fn schedule_task( + workspace: &Workspace, + task: &dyn Task, + task_cx: TaskContext, + cx: &mut ViewContext<'_, Workspace>, +) { + let spawn_in_terminal = task.exec(task_cx.clone()); if let Some(spawn_in_terminal) = spawn_in_terminal { workspace.project().update(cx, |project, cx| { project.task_inventory().update(cx, |inventory, _| { - inventory.task_scheduled(task.id().clone()); + inventory.task_scheduled(task.id().clone(), task_cx); }) }); cx.emit(workspace::Event::SpawnTask(spawn_in_terminal)); @@ -82,3 +198,176 @@ fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result Arc { + cx.update(|cx| { + let state = AppState::test(cx); + language::init(cx); + crate::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state + }) + } +} diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index dc308de1b2062307833699b160836b80a26a4598..491ed05971bbd95a15447a3a3089fff77b2d61bc 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -2,23 +2,36 @@ use std::{path::PathBuf, sync::Arc}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement, - Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext, - VisualContext, WeakView, + actions, impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, + InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, View, + ViewContext, VisualContext, WeakView, }; use picker::{ highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, Picker, PickerDelegate, }; use project::{Inventory, ProjectPath, TaskSourceKind}; -use task::{oneshot_source::OneshotSource, Task}; +use task::{oneshot_source::OneshotSource, Task, TaskContext}; use ui::{v_flex, ListItem, ListItemSpacing, RenderOnce, Selectable, WindowContext}; use util::{paths::PathExt, ResultExt}; use workspace::{ModalView, Workspace}; use crate::schedule_task; +use serde::Deserialize; +actions!(task, [Spawn]); + +/// Rerun last task +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct Rerun { + #[serde(default)] + /// Controls whether the task context is reevaluated prior to execution of a task. + /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task + /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. + /// default: false + pub reevaluate_context: bool, +} -actions!(task, [Spawn, Rerun]); +impl_actions!(task, [Rerun]); /// A modal used to spawn new tasks. pub(crate) struct TasksModalDelegate { @@ -28,10 +41,15 @@ pub(crate) struct TasksModalDelegate { selected_index: usize, workspace: WeakView, prompt: String, + task_context: TaskContext, } impl TasksModalDelegate { - fn new(inventory: Model, workspace: WeakView) -> Self { + fn new( + inventory: Model, + task_context: TaskContext, + workspace: WeakView, + ) -> Self { Self { inventory, workspace, @@ -39,6 +57,7 @@ impl TasksModalDelegate { matches: Vec::new(), selected_index: 0, prompt: String::default(), + task_context, } } @@ -79,11 +98,16 @@ pub(crate) struct TasksModal { impl TasksModal { pub(crate) fn new( inventory: Model, + task_context: TaskContext, workspace: WeakView, cx: &mut ViewContext, ) -> Self { - let picker = cx - .new_view(|cx| Picker::uniform_list(TasksModalDelegate::new(inventory, workspace), cx)); + let picker = cx.new_view(|cx| { + Picker::uniform_list( + TasksModalDelegate::new(inventory, task_context, workspace), + cx, + ) + }); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { cx.emit(DismissEvent); }); @@ -223,7 +247,7 @@ impl PickerDelegate for TasksModalDelegate { self.workspace .update(cx, |workspace, cx| { - schedule_task(workspace, task.as_ref(), cx); + schedule_task(workspace, task.as_ref(), self.task_context.clone(), cx); }) .ok(); cx.emit(DismissEvent); @@ -279,13 +303,12 @@ mod tests { use gpui::{TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; use serde_json::json; - use workspace::AppState; use super::*; #[gpui::test] async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) { - init_test(cx); + crate::tests::init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -431,16 +454,4 @@ mod tests { .collect::>() }) } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - language::init(cx); - crate::init(cx); - editor::init(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - state - }) - } }