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