Fix running Python commands that include paths with spaces (#21981)

Silvano Cerza and Piotr Osiewicz created

This PR fixes running Python commands that include paths with spaces by
wrapping python commands and their arguments in quotation marks.

I fixed this only in Python as I noticed this while trying to run
`pytest` in Zed.

Probably this is not the best approach as it doesn't fix other languages
too, though I don't know enough about the codebase to fix it like that.
I'm not even sure if it's actually feasible right now.

I didn't add tests for this either as I couldn't really understand how
to easily to that, I tried to look at other languages but couldn't find
one that tests their `ContextProvider` directly.

Release Notes:

- Fix running Python commands that include paths with spaces

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

crates/languages/src/python.rs | 20 +++++++++++++-------
crates/task/src/lib.rs         |  4 ++++
2 files changed, 17 insertions(+), 7 deletions(-)

Detailed changes

crates/languages/src/python.rs 🔗

@@ -315,7 +315,10 @@ impl ContextProvider for PythonContextProvider {
                 toolchains
                     .active_toolchain(worktree_id, "Python".into(), &mut cx)
                     .await
-                    .map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
+                    .map_or_else(
+                        || "python3".to_owned(),
+                        |toolchain| format!("\"{}\"", toolchain.path),
+                    )
             } else {
                 String::from("python3")
             };
@@ -336,14 +339,17 @@ impl ContextProvider for PythonContextProvider {
             TaskTemplate {
                 label: "execute selection".to_owned(),
                 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
-                args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
+                args: vec![
+                    "-c".to_owned(),
+                    VariableName::SelectedText.template_value_with_whitespace(),
+                ],
                 ..TaskTemplate::default()
             },
             // Execute an entire file
             TaskTemplate {
                 label: format!("run '{}'", VariableName::File.template_value()),
                 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
-                args: vec![VariableName::File.template_value()],
+                args: vec![VariableName::File.template_value_with_whitespace()],
                 ..TaskTemplate::default()
             },
         ];
@@ -358,7 +364,7 @@ impl ContextProvider for PythonContextProvider {
                         args: vec![
                             "-m".to_owned(),
                             "unittest".to_owned(),
-                            VariableName::File.template_value(),
+                            VariableName::File.template_value_with_whitespace(),
                         ],
                         ..TaskTemplate::default()
                     },
@@ -369,7 +375,7 @@ impl ContextProvider for PythonContextProvider {
                         args: vec![
                             "-m".to_owned(),
                             "unittest".to_owned(),
-                            "$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
+                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace()
                         ],
                         tags: vec![
                             "python-unittest-class".to_owned(),
@@ -388,7 +394,7 @@ impl ContextProvider for PythonContextProvider {
                         args: vec![
                             "-m".to_owned(),
                             "pytest".to_owned(),
-                            VariableName::File.template_value(),
+                            VariableName::File.template_value_with_whitespace(),
                         ],
                         ..TaskTemplate::default()
                     },
@@ -399,7 +405,7 @@ impl ContextProvider for PythonContextProvider {
                         args: vec![
                             "-m".to_owned(),
                             "pytest".to_owned(),
-                            "$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
+                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
                         ],
                         tags: vec![
                             "python-pytest-class".to_owned(),

crates/task/src/lib.rs 🔗

@@ -135,6 +135,10 @@ impl VariableName {
     pub fn template_value(&self) -> String {
         format!("${self}")
     }
+    /// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters.
+    pub fn template_value_with_whitespace(&self) -> String {
+        format!("\"${self}\"")
+    }
 }
 
 impl FromStr for VariableName {