Detailed changes
@@ -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",
@@ -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
@@ -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))
},
);
}
@@ -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"] }
@@ -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<Anchor>,
}
-pub struct LanguageContext {
- pub package: Option<String>,
- pub symbol: Option<String>,
-}
-
-pub trait LanguageContextProvider: Send + Sync {
- fn build_context(&self, location: Location, cx: &mut AppContext) -> Result<LanguageContext>;
-}
-
-/// 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<LanguageContext> {
- 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<Arc<Grammar>>,
- pub(crate) context_provider: Option<Arc<dyn LanguageContextProvider>>,
+ pub(crate) context_provider: Option<Arc<dyn ContextProvider>>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
@@ -892,10 +856,7 @@ impl Language {
}
}
- pub fn with_context_provider(
- mut self,
- provider: Option<Arc<dyn LanguageContextProvider>>,
- ) -> Self {
+ pub fn with_context_provider(mut self, provider: Option<Arc<dyn ContextProvider>>) -> Self {
self.context_provider = provider;
self
}
@@ -1220,7 +1181,7 @@ impl Language {
self.config.name.clone()
}
- pub fn context_provider(&self) -> Option<Arc<dyn LanguageContextProvider>> {
+ pub fn context_provider(&self) -> Option<Arc<dyn ContextProvider>> {
self.context_provider.clone()
}
@@ -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<str>,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
- load: Arc<dyn Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync>,
+ load: Arc<
+ dyn Fn() -> Result<(
+ LanguageConfig,
+ LanguageQueries,
+ Option<Arc<dyn ContextProvider>>,
+ )>
+ + 'static
+ + Send
+ + Sync,
+ >,
loaded: bool,
- context_provider: Option<Arc<dyn LanguageContextProvider>>,
}
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<str>,
grammar_name: Option<Arc<str>>,
matcher: LanguageMatcher,
- context_provider: Option<Arc<dyn LanguageContextProvider>>,
- load: impl Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync,
+ load: impl Fn() -> Result<(
+ LanguageConfig,
+ LanguageQueries,
+ Option<Arc<dyn ContextProvider>>,
+ )>
+ + '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?);
@@ -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<TaskVariables> {
+ Ok(TaskVariables::default())
+ }
+ fn associated_tasks(&self) -> Option<TaskDefinitions> {
+ 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<TaskVariables> {
+ 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<TaskDefinitions> {
+ Some(self.definitions.clone())
+ }
+
+ fn build_context(&self, location: Location, cx: &mut AppContext) -> Result<TaskVariables> {
+ SymbolContextProvider.build_context(location, cx)
+ }
+}
+
+/// A source that pulls in the tasks from language registry.
+pub struct LanguageSource {
+ languages: Arc<LanguageRegistry>,
+}
+
+impl LanguageSource {
+ pub fn new(
+ languages: Arc<LanguageRegistry>,
+ cx: &mut AppContext,
+ ) -> Model<Box<dyn TaskSource>> {
+ 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<Box<dyn TaskSource>>,
+ ) -> Vec<Arc<dyn task::Task>> {
+ 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()
+ }
+}
@@ -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()
+ },
+ ]))
+}
@@ -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": {
@@ -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()
);
}
}
@@ -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<LanguageContext> {
- 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();
-
- context.package = package_name;
+ ) -> Result<TaskVariables> {
+ let mut context = SymbolContextProvider.build_context(location.clone(), cx)?;
+
+ 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<TaskDefinitions> {
+ 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<LanguageServerBinary> {
@@ -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,
}
}
@@ -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<VariableName, VariableValue>);
+
+impl FromIterator<(String, String)> for TaskVariables {
+ fn from_iter<T: IntoIterator<Item = (String, String)>>(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<PathBuf>,
/// Additional environment variables associated with a given task.
- pub env: HashMap<String, String>,
+ pub task_variables: TaskVariables,
}
/// Represents a short lived recipe of a task, whose main purpose
@@ -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(),
@@ -19,17 +19,46 @@ struct StaticTask {
definition: Definition,
}
+impl StaticTask {
+ fn new(definition: Definition, (id_base, index_in_file): (&str, usize)) -> Arc<Self> {
+ 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<Arc<dyn Task>> {
+ 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<SpawnInTerminal> {
- 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<StaticTask>,
- _definitions: Model<TrackedFile<DefinitionProvider>>,
+ tasks: Vec<Arc<StaticTask>>,
+ _definitions: Model<TrackedFile<TaskDefinitions>>,
_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<Definition>);
+pub struct TaskDefinitions(pub Vec<Definition>);
-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<Cow<'static, str>>,
- definitions: Model<TrackedFile<DefinitionProvider>>,
+ definitions: Model<TrackedFile<TaskDefinitions>>,
cx: &mut AppContext,
) -> Model<Box<dyn TaskSource>> {
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<Box<dyn TaskSource>>,
) -> Vec<Arc<dyn Task>> {
self.tasks
- .clone()
- .into_iter()
- .map(|task| Arc::new(task) as Arc<dyn Task>)
+ .iter()
+ .map(|task| task.clone() as Arc<dyn Task>)
.collect()
}
@@ -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<VsCodeTaskDefinition>,
}
-impl TryFrom<VsCodeTaskFile> for DefinitionProvider {
+impl TryFrom<VsCodeTaskFile> for TaskDefinitions {
type Error = anyhow::Error;
fn try_from(value: VsCodeTaskFile) -> Result<Self, Self::Error> {
@@ -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);
}
}
@@ -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<Opt
#[cfg(test)]
mod tests {
- use std::{collections::HashMap, sync::Arc};
+ use std::sync::Arc;
use editor::Editor;
use gpui::{Entity, TestAppContext};
- use language::{DefaultContextProvider, Language, LanguageConfig};
+ use language::{Language, LanguageConfig, SymbolContextProvider};
use project::{FakeFs, Project, TaskSourceKind};
use serde_json::json;
- use task::{oneshot_source::OneshotSource, TaskContext};
+ use task::{oneshot_source::OneshotSource, TaskContext, TaskVariables};
use ui::VisualContext;
use workspace::{AppState, Workspace};
@@ -302,7 +299,7 @@ mod tests {
name: (_) @name) @item"#,
)
.unwrap()
- .with_context_provider(Some(Arc::new(DefaultContextProvider))),
+ .with_context_provider(Some(Arc::new(SymbolContextProvider))),
);
let typescript_language = Arc::new(
@@ -320,7 +317,7 @@ mod tests {
")" @context)) @item"#,
)
.unwrap()
- .with_context_provider(Some(Arc::new(DefaultContextProvider))),
+ .with_context_provider(Some(Arc::new(SymbolContextProvider))),
);
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, cx| {
@@ -362,7 +359,7 @@ mod tests {
task_context(this, task_cwd(this, cx).unwrap(), cx),
TaskContext {
cwd: Some("/dir".into()),
- env: HashMap::from_iter([
+ task_variables: TaskVariables::from_iter([
("ZED_FILE".into(), "/dir/rust/b.rs".into()),
("ZED_WORKTREE_ROOT".into(), "/dir".into()),
("ZED_ROW".into(), "1".into()),
@@ -379,7 +376,7 @@ mod tests {
task_context(this, task_cwd(this, cx).unwrap(), cx),
TaskContext {
cwd: Some("/dir".into()),
- env: HashMap::from_iter([
+ task_variables: TaskVariables::from_iter([
("ZED_FILE".into(), "/dir/rust/b.rs".into()),
("ZED_WORKTREE_ROOT".into(), "/dir".into()),
("ZED_SYMBOL".into(), "this_is_a_rust_file".into()),
@@ -396,7 +393,7 @@ mod tests {
task_context(this, task_cwd(this, cx).unwrap(), cx),
TaskContext {
cwd: Some("/dir".into()),
- env: HashMap::from_iter([
+ task_variables: TaskVariables::from_iter([
("ZED_FILE".into(), "/dir/a.ts".into()),
("ZED_WORKTREE_ROOT".into(), "/dir".into()),
("ZED_SYMBOL".into(), "this_is_a_test".into()),
@@ -45,7 +45,7 @@ impl_actions!(task, [Rerun, Spawn]);
/// A modal used to spawn new tasks.
pub(crate) struct TasksModalDelegate {
inventory: Model<Inventory>,
- candidates: Vec<(TaskSourceKind, Arc<dyn Task>)>,
+ candidates: Option<Vec<(TaskSourceKind, Arc<dyn Task>)>>,
matches: Vec<StringMatch>,
selected_index: usize,
workspace: WeakView<Workspace>,
@@ -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<Workspace>,
cx: &mut ViewContext<'_, Picker<Self>>,
) -> 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<picker::Picker<Self>>,
) -> Option<Self::ListItem> {
+ 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()
}
@@ -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<AppState>, cx: &mut AppContext) {
},
cx,
);
+ inventory.add_source(
+ TaskSourceKind::Buffer,
+ |cx| LanguageSource::new(app_state.languages.clone(), cx),
+ cx,
+ );
})
});
}