task: Add ZED_DIRNAME and ZED_RELATIVE_FILE task variables (#12245)

Piotr Osiewicz created

Release Notes:

- Added ZED_RELATIVE_FILE (path to current file relative to worktree
root) and ZED_DIRNAME (path to the directory containing current file)
task variables.

Change summary

Cargo.lock                           |  1 
Cargo.toml                           |  1 
crates/project/Cargo.toml            |  1 
crates/project/src/task_inventory.rs | 40 +++++++++++++++++++----------
crates/task/src/lib.rs               | 10 ++++++-
crates/tasks_ui/src/lib.rs           |  6 ++++
docs/src/tasks.md                    |  2 +
7 files changed, 45 insertions(+), 16 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7695,6 +7695,7 @@ dependencies = [
  "lsp",
  "node_runtime",
  "parking_lot",
+ "pathdiff",
  "postage",
  "prettier",
  "pretty_assertions",

Cargo.toml 🔗

@@ -305,6 +305,7 @@ once_cell = "1.19.0"
 ordered-float = "2.1.1"
 palette = { version = "0.7.5", default-features = false, features = ["std"] }
 parking_lot = "0.12.1"
+pathdiff = "0.2"
 profiling = "1"
 postage = { version = "0.5", features = ["futures-traits"] }
 pretty_assertions = "1.3.0"

crates/project/Cargo.toml 🔗

@@ -44,6 +44,7 @@ log.workspace = true
 lsp.workspace = true
 node_runtime.workspace = true
 parking_lot.workspace = true
+pathdiff.workspace = true
 postage.workspace = true
 prettier.workspace = true
 worktree.workspace = true

crates/project/src/task_inventory.rs 🔗

@@ -549,20 +549,6 @@ impl ContextProvider for BasicContextProvider {
         if !selected_text.trim().is_empty() {
             task_variables.insert(VariableName::SelectedText, selected_text);
         }
-        if let Some(path) = current_file {
-            task_variables.insert(VariableName::File, path.clone());
-
-            let path = Path::new(&path);
-
-            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
-                task_variables.insert(VariableName::Filename, String::from(filename));
-            }
-
-            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
-                task_variables.insert(VariableName::Stem, String::from(stem));
-            }
-        }
-
         let worktree_abs_path = buffer
             .file()
             .map(|file| WorktreeId::from_usize(file.worktree_id()))
@@ -577,6 +563,32 @@ impl ContextProvider for BasicContextProvider {
                 VariableName::WorktreeRoot,
                 worktree_path.to_string_lossy().to_string(),
             );
+            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 {
+                    task_variables.insert(
+                        VariableName::RelativeFile,
+                        relative_path.to_string_lossy().into_owned(),
+                    );
+                }
+            }
+        }
+
+        if let Some(path_as_string) = current_file {
+            let path = Path::new(&path_as_string);
+            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
+                task_variables.insert(VariableName::Filename, String::from(filename));
+            }
+
+            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
+                task_variables.insert(VariableName::Stem, stem.into());
+            }
+
+            if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
+                task_variables.insert(VariableName::Dirname, dirname.into());
+            }
+
+            task_variables.insert(VariableName::File, path_as_string);
         }
 
         Ok(task_variables)

crates/task/src/lib.rs 🔗

@@ -124,9 +124,13 @@ impl ResolvedTask {
 pub enum VariableName {
     /// An absolute path of the currently opened file.
     File,
-    /// the currently opened filename.
+    /// A path of the currently opened file (relative to worktree root).
+    RelativeFile,
+    /// The currently opened filename.
     Filename,
-    /// stem (filename without extension) of the currently opened file.
+    /// The path to a parent directory of a currently opened file.
+    Dirname,
+    /// Stem (filename without extension) of the currently opened file.
     Stem,
     /// An absolute path of the currently opened worktree, that contains the file.
     WorktreeRoot,
@@ -165,6 +169,8 @@ impl std::fmt::Display for VariableName {
         match self {
             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::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"),
             Self::Symbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SYMBOL"),

crates/tasks_ui/src/lib.rs 🔗

@@ -261,6 +261,8 @@ mod tests {
                     task_variables: TaskVariables::from_iter([
                         (VariableName::File, "/dir/rust/b.rs".into()),
                         (VariableName::Filename, "b.rs".into()),
+                        (VariableName::RelativeFile, "rust/b.rs".into()),
+                        (VariableName::Dirname, "/dir/rust".into()),
                         (VariableName::Stem, "b".into()),
                         (VariableName::WorktreeRoot, "/dir".into()),
                         (VariableName::Row, "1".into()),
@@ -279,6 +281,8 @@ mod tests {
                     task_variables: TaskVariables::from_iter([
                         (VariableName::File, "/dir/rust/b.rs".into()),
                         (VariableName::Filename, "b.rs".into()),
+                        (VariableName::RelativeFile, "rust/b.rs".into()),
+                        (VariableName::Dirname, "/dir/rust".into()),
                         (VariableName::Stem, "b".into()),
                         (VariableName::WorktreeRoot, "/dir".into()),
                         (VariableName::Row, "1".into()),
@@ -298,6 +302,8 @@ mod tests {
                     task_variables: TaskVariables::from_iter([
                         (VariableName::File, "/dir/a.ts".into()),
                         (VariableName::Filename, "a.ts".into()),
+                        (VariableName::RelativeFile, "a.ts".into()),
+                        (VariableName::Dirname, "/dir".into()),
                         (VariableName::Stem, "a".into()),
                         (VariableName::WorktreeRoot, "/dir".into()),
                         (VariableName::Row, "1".into()),

docs/src/tasks.md 🔗

@@ -45,6 +45,8 @@ These variables allow you to pull information from the current editor and use it
 - `ZED_ROW`: current line row
 - `ZED_FILE`: absolute path of the currently opened file (e.g. `/Users/my-user/path/to/project/src/main.rs`)
 - `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_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