go: Add runnables (#12110)

Thorsten Ball created

This adds support for runnables to Go.

It adds the following tasks:

- `go test $ZED_GO_PACKAGE -run $ZED_SYMBOL`
- `go test $ZED_GO_PACKAGE`
- `go test ./...`
- `go run $ZED_GO_PACKAGE` if it has a `main` function

Release Notes:

- Added built-in Go runnables and tasks that allow users to run Go test
functions, test packages, or run `main` functions.

Demo:



https://github.com/zed-industries/zed/assets/1185253/a6271d80-faf4-466a-bf63-efbec8fe6c35




https://github.com/zed-industries/zed/assets/1185253/92f2b616-7501-463d-b613-1ec1084ae0cd

Change summary

crates/languages/src/go.rs            | 91 ++++++++++++++++++++++++++++
crates/languages/src/go/runnables.scm |  9 ++
crates/languages/src/lib.rs           |  7 +
3 files changed, 104 insertions(+), 3 deletions(-)

Detailed changes

crates/languages/src/go.rs 🔗

@@ -13,15 +13,17 @@ use settings::Settings;
 use smol::{fs, process};
 use std::{
     any::Any,
+    borrow::Cow,
     ffi::{OsStr, OsString},
     ops::Range,
-    path::PathBuf,
+    path::{Path, PathBuf},
     str,
     sync::{
         atomic::{AtomicBool, Ordering::SeqCst},
         Arc,
     },
 };
+use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
 use util::{fs::remove_matching, maybe, ResultExt};
 
 fn server_binary_arguments() -> Vec<OsString> {
@@ -438,6 +440,93 @@ fn adjust_runs(
     runs
 }
 
+pub(crate) struct GoContextProvider;
+
+const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
+
+impl ContextProvider for GoContextProvider {
+    fn build_context(
+        &self,
+        worktree_abs_path: Option<&Path>,
+        location: &Location,
+        cx: &mut gpui::AppContext,
+    ) -> Result<TaskVariables> {
+        let local_abs_path = location
+            .buffer
+            .read(cx)
+            .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())
+            {
+                // 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.
+                let package_name = worktree_abs_path
+                    .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())
+                    .map(|relative_pkg_dir| {
+                        if relative_pkg_dir.as_os_str().is_empty() {
+                            ".".into()
+                        } else {
+                            format!("./{}", relative_pkg_dir.to_string_lossy())
+                        }
+                    })
+                    .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()
+            },
+        )
+    }
+
+    fn associated_tasks(&self) -> Option<TaskTemplates> {
+        Some(TaskTemplates(vec![
+            TaskTemplate {
+                label: format!(
+                    "go test {} -run {}",
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    VariableName::Symbol.template_value(),
+                ),
+                command: "go".into(),
+                args: vec![
+                    "test".into(),
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    "-run".into(),
+                    VariableName::Symbol.template_value(),
+                ],
+                tags: vec!["go-test".to_owned()],
+                ..TaskTemplate::default()
+            },
+            TaskTemplate {
+                label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
+                command: "go".into(),
+                args: vec!["test".into(), GO_PACKAGE_TASK_VARIABLE.template_value()],
+                ..TaskTemplate::default()
+            },
+            TaskTemplate {
+                label: "go test ./...".into(),
+                command: "go".into(),
+                args: vec!["test".into(), "./...".into()],
+                ..TaskTemplate::default()
+            },
+            TaskTemplate {
+                label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
+                command: "go".into(),
+                args: vec!["run".into(), GO_PACKAGE_TASK_VARIABLE.template_value()],
+                tags: vec!["go-main".to_owned()],
+                ..TaskTemplate::default()
+            },
+        ]))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/languages/src/go/runnables.scm 🔗

@@ -0,0 +1,9 @@
+(
+    (function_declaration name: (_) @run
+        (#match? @run "^Test.*"))
+) @go-test
+
+(
+    (function_declaration name: (_) @run
+        (#eq? @run "main"))
+) @go-main

crates/languages/src/lib.rs 🔗

@@ -8,7 +8,10 @@ use smol::stream::StreamExt;
 use std::{str, sync::Arc};
 use util::{asset_str, ResultExt};
 
-use crate::{bash::bash_task_context, python::python_task_context, rust::RustContextProvider};
+use crate::{
+    bash::bash_task_context, go::GoContextProvider, python::python_task_context,
+    rust::RustContextProvider,
+};
 
 mod bash;
 mod c;
@@ -103,7 +106,7 @@ pub fn init(
         "css",
         vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),]
     );
-    language!("go", vec![Arc::new(go::GoLspAdapter)]);
+    language!("go", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
     language!("gomod");
     language!("gowork");
     language!(