task: Add ZED_RELATIVE_DIR task variable (#31657)

Dhruvin Gandhi created

This is my first contribution to zed, let me know if I missed anything.

There is no corresponding issue/discussion.

`$ZED_RELATIVE_DIR` can be used in cases where a task's command's
filesystem namespace (e.g. inside a container) is different than the
host, where absolute paths cannot work.

I modified `relative_path` to `relative_file` after the addition of
`relative_dir`.

For top-level files, where `relative_file.parent() == Some("")`, I use
`"."` for `$ZED_RELATIVE_DIR`, which is a valid relative path in both
*nix and windows.

Thank you for building zed, and open-sourcing it. I hope to contribute
more as I use it as my primary editor.

Release Notes:

- Added ZED_RELATIVE_DIR (path to current file's directory relative to
worktree root) task variable.

Change summary

crates/project/src/task_inventory.rs | 14 ++++++++++++--
crates/task/src/lib.rs               |  4 ++++
crates/tasks_ui/src/tasks_ui.rs      |  3 +++
docs/src/tasks.md                    |  1 +
4 files changed, 20 insertions(+), 2 deletions(-)

Detailed changes

crates/project/src/task_inventory.rs 🔗

@@ -847,11 +847,21 @@ impl ContextProvider for BasicContextProvider {
             );
             if let Some(full_path) = current_file.as_ref() {
                 let relative_path = pathdiff::diff_paths(full_path, worktree_path);
-                if let Some(relative_path) = relative_path {
+                if let Some(relative_file) = relative_path {
                     task_variables.insert(
                         VariableName::RelativeFile,
-                        relative_path.to_sanitized_string(),
+                        relative_file.to_sanitized_string(),
                     );
+                    if let Some(relative_dir) = relative_file.parent() {
+                        task_variables.insert(
+                            VariableName::RelativeDir,
+                            if relative_dir.as_os_str().is_empty() {
+                                String::from(".")
+                            } else {
+                                relative_dir.to_sanitized_string()
+                            },
+                        );
+                    }
                 }
             }
         }

crates/task/src/lib.rs 🔗

@@ -151,6 +151,8 @@ pub enum VariableName {
     File,
     /// A path of the currently opened file (relative to worktree root).
     RelativeFile,
+    /// A path of the currently opened file's directory (relative to worktree root).
+    RelativeDir,
     /// The currently opened filename.
     Filename,
     /// The path to a parent directory of a currently opened file.
@@ -194,6 +196,7 @@ impl FromStr for VariableName {
             "FILE" => Self::File,
             "FILENAME" => Self::Filename,
             "RELATIVE_FILE" => Self::RelativeFile,
+            "RELATIVE_DIR" => Self::RelativeDir,
             "DIRNAME" => Self::Dirname,
             "STEM" => Self::Stem,
             "WORKTREE_ROOT" => Self::WorktreeRoot,
@@ -226,6 +229,7 @@ impl std::fmt::Display for VariableName {
             Self::File => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILE"),
             Self::Filename => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILENAME"),
             Self::RelativeFile => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_FILE"),
+            Self::RelativeDir => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_DIR"),
             Self::Dirname => write!(f, "{ZED_VARIABLE_NAME_PREFIX}DIRNAME"),
             Self::Stem => write!(f, "{ZED_VARIABLE_NAME_PREFIX}STEM"),
             Self::WorktreeRoot => write!(f, "{ZED_VARIABLE_NAME_PREFIX}WORKTREE_ROOT"),

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -509,6 +509,7 @@ mod tests {
                     (VariableName::File, path!("/dir/rust/b.rs").into()),
                     (VariableName::Filename, "b.rs".into()),
                     (VariableName::RelativeFile, separator!("rust/b.rs").into()),
+                    (VariableName::RelativeDir, "rust".into()),
                     (VariableName::Dirname, path!("/dir/rust").into()),
                     (VariableName::Stem, "b".into()),
                     (VariableName::WorktreeRoot, path!("/dir").into()),
@@ -540,6 +541,7 @@ mod tests {
                     (VariableName::File, path!("/dir/rust/b.rs").into()),
                     (VariableName::Filename, "b.rs".into()),
                     (VariableName::RelativeFile, separator!("rust/b.rs").into()),
+                    (VariableName::RelativeDir, "rust".into()),
                     (VariableName::Dirname, path!("/dir/rust").into()),
                     (VariableName::Stem, "b".into()),
                     (VariableName::WorktreeRoot, path!("/dir").into()),
@@ -568,6 +570,7 @@ mod tests {
                     (VariableName::File, path!("/dir/a.ts").into()),
                     (VariableName::Filename, "a.ts".into()),
                     (VariableName::RelativeFile, "a.ts".into()),
+                    (VariableName::RelativeDir, ".".into()),
                     (VariableName::Dirname, path!("/dir").into()),
                     (VariableName::Stem, "a".into()),
                     (VariableName::WorktreeRoot, path!("/dir").into()),

docs/src/tasks.md 🔗

@@ -80,6 +80,7 @@ These variables allow you to pull information from the current editor and use it
 - `ZED_FILENAME`: filename of the currently opened file (e.g. `main.rs`)
 - `ZED_DIRNAME`: absolute path of the currently opened file with file name stripped (e.g. `/Users/my-user/path/to/project/src`)
 - `ZED_RELATIVE_FILE`: path of the currently opened file, relative to `ZED_WORKTREE_ROOT` (e.g. `src/main.rs`)
+- `ZED_RELATIVE_DIR`: path of the currently opened file's directory, relative to `ZED_WORKTREE_ROOT` (e.g. `src`)
 - `ZED_STEM`: stem (filename without extension) of the currently opened file (e.g. `main`)
 - `ZED_SYMBOL`: currently selected symbol; should match the last symbol shown in a symbol breadcrumb (e.g. `mod tests > fn test_task_contexts`)
 - `ZED_SELECTED_TEXT`: currently selected text