1//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic.
2#![deny(missing_docs)]
3
4mod debug_format;
5mod serde_helpers;
6pub mod static_source;
7mod task_template;
8mod vscode_format;
9
10use collections::{HashMap, HashSet, hash_map};
11use gpui::SharedString;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::borrow::Cow;
15use std::path::PathBuf;
16use std::str::FromStr;
17
18pub use debug_format::{
19 AttachConfig, DebugConnectionType, DebugRequestDisposition, DebugRequestType,
20 DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
21};
22pub use task_template::{
23 DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
24 TaskTemplates, TaskType,
25};
26pub use vscode_format::VsCodeTaskFile;
27pub use zed_actions::RevealTarget;
28
29/// Task identifier, unique within the application.
30/// Based on it, task reruns and terminal tabs are managed.
31#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
32pub struct TaskId(pub String);
33
34/// Contains all information needed by Zed to spawn a new terminal tab for the given task.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct SpawnInTerminal {
37 /// Id of the task to use when determining task tab affinity.
38 pub id: TaskId,
39 /// Full unshortened form of `label` field.
40 pub full_label: String,
41 /// Human readable name of the terminal tab.
42 pub label: String,
43 /// Executable command to spawn.
44 pub command: String,
45 /// Arguments to the command, potentially unsubstituted,
46 /// to let the shell that spawns the command to do the substitution, if needed.
47 pub args: Vec<String>,
48 /// A human-readable label, containing command and all of its arguments, joined and substituted.
49 pub command_label: String,
50 /// Current working directory to spawn the command into.
51 pub cwd: Option<PathBuf>,
52 /// Env overrides for the command, will be appended to the terminal's environment from the settings.
53 pub env: HashMap<String, String>,
54 /// Whether to use a new terminal tab or reuse the existing one to spawn the process.
55 pub use_new_terminal: bool,
56 /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
57 pub allow_concurrent_runs: bool,
58 /// What to do with the terminal pane and tab, after the command was started.
59 pub reveal: RevealStrategy,
60 /// Where to show tasks' terminal output.
61 pub reveal_target: RevealTarget,
62 /// What to do with the terminal pane and tab, after the command had finished.
63 pub hide: HideStrategy,
64 /// Which shell to use when spawning the task.
65 pub shell: Shell,
66 /// Whether to show the task summary line in the task output (sucess/failure).
67 pub show_summary: bool,
68 /// Whether to show the command line in the task output.
69 pub show_command: bool,
70 /// Whether to show the rerun button in the terminal tab.
71 pub show_rerun: bool,
72}
73
74/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct ResolvedTask {
77 /// A way to distinguish tasks produced by the same template, but different contexts.
78 /// NOTE: Resolved tasks may have the same labels, commands and do the same things,
79 /// but still may have different ids if the context was different during the resolution.
80 /// Since the template has `env` field, for a generic task that may be a bash command,
81 /// so it's impossible to determine the id equality without more context in a generic case.
82 pub id: TaskId,
83 /// A template the task got resolved from.
84 original_task: TaskTemplate,
85 /// Full, unshortened label of the task after all resolutions are made.
86 pub resolved_label: String,
87 /// Variables that were substituted during the task template resolution.
88 substituted_variables: HashSet<VariableName>,
89 /// Further actions that need to take place after the resolved task is spawned,
90 /// with all task variables resolved.
91 pub resolved: Option<SpawnInTerminal>,
92}
93
94impl ResolvedTask {
95 /// A task template before the resolution.
96 pub fn original_task(&self) -> &TaskTemplate {
97 &self.original_task
98 }
99
100 /// Get the task type that determines what this task is used for
101 /// And where is it shown in the UI
102 pub fn task_type(&self) -> TaskType {
103 self.original_task.task_type.clone()
104 }
105
106 /// Get the configuration for the debug adapter that should be used for this task.
107 pub fn resolved_debug_adapter_config(&self) -> Option<DebugTaskDefinition> {
108 match self.original_task.task_type.clone() {
109 TaskType::Debug(debug_args) if self.resolved.is_some() => {
110 let resolved = self
111 .resolved
112 .as_ref()
113 .expect("We just checked if this was some");
114
115 let args = resolved
116 .args
117 .iter()
118 .cloned()
119 .map(|arg| {
120 if arg.starts_with("$") {
121 arg.strip_prefix("$")
122 .and_then(|arg| resolved.env.get(arg).map(ToOwned::to_owned))
123 .unwrap_or_else(|| arg)
124 } else {
125 arg
126 }
127 })
128 .collect();
129
130 Some(DebugTaskDefinition {
131 label: resolved.label.clone(),
132 adapter: debug_args.adapter.clone(),
133 request: match debug_args.request {
134 crate::task_template::DebugArgsRequest::Launch => {
135 DebugRequestType::Launch(LaunchConfig {
136 program: resolved.command.clone(),
137 cwd: resolved.cwd.clone(),
138 args,
139 })
140 }
141 crate::task_template::DebugArgsRequest::Attach(attach_config) => {
142 DebugRequestType::Attach(attach_config)
143 }
144 },
145 initialize_args: debug_args.initialize_args,
146 tcp_connection: debug_args.tcp_connection,
147 locator: debug_args.locator.clone(),
148 stop_on_entry: debug_args.stop_on_entry,
149 })
150 }
151 _ => None,
152 }
153 }
154
155 /// Variables that were substituted during the task template resolution.
156 pub fn substituted_variables(&self) -> &HashSet<VariableName> {
157 &self.substituted_variables
158 }
159
160 /// A human-readable label to display in the UI.
161 pub fn display_label(&self) -> &str {
162 self.resolved
163 .as_ref()
164 .map(|resolved| resolved.label.as_str())
165 .unwrap_or_else(|| self.resolved_label.as_str())
166 }
167}
168
169/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
170/// Name of the variable must be a valid shell variable identifier, which generally means that it is
171/// a word consisting only of alphanumeric characters and underscores,
172/// and beginning with an alphabetic character or an underscore.
173#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
174pub enum VariableName {
175 /// An absolute path of the currently opened file.
176 File,
177 /// A path of the currently opened file (relative to worktree root).
178 RelativeFile,
179 /// The currently opened filename.
180 Filename,
181 /// The path to a parent directory of a currently opened file.
182 Dirname,
183 /// Stem (filename without extension) of the currently opened file.
184 Stem,
185 /// An absolute path of the currently opened worktree, that contains the file.
186 WorktreeRoot,
187 /// A symbol text, that contains latest cursor/selection position.
188 Symbol,
189 /// A row with the latest cursor/selection position.
190 Row,
191 /// A column with the latest cursor/selection position.
192 Column,
193 /// Text from the latest selection.
194 SelectedText,
195 /// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
196 RunnableSymbol,
197 /// Custom variable, provided by the plugin or other external source.
198 /// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables.
199 Custom(Cow<'static, str>),
200}
201
202impl VariableName {
203 /// Generates a `$VARIABLE`-like string value to be used in templates.
204 pub fn template_value(&self) -> String {
205 format!("${self}")
206 }
207 /// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters.
208 pub fn template_value_with_whitespace(&self) -> String {
209 format!("\"${self}\"")
210 }
211}
212
213impl FromStr for VariableName {
214 type Err = ();
215
216 fn from_str(s: &str) -> Result<Self, Self::Err> {
217 let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
218 let value = match without_prefix {
219 "FILE" => Self::File,
220 "FILENAME" => Self::Filename,
221 "RELATIVE_FILE" => Self::RelativeFile,
222 "DIRNAME" => Self::Dirname,
223 "STEM" => Self::Stem,
224 "WORKTREE_ROOT" => Self::WorktreeRoot,
225 "SYMBOL" => Self::Symbol,
226 "RUNNABLE_SYMBOL" => Self::RunnableSymbol,
227 "SELECTED_TEXT" => Self::SelectedText,
228 "ROW" => Self::Row,
229 "COLUMN" => Self::Column,
230 _ => {
231 if let Some(custom_name) =
232 without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX)
233 {
234 Self::Custom(Cow::Owned(custom_name.to_owned()))
235 } else {
236 return Err(());
237 }
238 }
239 };
240 Ok(value)
241 }
242}
243
244/// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts.
245pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_";
246const ZED_CUSTOM_VARIABLE_NAME_PREFIX: &str = "CUSTOM_";
247
248impl std::fmt::Display for VariableName {
249 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
250 match self {
251 Self::File => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILE"),
252 Self::Filename => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILENAME"),
253 Self::RelativeFile => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_FILE"),
254 Self::Dirname => write!(f, "{ZED_VARIABLE_NAME_PREFIX}DIRNAME"),
255 Self::Stem => write!(f, "{ZED_VARIABLE_NAME_PREFIX}STEM"),
256 Self::WorktreeRoot => write!(f, "{ZED_VARIABLE_NAME_PREFIX}WORKTREE_ROOT"),
257 Self::Symbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SYMBOL"),
258 Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
259 Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
260 Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
261 Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
262 Self::Custom(s) => write!(
263 f,
264 "{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}"
265 ),
266 }
267 }
268}
269
270/// Container for predefined environment variables that describe state of Zed at the time the task was spawned.
271#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
272pub struct TaskVariables(HashMap<VariableName, String>);
273
274impl TaskVariables {
275 /// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned.
276 pub fn insert(&mut self, variable: VariableName, value: String) -> Option<String> {
277 self.0.insert(variable, value)
278 }
279
280 /// Extends the container with another one, overwriting the existing variables on collision.
281 pub fn extend(&mut self, other: Self) {
282 self.0.extend(other.0);
283 }
284 /// Get the value associated with given variable name, if there is one.
285 pub fn get(&self, key: &VariableName) -> Option<&str> {
286 self.0.get(key).map(|s| s.as_str())
287 }
288 /// Clear out variables obtained from tree-sitter queries, which are prefixed with '_' character
289 pub fn sweep(&mut self) {
290 self.0.retain(|name, _| {
291 if let VariableName::Custom(name) = name {
292 !name.starts_with('_')
293 } else {
294 true
295 }
296 })
297 }
298}
299
300impl FromIterator<(VariableName, String)> for TaskVariables {
301 fn from_iter<T: IntoIterator<Item = (VariableName, String)>>(iter: T) -> Self {
302 Self(HashMap::from_iter(iter))
303 }
304}
305
306impl IntoIterator for TaskVariables {
307 type Item = (VariableName, String);
308
309 type IntoIter = hash_map::IntoIter<VariableName, String>;
310
311 fn into_iter(self) -> Self::IntoIter {
312 self.0.into_iter()
313 }
314}
315
316/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
317/// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
318#[derive(Clone, Debug, Default, PartialEq, Eq)]
319pub struct TaskContext {
320 /// A path to a directory in which the task should be executed.
321 pub cwd: Option<PathBuf>,
322 /// Additional environment variables associated with a given task.
323 pub task_variables: TaskVariables,
324 /// Environment variables obtained when loading the project into Zed.
325 /// This is the environment one would get when `cd`ing in a terminal
326 /// into the project's root directory.
327 pub project_env: HashMap<String, String>,
328}
329
330/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
331#[derive(Clone, Debug)]
332pub struct RunnableTag(pub SharedString);
333
334/// Shell configuration to open the terminal with.
335#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
336#[serde(rename_all = "snake_case")]
337pub enum Shell {
338 /// Use the system's default terminal configuration in /etc/passwd
339 #[default]
340 System,
341 /// Use a specific program with no arguments.
342 Program(String),
343 /// Use a specific program with arguments.
344 WithArguments {
345 /// The program to run.
346 program: String,
347 /// The arguments to pass to the program.
348 args: Vec<String>,
349 /// An optional string to override the title of the terminal tab
350 title_override: Option<SharedString>,
351 },
352}
353
354#[cfg(target_os = "windows")]
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356enum WindowsShellType {
357 Powershell,
358 Cmd,
359 Other,
360}
361
362/// ShellBuilder is used to turn a user-requested task into a
363/// program that can be executed by the shell.
364pub struct ShellBuilder {
365 program: String,
366 args: Vec<String>,
367}
368
369impl ShellBuilder {
370 /// Create a new ShellBuilder as configured.
371 pub fn new(is_local: bool, shell: &Shell) -> Self {
372 let (program, args) = match shell {
373 Shell::System => {
374 if is_local {
375 (Self::system_shell(), Vec::new())
376 } else {
377 ("\"${SHELL:-sh}\"".to_string(), Vec::new())
378 }
379 }
380 Shell::Program(shell) => (shell.clone(), Vec::new()),
381 Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
382 };
383 Self { program, args }
384 }
385}
386
387#[cfg(not(target_os = "windows"))]
388impl ShellBuilder {
389 /// Returns the label to show in the terminal tab
390 pub fn command_label(&self, command_label: &str) -> String {
391 format!("{} -i -c '{}'", self.program, command_label)
392 }
393
394 /// Returns the program and arguments to run this task in a shell.
395 pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
396 let combined_command = task_args
397 .into_iter()
398 .fold(task_command, |mut command, arg| {
399 command.push(' ');
400 command.push_str(&arg);
401 command
402 });
403 self.args
404 .extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
405
406 (self.program, self.args)
407 }
408
409 fn system_shell() -> String {
410 std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
411 }
412}
413
414#[cfg(target_os = "windows")]
415impl ShellBuilder {
416 /// Returns the label to show in the terminal tab
417 pub fn command_label(&self, command_label: &str) -> String {
418 match self.windows_shell_type() {
419 WindowsShellType::Powershell => {
420 format!("{} -C '{}'", self.program, command_label)
421 }
422 WindowsShellType::Cmd => {
423 format!("{} /C '{}'", self.program, command_label)
424 }
425 WindowsShellType::Other => {
426 format!("{} -i -c '{}'", self.program, command_label)
427 }
428 }
429 }
430
431 /// Returns the program and arguments to run this task in a shell.
432 pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
433 let combined_command = task_args
434 .into_iter()
435 .fold(task_command, |mut command, arg| {
436 command.push(' ');
437 command.push_str(&self.to_windows_shell_variable(arg.to_string()));
438 command
439 });
440
441 match self.windows_shell_type() {
442 WindowsShellType::Powershell => self.args.extend(["-C".to_owned(), combined_command]),
443 WindowsShellType::Cmd => self.args.extend(["/C".to_owned(), combined_command]),
444 WindowsShellType::Other => {
445 self.args
446 .extend(["-i".to_owned(), "-c".to_owned(), combined_command])
447 }
448 }
449
450 (self.program, self.args)
451 }
452 fn windows_shell_type(&self) -> WindowsShellType {
453 if self.program == "powershell"
454 || self.program.ends_with("powershell.exe")
455 || self.program == "pwsh"
456 || self.program.ends_with("pwsh.exe")
457 {
458 WindowsShellType::Powershell
459 } else if self.program == "cmd" || self.program.ends_with("cmd.exe") {
460 WindowsShellType::Cmd
461 } else {
462 // Someother shell detected, the user might install and use a
463 // unix-like shell.
464 WindowsShellType::Other
465 }
466 }
467
468 // `alacritty_terminal` uses this as default on Windows. See:
469 // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
470 // We could use `util::get_windows_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe`
471 // should be okay.
472 fn system_shell() -> String {
473 "powershell.exe".to_string()
474 }
475
476 fn to_windows_shell_variable(&self, input: String) -> String {
477 match self.windows_shell_type() {
478 WindowsShellType::Powershell => Self::to_powershell_variable(input),
479 WindowsShellType::Cmd => Self::to_cmd_variable(input),
480 WindowsShellType::Other => input,
481 }
482 }
483
484 fn to_cmd_variable(input: String) -> String {
485 if let Some(var_str) = input.strip_prefix("${") {
486 if var_str.find(':').is_none() {
487 // If the input starts with "${", remove the trailing "}"
488 format!("%{}%", &var_str[..var_str.len() - 1])
489 } else {
490 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
491 // which will result in the task failing to run in such cases.
492 input
493 }
494 } else if let Some(var_str) = input.strip_prefix('$') {
495 // If the input starts with "$", directly append to "$env:"
496 format!("%{}%", var_str)
497 } else {
498 // If no prefix is found, return the input as is
499 input
500 }
501 }
502
503 fn to_powershell_variable(input: String) -> String {
504 if let Some(var_str) = input.strip_prefix("${") {
505 if var_str.find(':').is_none() {
506 // If the input starts with "${", remove the trailing "}"
507 format!("$env:{}", &var_str[..var_str.len() - 1])
508 } else {
509 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
510 // which will result in the task failing to run in such cases.
511 input
512 }
513 } else if let Some(var_str) = input.strip_prefix('$') {
514 // If the input starts with "$", directly append to "$env:"
515 format!("$env:{}", var_str)
516 } else {
517 // If no prefix is found, return the input as is
518 input
519 }
520 }
521}