task.rs

  1//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic.
  2
  3mod adapter_schema;
  4mod debug_format;
  5mod serde_helpers;
  6mod shell_builder;
  7pub mod static_source;
  8mod task_template;
  9mod vscode_debug_format;
 10mod vscode_format;
 11
 12use anyhow::Context as _;
 13use collections::{HashMap, HashSet, hash_map};
 14use gpui::SharedString;
 15use schemars::JsonSchema;
 16use serde::{Deserialize, Serialize};
 17use std::borrow::Cow;
 18use std::path::PathBuf;
 19use std::str::FromStr;
 20use util::get_system_shell;
 21
 22pub use adapter_schema::{AdapterSchema, AdapterSchemas};
 23pub use debug_format::{
 24    AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
 25    Request, TcpArgumentsTemplate, ZedDebugConfig,
 26};
 27pub use shell_builder::{ShellBuilder, ShellKind};
 28pub use task_template::{
 29    DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
 30    substitute_variables_in_map, substitute_variables_in_str,
 31};
 32pub use vscode_debug_format::VsCodeDebugTaskFile;
 33pub use vscode_format::VsCodeTaskFile;
 34pub use zed_actions::RevealTarget;
 35
 36/// Task identifier, unique within the application.
 37/// Based on it, task reruns and terminal tabs are managed.
 38#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
 39pub struct TaskId(pub String);
 40
 41/// Contains all information needed by Zed to spawn a new terminal tab for the given task.
 42#[derive(Default, Debug, Clone, PartialEq, Eq)]
 43pub struct SpawnInTerminal {
 44    /// Id of the task to use when determining task tab affinity.
 45    pub id: TaskId,
 46    /// Full unshortened form of `label` field.
 47    pub full_label: String,
 48    /// Human readable name of the terminal tab.
 49    pub label: String,
 50    /// Executable command to spawn.
 51    pub command: Option<String>,
 52    /// Arguments to the command, potentially unsubstituted,
 53    /// to let the shell that spawns the command to do the substitution, if needed.
 54    pub args: Vec<String>,
 55    /// A human-readable label, containing command and all of its arguments, joined and substituted.
 56    pub command_label: String,
 57    /// Current working directory to spawn the command into.
 58    pub cwd: Option<PathBuf>,
 59    /// Env overrides for the command, will be appended to the terminal's environment from the settings.
 60    pub env: HashMap<String, String>,
 61    /// Whether to use a new terminal tab or reuse the existing one to spawn the process.
 62    pub use_new_terminal: bool,
 63    /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
 64    pub allow_concurrent_runs: bool,
 65    /// What to do with the terminal pane and tab, after the command was started.
 66    pub reveal: RevealStrategy,
 67    /// Where to show tasks' terminal output.
 68    pub reveal_target: RevealTarget,
 69    /// What to do with the terminal pane and tab, after the command had finished.
 70    pub hide: HideStrategy,
 71    /// Which shell to use when spawning the task.
 72    pub shell: Shell,
 73    /// Whether to show the task summary line in the task output (sucess/failure).
 74    pub show_summary: bool,
 75    /// Whether to show the command line in the task output.
 76    pub show_command: bool,
 77    /// Whether to show the rerun button in the terminal tab.
 78    pub show_rerun: bool,
 79}
 80
 81impl SpawnInTerminal {
 82    pub fn to_proto(&self) -> proto::SpawnInTerminal {
 83        proto::SpawnInTerminal {
 84            label: self.label.clone(),
 85            command: self.command.clone(),
 86            args: self.args.clone(),
 87            env: self
 88                .env
 89                .iter()
 90                .map(|(k, v)| (k.clone(), v.clone()))
 91                .collect(),
 92            cwd: self
 93                .cwd
 94                .clone()
 95                .map(|cwd| cwd.to_string_lossy().into_owned()),
 96        }
 97    }
 98
 99    pub fn from_proto(proto: proto::SpawnInTerminal) -> Self {
100        Self {
101            label: proto.label.clone(),
102            command: proto.command.clone(),
103            args: proto.args.clone(),
104            env: proto.env.into_iter().collect(),
105            cwd: proto.cwd.map(PathBuf::from),
106            ..Default::default()
107        }
108    }
109}
110
111/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct ResolvedTask {
114    /// A way to distinguish tasks produced by the same template, but different contexts.
115    /// NOTE: Resolved tasks may have the same labels, commands and do the same things,
116    /// but still may have different ids if the context was different during the resolution.
117    /// Since the template has `env` field, for a generic task that may be a bash command,
118    /// so it's impossible to determine the id equality without more context in a generic case.
119    pub id: TaskId,
120    /// A template the task got resolved from.
121    original_task: TaskTemplate,
122    /// Full, unshortened label of the task after all resolutions are made.
123    pub resolved_label: String,
124    /// Variables that were substituted during the task template resolution.
125    substituted_variables: HashSet<VariableName>,
126    /// Further actions that need to take place after the resolved task is spawned,
127    /// with all task variables resolved.
128    pub resolved: SpawnInTerminal,
129}
130
131impl ResolvedTask {
132    /// A task template before the resolution.
133    pub fn original_task(&self) -> &TaskTemplate {
134        &self.original_task
135    }
136
137    /// Variables that were substituted during the task template resolution.
138    pub fn substituted_variables(&self) -> &HashSet<VariableName> {
139        &self.substituted_variables
140    }
141
142    /// A human-readable label to display in the UI.
143    pub fn display_label(&self) -> &str {
144        self.resolved.label.as_str()
145    }
146}
147
148/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
149/// Name of the variable must be a valid shell variable identifier, which generally means that it is
150/// a word  consisting only  of alphanumeric characters and underscores,
151/// and beginning with an alphabetic character or an  underscore.
152#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
153pub enum VariableName {
154    /// An absolute path of the currently opened file.
155    File,
156    /// A path of the currently opened file (relative to worktree root).
157    RelativeFile,
158    /// A path of the currently opened file's directory (relative to worktree root).
159    RelativeDir,
160    /// The currently opened filename.
161    Filename,
162    /// The path to a parent directory of a currently opened file.
163    Dirname,
164    /// Stem (filename without extension) of the currently opened file.
165    Stem,
166    /// An absolute path of the currently opened worktree, that contains the file.
167    WorktreeRoot,
168    /// A symbol text, that contains latest cursor/selection position.
169    Symbol,
170    /// A row with the latest cursor/selection position.
171    Row,
172    /// A column with the latest cursor/selection position.
173    Column,
174    /// Text from the latest selection.
175    SelectedText,
176    /// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
177    RunnableSymbol,
178    /// Custom variable, provided by the plugin or other external source.
179    /// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables.
180    Custom(Cow<'static, str>),
181}
182
183impl VariableName {
184    /// Generates a `$VARIABLE`-like string value to be used in templates.
185    pub fn template_value(&self) -> String {
186        format!("${self}")
187    }
188    /// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters.
189    pub fn template_value_with_whitespace(&self) -> String {
190        format!("\"${self}\"")
191    }
192}
193
194impl FromStr for VariableName {
195    type Err = ();
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
199        let value = match without_prefix {
200            "FILE" => Self::File,
201            "FILENAME" => Self::Filename,
202            "RELATIVE_FILE" => Self::RelativeFile,
203            "RELATIVE_DIR" => Self::RelativeDir,
204            "DIRNAME" => Self::Dirname,
205            "STEM" => Self::Stem,
206            "WORKTREE_ROOT" => Self::WorktreeRoot,
207            "SYMBOL" => Self::Symbol,
208            "RUNNABLE_SYMBOL" => Self::RunnableSymbol,
209            "SELECTED_TEXT" => Self::SelectedText,
210            "ROW" => Self::Row,
211            "COLUMN" => Self::Column,
212            _ => {
213                if let Some(custom_name) =
214                    without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX)
215                {
216                    Self::Custom(Cow::Owned(custom_name.to_owned()))
217                } else {
218                    return Err(());
219                }
220            }
221        };
222        Ok(value)
223    }
224}
225
226/// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts.
227pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_";
228const ZED_CUSTOM_VARIABLE_NAME_PREFIX: &str = "CUSTOM_";
229
230impl std::fmt::Display for VariableName {
231    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
232        match self {
233            Self::File => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILE"),
234            Self::Filename => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILENAME"),
235            Self::RelativeFile => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_FILE"),
236            Self::RelativeDir => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_DIR"),
237            Self::Dirname => write!(f, "{ZED_VARIABLE_NAME_PREFIX}DIRNAME"),
238            Self::Stem => write!(f, "{ZED_VARIABLE_NAME_PREFIX}STEM"),
239            Self::WorktreeRoot => write!(f, "{ZED_VARIABLE_NAME_PREFIX}WORKTREE_ROOT"),
240            Self::Symbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SYMBOL"),
241            Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
242            Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
243            Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
244            Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
245            Self::Custom(s) => write!(
246                f,
247                "{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}"
248            ),
249        }
250    }
251}
252
253/// Container for predefined environment variables that describe state of Zed at the time the task was spawned.
254#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
255pub struct TaskVariables(HashMap<VariableName, String>);
256
257impl TaskVariables {
258    /// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned.
259    pub fn insert(&mut self, variable: VariableName, value: String) -> Option<String> {
260        self.0.insert(variable, value)
261    }
262
263    /// Extends the container with another one, overwriting the existing variables on collision.
264    pub fn extend(&mut self, other: Self) {
265        self.0.extend(other.0);
266    }
267    /// Get the value associated with given variable name, if there is one.
268    pub fn get(&self, key: &VariableName) -> Option<&str> {
269        self.0.get(key).map(|s| s.as_str())
270    }
271    /// Clear out variables obtained from tree-sitter queries, which are prefixed with '_' character
272    pub fn sweep(&mut self) {
273        self.0.retain(|name, _| {
274            if let VariableName::Custom(name) = name {
275                !name.starts_with('_')
276            } else {
277                true
278            }
279        })
280    }
281
282    pub fn iter(&self) -> impl Iterator<Item = (&VariableName, &String)> {
283        self.0.iter()
284    }
285}
286
287impl FromIterator<(VariableName, String)> for TaskVariables {
288    fn from_iter<T: IntoIterator<Item = (VariableName, String)>>(iter: T) -> Self {
289        Self(HashMap::from_iter(iter))
290    }
291}
292
293impl IntoIterator for TaskVariables {
294    type Item = (VariableName, String);
295
296    type IntoIter = hash_map::IntoIter<VariableName, String>;
297
298    fn into_iter(self) -> Self::IntoIter {
299        self.0.into_iter()
300    }
301}
302
303/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
304/// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
305#[derive(Clone, Debug, Default, PartialEq, Eq)]
306pub struct TaskContext {
307    /// A path to a directory in which the task should be executed.
308    pub cwd: Option<PathBuf>,
309    /// Additional environment variables associated with a given task.
310    pub task_variables: TaskVariables,
311    /// Environment variables obtained when loading the project into Zed.
312    /// This is the environment one would get when `cd`ing in a terminal
313    /// into the project's root directory.
314    pub project_env: HashMap<String, String>,
315}
316
317/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
318#[derive(Clone, Debug)]
319pub struct RunnableTag(pub SharedString);
320
321/// Shell configuration to open the terminal with.
322#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
323#[serde(rename_all = "snake_case")]
324pub enum Shell {
325    /// Use the system's default terminal configuration in /etc/passwd
326    #[default]
327    System,
328    /// Use a specific program with no arguments.
329    Program(String),
330    /// Use a specific program with arguments.
331    WithArguments {
332        /// The program to run.
333        program: String,
334        /// The arguments to pass to the program.
335        args: Vec<String>,
336        /// An optional string to override the title of the terminal tab
337        title_override: Option<SharedString>,
338    },
339}
340
341impl Shell {
342    pub fn program(&self) -> String {
343        match self {
344            Shell::Program(program) => program.clone(),
345            Shell::WithArguments { program, .. } => program.clone(),
346            Shell::System => get_system_shell(),
347        }
348    }
349
350    pub fn program_and_args(&self) -> (String, &[String]) {
351        match self {
352            Shell::Program(program) => (program.clone(), &[]),
353            Shell::WithArguments { program, args, .. } => (program.clone(), args),
354            Shell::System => (get_system_shell(), &[]),
355        }
356    }
357
358    pub fn shell_kind(&self, is_windows: bool) -> ShellKind {
359        match self {
360            Shell::Program(program) => ShellKind::new(program, is_windows),
361            Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows),
362            Shell::System => ShellKind::system(),
363        }
364    }
365
366    pub fn from_proto(proto: proto::Shell) -> anyhow::Result<Self> {
367        let shell_type = proto.shell_type.context("invalid shell type")?;
368        let shell = match shell_type {
369            proto::shell::ShellType::System(_) => Self::System,
370            proto::shell::ShellType::Program(program) => Self::Program(program),
371            proto::shell::ShellType::WithArguments(program) => Self::WithArguments {
372                program: program.program,
373                args: program.args,
374                title_override: None,
375            },
376        };
377        Ok(shell)
378    }
379
380    pub fn to_proto(self) -> proto::Shell {
381        let shell_type = match self {
382            Shell::System => proto::shell::ShellType::System(proto::System {}),
383            Shell::Program(program) => proto::shell::ShellType::Program(program),
384            Shell::WithArguments {
385                program,
386                args,
387                title_override: _,
388            } => proto::shell::ShellType::WithArguments(proto::shell::WithArguments {
389                program,
390                args,
391            }),
392        };
393        proto::Shell {
394            shell_type: Some(shell_type),
395        }
396    }
397}
398
399type VsCodeEnvVariable = String;
400type ZedEnvVariable = String;
401
402struct EnvVariableReplacer {
403    variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>,
404}
405
406impl EnvVariableReplacer {
407    fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
408        Self { variables }
409    }
410
411    fn replace_value(&self, input: serde_json::Value) -> serde_json::Value {
412        match input {
413            serde_json::Value::String(s) => serde_json::Value::String(self.replace(&s)),
414            serde_json::Value::Array(arr) => {
415                serde_json::Value::Array(arr.into_iter().map(|v| self.replace_value(v)).collect())
416            }
417            serde_json::Value::Object(obj) => serde_json::Value::Object(
418                obj.into_iter()
419                    .map(|(k, v)| (self.replace(&k), self.replace_value(v)))
420                    .collect(),
421            ),
422            _ => input,
423        }
424    }
425    // Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
426    fn replace(&self, input: &str) -> String {
427        shellexpand::env_with_context_no_errors(&input, |var: &str| {
428            // Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us.
429            let colon_position = var.find(':').unwrap_or(var.len());
430            let (left, right) = var.split_at(colon_position);
431            if left == "env" && !right.is_empty() {
432                let variable_name = &right[1..];
433                return Some(format!("${{{variable_name}}}"));
434            }
435            let (variable_name, default) = (left, right);
436            let append_previous_default = |ret: &mut String| {
437                if !default.is_empty() {
438                    ret.push_str(default);
439                }
440            };
441            if let Some(substitution) = self.variables.get(variable_name) {
442                // Got a VSCode->Zed hit, perform a substitution
443                let mut name = format!("${{{substitution}");
444                append_previous_default(&mut name);
445                name.push('}');
446                return Some(name);
447            }
448            // This is an unknown variable.
449            // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect.
450            // If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us.
451            if !default.is_empty() {
452                return Some(format!("${{{var}}}"));
453            }
454            // Else we can just return None and that variable will be left as is.
455            None
456        })
457        .into_owned()
458    }
459}