go: Add runnables for Go (#12003)

Anıl Şenay , Thorsten Ball , and Piotr Osiewicz created

Implemented runnables for specially for running tests for Go.

I'm grateful for your feedback because this is my first experience with
Rust and Zed codebase.
![resim](https://github.com/zed-industries/zed/assets/1047345/789b31da-554f-47cd-a08c-444eced104f4)

https://github.com/zed-industries/zed/assets/1047345/ae1abd9e-3657-4322-9c28-02d0752b5ccd


Release Notes:

- Added Runnables/Tasks for:
  - Run test functions which start with "Test"
  - Run subtests
  - Run benchmark tests
  - Run main function



---------

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

Change summary

crates/languages/src/go.rs            | 87 ++++++++++++++++++++++++----
crates/languages/src/go/runnables.scm | 67 +++++++++++++++++++---
crates/languages/src/lib.rs           |  1 
crates/task/src/task_template.rs      | 23 +++++++
4 files changed, 155 insertions(+), 23 deletions(-)

Detailed changes

crates/languages/src/go.rs πŸ”—

@@ -39,6 +39,9 @@ impl GoLspAdapter {
 
 lazy_static! {
     static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap();
+    static ref GO_EXTRACT_SUBTEST_NAME_REGEX: Regex =
+        Regex::new(r#".*t\.Run\("([^"]*)".*"#).unwrap();
+    static ref GO_ESCAPE_SUBTEST_NAME_REGEX: Regex = Regex::new(r#"[.*+?^${}()|\[\]\\]"#).unwrap();
 }
 
 #[async_trait(?Send)]
@@ -443,6 +446,8 @@ fn adjust_runs(
 pub(crate) struct GoContextProvider;
 
 const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
+const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
 
 impl ContextProvider for GoContextProvider {
     fn build_context(
@@ -457,11 +462,10 @@ impl ContextProvider for GoContextProvider {
             .file()
             .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 
-        Ok(
-            if let Some(buffer_dir) = local_abs_path
-                .as_deref()
-                .and_then(|local_abs_path| local_abs_path.parent())
-            {
+        let go_package_variable = local_abs_path
+            .as_deref()
+            .and_then(|local_abs_path| local_abs_path.parent())
+            .map(|buffer_dir| {
                 // Prefer the relative form `./my-nested-package/is-here` over
                 // absolute path, because it's more readable in the modal, but
                 // the absolute path also works.
@@ -477,14 +481,19 @@ impl ContextProvider for GoContextProvider {
                     })
                     .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
 
-                TaskVariables::from_iter(Some((
-                    GO_PACKAGE_TASK_VARIABLE.clone(),
-                    package_name.to_string(),
-                )))
-            } else {
-                TaskVariables::default()
-            },
-        )
+                (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string())
+            });
+
+        let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
+
+        let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
+            .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
+
+        Ok(TaskVariables::from_iter(
+            [go_package_variable, go_subtest_variable]
+                .into_iter()
+                .flatten(),
+        ))
     }
 
     fn associated_tasks(&self) -> Option<TaskTemplates> {
@@ -517,6 +526,46 @@ impl ContextProvider for GoContextProvider {
                 args: vec!["test".into(), "./...".into()],
                 ..TaskTemplate::default()
             },
+            TaskTemplate {
+                label: format!(
+                    "go test {} -run {}/{}",
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    VariableName::Symbol.template_value(),
+                    GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
+                ),
+                command: "go".into(),
+                args: vec![
+                    "test".into(),
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    "-v".into(),
+                    "-run".into(),
+                    format!(
+                        "^{}$/^{}$",
+                        VariableName::Symbol.template_value(),
+                        GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
+                    ),
+                ],
+                tags: vec!["go-subtest".to_owned()],
+                ..TaskTemplate::default()
+            },
+            TaskTemplate {
+                label: format!(
+                    "go test {} -bench {}",
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    VariableName::Symbol.template_value()
+                ),
+                command: "go".into(),
+                args: vec![
+                    "test".into(),
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    "-benchmem".into(),
+                    "-run=^$".into(),
+                    "-bench".into(),
+                    format!("^{}$", VariableName::Symbol.template_value()),
+                ],
+                tags: vec!["go-benchmark".to_owned()],
+                ..TaskTemplate::default()
+            },
             TaskTemplate {
                 label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
                 command: "go".into(),
@@ -528,6 +577,18 @@ impl ContextProvider for GoContextProvider {
     }
 }
 
+fn extract_subtest_name(input: &str) -> Option<String> {
+    let replaced_spaces = input.trim_matches('"').replace(' ', "_");
+
+    Some(
+        GO_ESCAPE_SUBTEST_NAME_REGEX
+            .replace_all(&replaced_spaces, |caps: &regex::Captures| {
+                format!("\\{}", &caps[0])
+            })
+            .to_string(),
+    )
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/languages/src/go/runnables.scm πŸ”—

@@ -1,15 +1,62 @@
+; Functions names start with `Test`
 (
-    (
-        (function_declaration name: (_) @run
-            (#match? @run "^Test.*"))
-    ) @_
-    (#set! tag go-test)
+  (
+    (function_declaration name: (_) @run
+      (#match? @run "^Test.*"))
+  ) @_
+  (#set! tag go-test)
 )
 
+; `t.Run`
 (
-    (
-        (function_declaration name: (_) @run
-            (#eq? @run "main"))
-    ) @_
-    (#set! tag go-main)
+  (
+    (call_expression
+      function: (
+        selector_expression
+        field: _ @run @_name
+        (#eq? @_name "Run")
+      )
+      arguments: (
+        argument_list
+        .
+        (interpreted_string_literal) @_subtest_name
+        .
+        (func_literal
+          parameters: (
+            parameter_list
+            (parameter_declaration
+              name: (identifier) @_param_name
+              type: (pointer_type
+                (qualified_type
+                  package: (package_identifier) @_pkg
+                  name: (type_identifier) @_type
+                  (#eq? @_pkg "testing")
+                  (#eq? @_type "T")
+                )
+              )
+            )
+          )
+        ) @_second_argument
+      )
+    )
+  ) @_
+  (#set! tag go-subtest)
+)
+
+; Functions names start with `Benchmark`
+(
+  (
+    (function_declaration name: (_) @run @_name
+      (#match? @_name "^Benchmark.+"))
+  ) @_
+  (#set! tag go-benchmark)
+)
+
+; go run
+(
+  (
+    (function_declaration name: (_) @run
+      (#eq? @run "main"))
+  ) @_
+  (#set! tag go-main)
 )

crates/languages/src/lib.rs πŸ”—

@@ -113,6 +113,7 @@ pub fn init(
         vec![Arc::new(go::GoLspAdapter)],
         GoContextProvider
     );
+
     language!(
         "json",
         vec![Arc::new(json::JsonLspAdapter::new(

crates/task/src/task_template.rs πŸ”—

@@ -679,4 +679,27 @@ mod tests {
         expected.sort_by_key(|var| var.to_string());
         assert_eq!(resolved_variables, expected)
     }
+
+    #[test]
+    fn substitute_funky_labels() {
+        let faulty_go_test = TaskTemplate {
+            label: format!(
+                "go test {}/{}",
+                VariableName::Symbol.template_value(),
+                VariableName::Symbol.template_value(),
+            ),
+            command: "go".into(),
+            args: vec![format!(
+                "^{}$/^{}$",
+                VariableName::Symbol.template_value(),
+                VariableName::Symbol.template_value()
+            )],
+            ..TaskTemplate::default()
+        };
+        let mut context = TaskContext::default();
+        context
+            .task_variables
+            .insert(VariableName::Symbol, "my-symbol".to_string());
+        assert!(faulty_go_test.resolve_task("base", &context).is_some());
+    }
 }