go.rs

   1use anyhow::{Context as _, Result};
   2use async_trait::async_trait;
   3use collections::HashMap;
   4use futures::StreamExt;
   5use gpui::{App, AsyncApp, Task};
   6use http_client::github::latest_github_release;
   7pub use language::*;
   8use language::{LanguageToolchainStore, LspAdapterDelegate, LspInstaller};
   9use lsp::{LanguageServerBinary, LanguageServerName};
  10
  11use regex::Regex;
  12use serde_json::json;
  13use smol::fs;
  14use std::{
  15    borrow::Cow,
  16    ffi::{OsStr, OsString},
  17    ops::Range,
  18    path::{Path, PathBuf},
  19    process::Output,
  20    str,
  21    sync::{
  22        Arc, LazyLock,
  23        atomic::{AtomicBool, Ordering::SeqCst},
  24    },
  25};
  26use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
  27use util::{ResultExt, fs::remove_matching, maybe};
  28
  29fn server_binary_arguments() -> Vec<OsString> {
  30    vec!["-mode=stdio".into()]
  31}
  32
  33#[derive(Copy, Clone)]
  34pub struct GoLspAdapter;
  35
  36impl GoLspAdapter {
  37    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls");
  38}
  39
  40static VERSION_REGEX: LazyLock<Regex> =
  41    LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create VERSION_REGEX"));
  42
  43static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
  44    Regex::new(r#"[.*+?^${}()|\[\]\\"']"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")
  45});
  46
  47const BINARY: &str = if cfg!(target_os = "windows") {
  48    "gopls.exe"
  49} else {
  50    "gopls"
  51};
  52
  53impl LspInstaller for GoLspAdapter {
  54    type BinaryVersion = Option<String>;
  55
  56    async fn fetch_latest_server_version(
  57        &self,
  58        delegate: &dyn LspAdapterDelegate,
  59        _: bool,
  60        cx: &mut AsyncApp,
  61    ) -> Result<Option<String>> {
  62        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
  63
  64        const NOTIFICATION_MESSAGE: &str =
  65            "Could not install the Go language server `gopls`, because `go` was not found.";
  66
  67        if delegate.which("go".as_ref()).await.is_none() {
  68            if DID_SHOW_NOTIFICATION
  69                .compare_exchange(false, true, SeqCst, SeqCst)
  70                .is_ok()
  71            {
  72                cx.update(|cx| {
  73                    delegate.show_notification(NOTIFICATION_MESSAGE, cx);
  74                })?
  75            }
  76            anyhow::bail!(
  77                "Could not install the Go language server `gopls`, because `go` was not found."
  78            );
  79        }
  80
  81        let release =
  82            latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
  83        let version: Option<String> = release.tag_name.strip_prefix("gopls/v").map(str::to_string);
  84        if version.is_none() {
  85            log::warn!(
  86                "couldn't infer gopls version from GitHub release tag name '{}'",
  87                release.tag_name
  88            );
  89        }
  90        Ok(version)
  91    }
  92
  93    async fn check_if_user_installed(
  94        &self,
  95        delegate: &dyn LspAdapterDelegate,
  96        _: Option<Toolchain>,
  97        _: &AsyncApp,
  98    ) -> Option<LanguageServerBinary> {
  99        let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
 100        Some(LanguageServerBinary {
 101            path,
 102            arguments: server_binary_arguments(),
 103            env: None,
 104        })
 105    }
 106
 107    async fn fetch_server_binary(
 108        &self,
 109        version: Option<String>,
 110        container_dir: PathBuf,
 111        delegate: &dyn LspAdapterDelegate,
 112    ) -> Result<LanguageServerBinary> {
 113        let go = delegate.which("go".as_ref()).await.unwrap_or("go".into());
 114        let go_version_output = util::command::new_smol_command(&go)
 115            .args(["version"])
 116            .output()
 117            .await
 118            .context("failed to get go version via `go version` command`")?;
 119        let go_version = parse_version_output(&go_version_output)?;
 120
 121        if let Some(version) = version {
 122            let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));
 123            if let Ok(metadata) = fs::metadata(&binary_path).await
 124                && metadata.is_file()
 125            {
 126                remove_matching(&container_dir, |entry| {
 127                    entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
 128                })
 129                .await;
 130
 131                return Ok(LanguageServerBinary {
 132                    path: binary_path.to_path_buf(),
 133                    arguments: server_binary_arguments(),
 134                    env: None,
 135                });
 136            }
 137        } else if let Some(path) = get_cached_server_binary(&container_dir).await {
 138            return Ok(path);
 139        }
 140
 141        let gobin_dir = container_dir.join("gobin");
 142        fs::create_dir_all(&gobin_dir).await?;
 143        let install_output = util::command::new_smol_command(go)
 144            .env("GO111MODULE", "on")
 145            .env("GOBIN", &gobin_dir)
 146            .args(["install", "golang.org/x/tools/gopls@latest"])
 147            .output()
 148            .await?;
 149
 150        if !install_output.status.success() {
 151            log::error!(
 152                "failed to install gopls via `go install`. stdout: {:?}, stderr: {:?}",
 153                String::from_utf8_lossy(&install_output.stdout),
 154                String::from_utf8_lossy(&install_output.stderr)
 155            );
 156            anyhow::bail!(
 157                "failed to install gopls with `go install`. Is `go` installed and in the PATH? Check logs for more information."
 158            );
 159        }
 160
 161        let installed_binary_path = gobin_dir.join(BINARY);
 162        let version_output = util::command::new_smol_command(&installed_binary_path)
 163            .arg("version")
 164            .output()
 165            .await
 166            .context("failed to run installed gopls binary")?;
 167        let gopls_version = parse_version_output(&version_output)?;
 168        let binary_path = container_dir.join(format!("gopls_{gopls_version}_go_{go_version}"));
 169        fs::rename(&installed_binary_path, &binary_path).await?;
 170
 171        Ok(LanguageServerBinary {
 172            path: binary_path.to_path_buf(),
 173            arguments: server_binary_arguments(),
 174            env: None,
 175        })
 176    }
 177
 178    async fn cached_server_binary(
 179        &self,
 180        container_dir: PathBuf,
 181        _: &dyn LspAdapterDelegate,
 182    ) -> Option<LanguageServerBinary> {
 183        get_cached_server_binary(&container_dir).await
 184    }
 185}
 186
 187#[async_trait(?Send)]
 188impl LspAdapter for GoLspAdapter {
 189    fn name(&self) -> LanguageServerName {
 190        Self::SERVER_NAME
 191    }
 192
 193    async fn initialization_options(
 194        self: Arc<Self>,
 195        _: &Arc<dyn LspAdapterDelegate>,
 196    ) -> Result<Option<serde_json::Value>> {
 197        Ok(Some(json!({
 198            "usePlaceholders": false,
 199            "hints": {
 200                "assignVariableTypes": true,
 201                "compositeLiteralFields": true,
 202                "compositeLiteralTypes": true,
 203                "constantValues": true,
 204                "functionTypeParameters": true,
 205                "parameterNames": true,
 206                "rangeVariableTypes": true
 207            }
 208        })))
 209    }
 210
 211    async fn label_for_completion(
 212        &self,
 213        completion: &lsp::CompletionItem,
 214        language: &Arc<Language>,
 215    ) -> Option<CodeLabel> {
 216        let label = &completion.label;
 217
 218        // Gopls returns nested fields and methods as completions.
 219        // To syntax highlight these, combine their final component
 220        // with their detail.
 221        let name_offset = label.rfind('.').unwrap_or(0);
 222
 223        match completion.kind.zip(completion.detail.as_ref()) {
 224            Some((lsp::CompletionItemKind::MODULE, detail)) => {
 225                let text = format!("{label} {detail}");
 226                let source = Rope::from(format!("import {text}").as_str());
 227                let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
 228                let filter_range = completion
 229                    .filter_text
 230                    .as_deref()
 231                    .and_then(|filter_text| {
 232                        text.find(filter_text)
 233                            .map(|start| start..start + filter_text.len())
 234                    })
 235                    .unwrap_or(0..label.len());
 236                return Some(CodeLabel::new(text, filter_range, runs));
 237            }
 238            Some((
 239                lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
 240                detail,
 241            )) => {
 242                let text = format!("{label} {detail}");
 243                let source =
 244                    Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
 245                let runs = adjust_runs(
 246                    name_offset,
 247                    language.highlight_text(&source, 4..4 + text[name_offset..].len()),
 248                );
 249                let filter_range = completion
 250                    .filter_text
 251                    .as_deref()
 252                    .and_then(|filter_text| {
 253                        text.find(filter_text)
 254                            .map(|start| start..start + filter_text.len())
 255                    })
 256                    .unwrap_or(0..label.len());
 257                return Some(CodeLabel::new(text, filter_range, runs));
 258            }
 259            Some((lsp::CompletionItemKind::STRUCT, _)) => {
 260                let text = format!("{label} struct {{}}");
 261                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
 262                let runs = adjust_runs(
 263                    name_offset,
 264                    language.highlight_text(&source, 5..5 + text[name_offset..].len()),
 265                );
 266                let filter_range = completion
 267                    .filter_text
 268                    .as_deref()
 269                    .and_then(|filter_text| {
 270                        text.find(filter_text)
 271                            .map(|start| start..start + filter_text.len())
 272                    })
 273                    .unwrap_or(0..label.len());
 274                return Some(CodeLabel::new(text, filter_range, runs));
 275            }
 276            Some((lsp::CompletionItemKind::INTERFACE, _)) => {
 277                let text = format!("{label} interface {{}}");
 278                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
 279                let runs = adjust_runs(
 280                    name_offset,
 281                    language.highlight_text(&source, 5..5 + text[name_offset..].len()),
 282                );
 283                let filter_range = completion
 284                    .filter_text
 285                    .as_deref()
 286                    .and_then(|filter_text| {
 287                        text.find(filter_text)
 288                            .map(|start| start..start + filter_text.len())
 289                    })
 290                    .unwrap_or(0..label.len());
 291                return Some(CodeLabel::new(text, filter_range, runs));
 292            }
 293            Some((lsp::CompletionItemKind::FIELD, detail)) => {
 294                let text = format!("{label} {detail}");
 295                let source =
 296                    Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
 297                let runs = adjust_runs(
 298                    name_offset,
 299                    language.highlight_text(&source, 16..16 + text[name_offset..].len()),
 300                );
 301                let filter_range = completion
 302                    .filter_text
 303                    .as_deref()
 304                    .and_then(|filter_text| {
 305                        text.find(filter_text)
 306                            .map(|start| start..start + filter_text.len())
 307                    })
 308                    .unwrap_or(0..label.len());
 309                return Some(CodeLabel::new(text, filter_range, runs));
 310            }
 311            Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
 312                if let Some(signature) = detail.strip_prefix("func") {
 313                    let text = format!("{label}{signature}");
 314                    let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
 315                    let runs = adjust_runs(
 316                        name_offset,
 317                        language.highlight_text(&source, 5..5 + text[name_offset..].len()),
 318                    );
 319                    let filter_range = completion
 320                        .filter_text
 321                        .as_deref()
 322                        .and_then(|filter_text| {
 323                            text.find(filter_text)
 324                                .map(|start| start..start + filter_text.len())
 325                        })
 326                        .unwrap_or(0..label.len());
 327                    return Some(CodeLabel::new(text, filter_range, runs));
 328                }
 329            }
 330            _ => {}
 331        }
 332        None
 333    }
 334
 335    async fn label_for_symbol(
 336        &self,
 337        name: &str,
 338        kind: lsp::SymbolKind,
 339        language: &Arc<Language>,
 340    ) -> Option<CodeLabel> {
 341        let (text, filter_range, display_range) = match kind {
 342            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 343                let text = format!("func {} () {{}}", name);
 344                let filter_range = 5..5 + name.len();
 345                let display_range = 0..filter_range.end;
 346                (text, filter_range, display_range)
 347            }
 348            lsp::SymbolKind::STRUCT => {
 349                let text = format!("type {} struct {{}}", name);
 350                let filter_range = 5..5 + name.len();
 351                let display_range = 0..text.len();
 352                (text, filter_range, display_range)
 353            }
 354            lsp::SymbolKind::INTERFACE => {
 355                let text = format!("type {} interface {{}}", name);
 356                let filter_range = 5..5 + name.len();
 357                let display_range = 0..text.len();
 358                (text, filter_range, display_range)
 359            }
 360            lsp::SymbolKind::CLASS => {
 361                let text = format!("type {} T", name);
 362                let filter_range = 5..5 + name.len();
 363                let display_range = 0..filter_range.end;
 364                (text, filter_range, display_range)
 365            }
 366            lsp::SymbolKind::CONSTANT => {
 367                let text = format!("const {} = nil", name);
 368                let filter_range = 6..6 + name.len();
 369                let display_range = 0..filter_range.end;
 370                (text, filter_range, display_range)
 371            }
 372            lsp::SymbolKind::VARIABLE => {
 373                let text = format!("var {} = nil", name);
 374                let filter_range = 4..4 + name.len();
 375                let display_range = 0..filter_range.end;
 376                (text, filter_range, display_range)
 377            }
 378            lsp::SymbolKind::MODULE => {
 379                let text = format!("package {}", name);
 380                let filter_range = 8..8 + name.len();
 381                let display_range = 0..filter_range.end;
 382                (text, filter_range, display_range)
 383            }
 384            _ => return None,
 385        };
 386
 387        Some(CodeLabel::new(
 388            text[display_range.clone()].to_string(),
 389            filter_range,
 390            language.highlight_text(&text.as_str().into(), display_range),
 391        ))
 392    }
 393
 394    fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
 395        static REGEX: LazyLock<Regex> =
 396            LazyLock::new(|| Regex::new(r"(?m)\n\s*").expect("Failed to create REGEX"));
 397        Some(REGEX.replace_all(message, "\n\n").to_string())
 398    }
 399}
 400
 401fn parse_version_output(output: &Output) -> Result<&str> {
 402    let version_stdout =
 403        str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?;
 404
 405    let version = VERSION_REGEX
 406        .find(version_stdout)
 407        .with_context(|| format!("failed to parse version output '{version_stdout}'"))?
 408        .as_str();
 409
 410    Ok(version)
 411}
 412
 413async fn get_cached_server_binary(container_dir: &Path) -> Option<LanguageServerBinary> {
 414    maybe!(async {
 415        let mut last_binary_path = None;
 416        let mut entries = fs::read_dir(container_dir).await?;
 417        while let Some(entry) = entries.next().await {
 418            let entry = entry?;
 419            if entry.file_type().await?.is_file()
 420                && entry
 421                    .file_name()
 422                    .to_str()
 423                    .is_some_and(|name| name.starts_with("gopls_"))
 424            {
 425                last_binary_path = Some(entry.path());
 426            }
 427        }
 428
 429        let path = last_binary_path.context("no cached binary")?;
 430        anyhow::Ok(LanguageServerBinary {
 431            path,
 432            arguments: server_binary_arguments(),
 433            env: None,
 434        })
 435    })
 436    .await
 437    .log_err()
 438}
 439
 440fn adjust_runs(
 441    delta: usize,
 442    mut runs: Vec<(Range<usize>, HighlightId)>,
 443) -> Vec<(Range<usize>, HighlightId)> {
 444    for (range, _) in &mut runs {
 445        range.start += delta;
 446        range.end += delta;
 447    }
 448    runs
 449}
 450
 451pub(crate) struct GoContextProvider;
 452
 453const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
 454const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
 455    VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
 456const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
 457    VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
 458const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName =
 459    VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME"));
 460const GO_SUITE_NAME_TASK_VARIABLE: VariableName =
 461    VariableName::Custom(Cow::Borrowed("GO_SUITE_NAME"));
 462
 463impl ContextProvider for GoContextProvider {
 464    fn build_context(
 465        &self,
 466        variables: &TaskVariables,
 467        location: ContextLocation<'_>,
 468        _: Option<HashMap<String, String>>,
 469        _: Arc<dyn LanguageToolchainStore>,
 470        cx: &mut gpui::App,
 471    ) -> Task<Result<TaskVariables>> {
 472        let local_abs_path = location
 473            .file_location
 474            .buffer
 475            .read(cx)
 476            .file()
 477            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 478
 479        let go_package_variable = local_abs_path
 480            .as_deref()
 481            .and_then(|local_abs_path| local_abs_path.parent())
 482            .map(|buffer_dir| {
 483                // Prefer the relative form `./my-nested-package/is-here` over
 484                // absolute path, because it's more readable in the modal, but
 485                // the absolute path also works.
 486                let package_name = variables
 487                    .get(&VariableName::WorktreeRoot)
 488                    .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())
 489                    .map(|relative_pkg_dir| {
 490                        if relative_pkg_dir.as_os_str().is_empty() {
 491                            ".".into()
 492                        } else {
 493                            format!("./{}", relative_pkg_dir.to_string_lossy())
 494                        }
 495                    })
 496                    .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
 497
 498                (GO_PACKAGE_TASK_VARIABLE.clone(), package_name)
 499            });
 500
 501        let go_module_root_variable = local_abs_path
 502            .as_deref()
 503            .and_then(|local_abs_path| local_abs_path.parent())
 504            .map(|buffer_dir| {
 505                // Walk dirtree up until getting the first go.mod file
 506                let module_dir = buffer_dir
 507                    .ancestors()
 508                    .find(|dir| dir.join("go.mod").is_file())
 509                    .map(|dir| dir.to_string_lossy().into_owned())
 510                    .unwrap_or_else(|| ".".to_string());
 511
 512                (GO_MODULE_ROOT_TASK_VARIABLE.clone(), module_dir)
 513            });
 514
 515        let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
 516
 517        let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
 518            .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
 519
 520        let _table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed(
 521            "_table_test_case_name",
 522        )));
 523
 524        let go_table_test_case_variable = _table_test_case_name
 525            .and_then(extract_subtest_name)
 526            .map(|case_name| (GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.clone(), case_name));
 527
 528        let _suite_name = variables.get(&VariableName::Custom(Cow::Borrowed("_suite_name")));
 529
 530        let go_suite_variable = _suite_name
 531            .and_then(extract_subtest_name)
 532            .map(|suite_name| (GO_SUITE_NAME_TASK_VARIABLE.clone(), suite_name));
 533
 534        Task::ready(Ok(TaskVariables::from_iter(
 535            [
 536                go_package_variable,
 537                go_subtest_variable,
 538                go_table_test_case_variable,
 539                go_suite_variable,
 540                go_module_root_variable,
 541            ]
 542            .into_iter()
 543            .flatten(),
 544        )))
 545    }
 546
 547    fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Task<Option<TaskTemplates>> {
 548        let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
 549            None
 550        } else {
 551            Some("$ZED_DIRNAME".to_string())
 552        };
 553        let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
 554
 555        Task::ready(Some(TaskTemplates(vec![
 556            TaskTemplate {
 557                label: format!(
 558                    "go test {} -v -run Test{}/{}",
 559                    GO_PACKAGE_TASK_VARIABLE.template_value(),
 560                    GO_SUITE_NAME_TASK_VARIABLE.template_value(),
 561                    VariableName::Symbol.template_value(),
 562                ),
 563                command: "go".into(),
 564                args: vec![
 565                    "test".into(),
 566                    "-v".into(),
 567                    "-run".into(),
 568                    format!(
 569                        "\\^Test{}\\$/\\^{}\\$",
 570                        GO_SUITE_NAME_TASK_VARIABLE.template_value(),
 571                        VariableName::Symbol.template_value(),
 572                    ),
 573                ],
 574                cwd: package_cwd.clone(),
 575                tags: vec!["go-testify-suite".to_owned()],
 576                ..TaskTemplate::default()
 577            },
 578            TaskTemplate {
 579                label: format!(
 580                    "go test {} -v -run {}/{}",
 581                    GO_PACKAGE_TASK_VARIABLE.template_value(),
 582                    VariableName::Symbol.template_value(),
 583                    GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
 584                ),
 585                command: "go".into(),
 586                args: vec![
 587                    "test".into(),
 588                    "-v".into(),
 589                    "-run".into(),
 590                    format!(
 591                        "\\^{}\\$/\\^{}\\$",
 592                        VariableName::Symbol.template_value(),
 593                        GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
 594                    ),
 595                ],
 596                cwd: package_cwd.clone(),
 597                tags: vec!["go-table-test-case".to_owned()],
 598                ..TaskTemplate::default()
 599            },
 600            TaskTemplate {
 601                label: format!(
 602                    "go test {} -run {}",
 603                    GO_PACKAGE_TASK_VARIABLE.template_value(),
 604                    VariableName::Symbol.template_value(),
 605                ),
 606                command: "go".into(),
 607                args: vec![
 608                    "test".into(),
 609                    "-run".into(),
 610                    format!("\\^{}\\$", VariableName::Symbol.template_value(),),
 611                ],
 612                tags: vec!["go-test".to_owned()],
 613                cwd: package_cwd.clone(),
 614                ..TaskTemplate::default()
 615            },
 616            TaskTemplate {
 617                label: format!(
 618                    "go test {} -run {}",
 619                    GO_PACKAGE_TASK_VARIABLE.template_value(),
 620                    VariableName::Symbol.template_value(),
 621                ),
 622                command: "go".into(),
 623                args: vec![
 624                    "test".into(),
 625                    "-run".into(),
 626                    format!("\\^{}\\$", VariableName::Symbol.template_value(),),
 627                ],
 628                tags: vec!["go-example".to_owned()],
 629                cwd: package_cwd.clone(),
 630                ..TaskTemplate::default()
 631            },
 632            TaskTemplate {
 633                label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
 634                command: "go".into(),
 635                args: vec!["test".into()],
 636                cwd: package_cwd.clone(),
 637                ..TaskTemplate::default()
 638            },
 639            TaskTemplate {
 640                label: "go test ./...".into(),
 641                command: "go".into(),
 642                args: vec!["test".into(), "./...".into()],
 643                cwd: module_cwd.clone(),
 644                ..TaskTemplate::default()
 645            },
 646            TaskTemplate {
 647                label: format!(
 648                    "go test {} -v -run {}/{}",
 649                    GO_PACKAGE_TASK_VARIABLE.template_value(),
 650                    VariableName::Symbol.template_value(),
 651                    GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
 652                ),
 653                command: "go".into(),
 654                args: vec![
 655                    "test".into(),
 656                    "-v".into(),
 657                    "-run".into(),
 658                    format!(
 659                        "'^{}$/^{}$'",
 660                        VariableName::Symbol.template_value(),
 661                        GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
 662                    ),
 663                ],
 664                cwd: package_cwd.clone(),
 665                tags: vec!["go-subtest".to_owned()],
 666                ..TaskTemplate::default()
 667            },
 668            TaskTemplate {
 669                label: format!(
 670                    "go test {} -bench {}",
 671                    GO_PACKAGE_TASK_VARIABLE.template_value(),
 672                    VariableName::Symbol.template_value()
 673                ),
 674                command: "go".into(),
 675                args: vec![
 676                    "test".into(),
 677                    "-benchmem".into(),
 678                    "-run='^$'".into(),
 679                    "-bench".into(),
 680                    format!("\\^{}\\$", VariableName::Symbol.template_value()),
 681                ],
 682                cwd: package_cwd.clone(),
 683                tags: vec!["go-benchmark".to_owned()],
 684                ..TaskTemplate::default()
 685            },
 686            TaskTemplate {
 687                label: format!(
 688                    "go test {} -fuzz=Fuzz -run {}",
 689                    GO_PACKAGE_TASK_VARIABLE.template_value(),
 690                    VariableName::Symbol.template_value(),
 691                ),
 692                command: "go".into(),
 693                args: vec![
 694                    "test".into(),
 695                    "-fuzz=Fuzz".into(),
 696                    "-run".into(),
 697                    format!("\\^{}\\$", VariableName::Symbol.template_value(),),
 698                ],
 699                tags: vec!["go-fuzz".to_owned()],
 700                cwd: package_cwd.clone(),
 701                ..TaskTemplate::default()
 702            },
 703            TaskTemplate {
 704                label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
 705                command: "go".into(),
 706                args: vec!["run".into(), ".".into()],
 707                cwd: package_cwd.clone(),
 708                tags: vec!["go-main".to_owned()],
 709                ..TaskTemplate::default()
 710            },
 711            TaskTemplate {
 712                label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
 713                command: "go".into(),
 714                args: vec!["generate".into()],
 715                cwd: package_cwd,
 716                tags: vec!["go-generate".to_owned()],
 717                ..TaskTemplate::default()
 718            },
 719            TaskTemplate {
 720                label: "go generate ./...".into(),
 721                command: "go".into(),
 722                args: vec!["generate".into(), "./...".into()],
 723                cwd: module_cwd,
 724                ..TaskTemplate::default()
 725            },
 726        ])))
 727    }
 728}
 729
 730fn extract_subtest_name(input: &str) -> Option<String> {
 731    let content = if input.starts_with('`') && input.ends_with('`') {
 732        input.trim_matches('`')
 733    } else {
 734        input.trim_matches('"')
 735    };
 736
 737    let processed = content
 738        .chars()
 739        .map(|c| if c.is_whitespace() { '_' } else { c })
 740        .collect::<String>();
 741
 742    Some(
 743        GO_ESCAPE_SUBTEST_NAME_REGEX
 744            .replace_all(&processed, |caps: &regex::Captures| {
 745                format!("\\{}", &caps[0])
 746            })
 747            .to_string(),
 748    )
 749}
 750
 751#[cfg(test)]
 752mod tests {
 753    use super::*;
 754    use crate::language;
 755    use gpui::{AppContext, Hsla, TestAppContext};
 756    use theme::SyntaxTheme;
 757
 758    #[gpui::test]
 759    async fn test_go_label_for_completion() {
 760        let adapter = Arc::new(GoLspAdapter);
 761        let language = language("go", tree_sitter_go::LANGUAGE.into());
 762
 763        let theme = SyntaxTheme::new_test([
 764            ("type", Hsla::default()),
 765            ("keyword", Hsla::default()),
 766            ("function", Hsla::default()),
 767            ("number", Hsla::default()),
 768            ("property", Hsla::default()),
 769        ]);
 770        language.set_theme(&theme);
 771
 772        let grammar = language.grammar().unwrap();
 773        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
 774        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
 775        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
 776        let highlight_number = grammar.highlight_id_for_name("number").unwrap();
 777        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
 778
 779        assert_eq!(
 780            adapter
 781                .label_for_completion(
 782                    &lsp::CompletionItem {
 783                        kind: Some(lsp::CompletionItemKind::FUNCTION),
 784                        label: "Hello".to_string(),
 785                        detail: Some("func(a B) c.D".to_string()),
 786                        ..Default::default()
 787                    },
 788                    &language
 789                )
 790                .await,
 791            Some(CodeLabel::new(
 792                "Hello(a B) c.D".to_string(),
 793                0..5,
 794                vec![
 795                    (0..5, highlight_function),
 796                    (8..9, highlight_type),
 797                    (13..14, highlight_type),
 798                ]
 799            ))
 800        );
 801
 802        // Nested methods
 803        assert_eq!(
 804            adapter
 805                .label_for_completion(
 806                    &lsp::CompletionItem {
 807                        kind: Some(lsp::CompletionItemKind::METHOD),
 808                        label: "one.two.Three".to_string(),
 809                        detail: Some("func() [3]interface{}".to_string()),
 810                        ..Default::default()
 811                    },
 812                    &language
 813                )
 814                .await,
 815            Some(CodeLabel::new(
 816                "one.two.Three() [3]interface{}".to_string(),
 817                0..13,
 818                vec![
 819                    (8..13, highlight_function),
 820                    (17..18, highlight_number),
 821                    (19..28, highlight_keyword),
 822                ],
 823            ))
 824        );
 825
 826        // Nested fields
 827        assert_eq!(
 828            adapter
 829                .label_for_completion(
 830                    &lsp::CompletionItem {
 831                        kind: Some(lsp::CompletionItemKind::FIELD),
 832                        label: "two.Three".to_string(),
 833                        detail: Some("a.Bcd".to_string()),
 834                        ..Default::default()
 835                    },
 836                    &language
 837                )
 838                .await,
 839            Some(CodeLabel::new(
 840                "two.Three a.Bcd".to_string(),
 841                0..9,
 842                vec![(4..9, highlight_field), (12..15, highlight_type)],
 843            ))
 844        );
 845    }
 846
 847    #[gpui::test]
 848    fn test_testify_suite_detection(cx: &mut TestAppContext) {
 849        let language = language("go", tree_sitter_go::LANGUAGE.into());
 850
 851        let testify_suite = r#"
 852        package main
 853
 854        import (
 855            "testing"
 856
 857            "github.com/stretchr/testify/suite"
 858        )
 859
 860        type ExampleSuite struct {
 861            suite.Suite
 862        }
 863
 864        func TestExampleSuite(t *testing.T) {
 865            suite.Run(t, new(ExampleSuite))
 866        }
 867
 868        func (s *ExampleSuite) TestSomething_Success() {
 869            // test code
 870        }
 871        "#;
 872
 873        let buffer = cx
 874            .new(|cx| crate::Buffer::local(testify_suite, cx).with_language(language.clone(), cx));
 875        cx.executor().run_until_parked();
 876
 877        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
 878            let snapshot = buffer.snapshot();
 879            snapshot.runnable_ranges(0..testify_suite.len()).collect()
 880        });
 881
 882        let tag_strings: Vec<String> = runnables
 883            .iter()
 884            .flat_map(|r| &r.runnable.tags)
 885            .map(|tag| tag.0.to_string())
 886            .collect();
 887
 888        assert!(
 889            tag_strings.contains(&"go-test".to_string()),
 890            "Should find go-test tag, found: {:?}",
 891            tag_strings
 892        );
 893        assert!(
 894            tag_strings.contains(&"go-testify-suite".to_string()),
 895            "Should find go-testify-suite tag, found: {:?}",
 896            tag_strings
 897        );
 898    }
 899
 900    #[gpui::test]
 901    fn test_go_runnable_detection(cx: &mut TestAppContext) {
 902        let language = language("go", tree_sitter_go::LANGUAGE.into());
 903
 904        let interpreted_string_subtest = r#"
 905        package main
 906
 907        import "testing"
 908
 909        func TestExample(t *testing.T) {
 910            t.Run("subtest with double quotes", func(t *testing.T) {
 911                // test code
 912            })
 913        }
 914        "#;
 915
 916        let raw_string_subtest = r#"
 917        package main
 918
 919        import "testing"
 920
 921        func TestExample(t *testing.T) {
 922            t.Run(`subtest with
 923            multiline
 924            backticks`, func(t *testing.T) {
 925                // test code
 926            })
 927        }
 928        "#;
 929
 930        let buffer = cx.new(|cx| {
 931            crate::Buffer::local(interpreted_string_subtest, cx).with_language(language.clone(), cx)
 932        });
 933        cx.executor().run_until_parked();
 934
 935        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
 936            let snapshot = buffer.snapshot();
 937            snapshot
 938                .runnable_ranges(0..interpreted_string_subtest.len())
 939                .collect()
 940        });
 941
 942        let tag_strings: Vec<String> = runnables
 943            .iter()
 944            .flat_map(|r| &r.runnable.tags)
 945            .map(|tag| tag.0.to_string())
 946            .collect();
 947
 948        assert!(
 949            tag_strings.contains(&"go-test".to_string()),
 950            "Should find go-test tag, found: {:?}",
 951            tag_strings
 952        );
 953        assert!(
 954            tag_strings.contains(&"go-subtest".to_string()),
 955            "Should find go-subtest tag, found: {:?}",
 956            tag_strings
 957        );
 958
 959        let buffer = cx.new(|cx| {
 960            crate::Buffer::local(raw_string_subtest, cx).with_language(language.clone(), cx)
 961        });
 962        cx.executor().run_until_parked();
 963
 964        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
 965            let snapshot = buffer.snapshot();
 966            snapshot
 967                .runnable_ranges(0..raw_string_subtest.len())
 968                .collect()
 969        });
 970
 971        let tag_strings: Vec<String> = runnables
 972            .iter()
 973            .flat_map(|r| &r.runnable.tags)
 974            .map(|tag| tag.0.to_string())
 975            .collect();
 976
 977        assert!(
 978            tag_strings.contains(&"go-test".to_string()),
 979            "Should find go-test tag, found: {:?}",
 980            tag_strings
 981        );
 982        assert!(
 983            tag_strings.contains(&"go-subtest".to_string()),
 984            "Should find go-subtest tag, found: {:?}",
 985            tag_strings
 986        );
 987    }
 988
 989    #[gpui::test]
 990    fn test_go_example_test_detection(cx: &mut TestAppContext) {
 991        let language = language("go", tree_sitter_go::LANGUAGE.into());
 992
 993        let example_test = r#"
 994        package main
 995
 996        import "fmt"
 997
 998        func Example() {
 999            fmt.Println("Hello, world!")
1000            // Output: Hello, world!
1001        }
1002        "#;
1003
1004        let buffer =
1005            cx.new(|cx| crate::Buffer::local(example_test, cx).with_language(language.clone(), cx));
1006        cx.executor().run_until_parked();
1007
1008        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1009            let snapshot = buffer.snapshot();
1010            snapshot.runnable_ranges(0..example_test.len()).collect()
1011        });
1012
1013        let tag_strings: Vec<String> = runnables
1014            .iter()
1015            .flat_map(|r| &r.runnable.tags)
1016            .map(|tag| tag.0.to_string())
1017            .collect();
1018
1019        assert!(
1020            tag_strings.contains(&"go-example".to_string()),
1021            "Should find go-example tag, found: {:?}",
1022            tag_strings
1023        );
1024    }
1025
1026    #[gpui::test]
1027    fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
1028        let language = language("go", tree_sitter_go::LANGUAGE.into());
1029
1030        let table_test = r#"
1031        package main
1032
1033        import "testing"
1034
1035        func TestExample(t *testing.T) {
1036            _ = "some random string"
1037
1038            testCases := []struct{
1039                name string
1040                anotherStr string
1041            }{
1042                {
1043                    name: "test case 1",
1044                    anotherStr: "foo",
1045                },
1046                {
1047                    name: "test case 2",
1048                    anotherStr: "bar",
1049                },
1050                {
1051                    name: "test case 3",
1052                    anotherStr: "baz",
1053                },
1054            }
1055
1056            notATableTest := []struct{
1057                name string
1058            }{
1059                {
1060                    name: "some string",
1061                },
1062                {
1063                    name: "some other string",
1064                },
1065            }
1066
1067            for _, tc := range testCases {
1068                t.Run(tc.name, func(t *testing.T) {
1069                    // test code here
1070                })
1071            }
1072        }
1073        "#;
1074
1075        let buffer =
1076            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1077        cx.executor().run_until_parked();
1078
1079        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1080            let snapshot = buffer.snapshot();
1081            snapshot.runnable_ranges(0..table_test.len()).collect()
1082        });
1083
1084        let tag_strings: Vec<String> = runnables
1085            .iter()
1086            .flat_map(|r| &r.runnable.tags)
1087            .map(|tag| tag.0.to_string())
1088            .collect();
1089
1090        assert!(
1091            tag_strings.contains(&"go-test".to_string()),
1092            "Should find go-test tag, found: {:?}",
1093            tag_strings
1094        );
1095        assert!(
1096            tag_strings.contains(&"go-table-test-case".to_string()),
1097            "Should find go-table-test-case tag, found: {:?}",
1098            tag_strings
1099        );
1100
1101        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1102        // This is currently broken; see #39148
1103        // let go_table_test_count = tag_strings
1104        //     .iter()
1105        //     .filter(|&tag| tag == "go-table-test-case")
1106        //     .count();
1107
1108        assert!(
1109            go_test_count == 1,
1110            "Should find exactly 1 go-test, found: {}",
1111            go_test_count
1112        );
1113        // assert!(
1114        //     go_table_test_count == 3,
1115        //     "Should find exactly 3 go-table-test-case, found: {}",
1116        //     go_table_test_count
1117        // );
1118    }
1119
1120    #[gpui::test]
1121    fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
1122        let language = language("go", tree_sitter_go::LANGUAGE.into());
1123
1124        let table_test = r#"
1125        package main
1126
1127        func Example() {
1128            _ = "some random string"
1129
1130            notATableTest := []struct{
1131                name string
1132            }{
1133                {
1134                    name: "some string",
1135                },
1136                {
1137                    name: "some other string",
1138                },
1139            }
1140        }
1141        "#;
1142
1143        let buffer =
1144            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1145        cx.executor().run_until_parked();
1146
1147        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1148            let snapshot = buffer.snapshot();
1149            snapshot.runnable_ranges(0..table_test.len()).collect()
1150        });
1151
1152        let tag_strings: Vec<String> = runnables
1153            .iter()
1154            .flat_map(|r| &r.runnable.tags)
1155            .map(|tag| tag.0.to_string())
1156            .collect();
1157
1158        assert!(
1159            !tag_strings.contains(&"go-test".to_string()),
1160            "Should find go-test tag, found: {:?}",
1161            tag_strings
1162        );
1163        assert!(
1164            !tag_strings.contains(&"go-table-test-case".to_string()),
1165            "Should find go-table-test-case tag, found: {:?}",
1166            tag_strings
1167        );
1168    }
1169
1170    #[gpui::test]
1171    fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
1172        let language = language("go", tree_sitter_go::LANGUAGE.into());
1173
1174        let table_test = r#"
1175        package main
1176
1177        import "testing"
1178
1179        func TestExample(t *testing.T) {
1180            _ = "some random string"
1181
1182           	testCases := map[string]struct {
1183          		someStr string
1184          		fail    bool
1185           	}{
1186          		"test failure": {
1187         			someStr: "foo",
1188         			fail:    true,
1189          		},
1190          		"test success": {
1191         			someStr: "bar",
1192         			fail:    false,
1193          		},
1194           	}
1195
1196           	notATableTest := map[string]struct {
1197          		someStr string
1198           	}{
1199          		"some string": {
1200         			someStr: "foo",
1201          		},
1202          		"some other string": {
1203         			someStr: "bar",
1204          		},
1205           	}
1206
1207            for name, tc := range testCases {
1208                t.Run(name, func(t *testing.T) {
1209                    // test code here
1210                })
1211            }
1212        }
1213        "#;
1214
1215        let buffer =
1216            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1217        cx.executor().run_until_parked();
1218
1219        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1220            let snapshot = buffer.snapshot();
1221            snapshot.runnable_ranges(0..table_test.len()).collect()
1222        });
1223
1224        let tag_strings: Vec<String> = runnables
1225            .iter()
1226            .flat_map(|r| &r.runnable.tags)
1227            .map(|tag| tag.0.to_string())
1228            .collect();
1229
1230        assert!(
1231            tag_strings.contains(&"go-test".to_string()),
1232            "Should find go-test tag, found: {:?}",
1233            tag_strings
1234        );
1235        assert!(
1236            tag_strings.contains(&"go-table-test-case".to_string()),
1237            "Should find go-table-test-case tag, found: {:?}",
1238            tag_strings
1239        );
1240
1241        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1242        let go_table_test_count = tag_strings
1243            .iter()
1244            .filter(|&tag| tag == "go-table-test-case")
1245            .count();
1246
1247        assert!(
1248            go_test_count == 1,
1249            "Should find exactly 1 go-test, found: {}",
1250            go_test_count
1251        );
1252        assert!(
1253            go_table_test_count == 2,
1254            "Should find exactly 2 go-table-test-case, found: {}",
1255            go_table_test_count
1256        );
1257    }
1258
1259    #[gpui::test]
1260    fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
1261        let language = language("go", tree_sitter_go::LANGUAGE.into());
1262
1263        let table_test = r#"
1264        package main
1265
1266        func Example() {
1267            _ = "some random string"
1268
1269           	notATableTest := map[string]struct {
1270          		someStr string
1271           	}{
1272          		"some string": {
1273         			someStr: "foo",
1274          		},
1275          		"some other string": {
1276         			someStr: "bar",
1277          		},
1278           	}
1279        }
1280        "#;
1281
1282        let buffer =
1283            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1284        cx.executor().run_until_parked();
1285
1286        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1287            let snapshot = buffer.snapshot();
1288            snapshot.runnable_ranges(0..table_test.len()).collect()
1289        });
1290
1291        let tag_strings: Vec<String> = runnables
1292            .iter()
1293            .flat_map(|r| &r.runnable.tags)
1294            .map(|tag| tag.0.to_string())
1295            .collect();
1296
1297        assert!(
1298            !tag_strings.contains(&"go-test".to_string()),
1299            "Should find go-test tag, found: {:?}",
1300            tag_strings
1301        );
1302        assert!(
1303            !tag_strings.contains(&"go-table-test-case".to_string()),
1304            "Should find go-table-test-case tag, found: {:?}",
1305            tag_strings
1306        );
1307    }
1308
1309    #[test]
1310    fn test_extract_subtest_name() {
1311        // Interpreted string literal
1312        let input_double_quoted = r#""subtest with double quotes""#;
1313        let result = extract_subtest_name(input_double_quoted);
1314        assert_eq!(result, Some(r#"subtest_with_double_quotes"#.to_string()));
1315
1316        let input_double_quoted_with_backticks = r#""test with `backticks` inside""#;
1317        let result = extract_subtest_name(input_double_quoted_with_backticks);
1318        assert_eq!(result, Some(r#"test_with_`backticks`_inside"#.to_string()));
1319
1320        // Raw string literal
1321        let input_with_backticks = r#"`subtest with backticks`"#;
1322        let result = extract_subtest_name(input_with_backticks);
1323        assert_eq!(result, Some(r#"subtest_with_backticks"#.to_string()));
1324
1325        let input_raw_with_quotes = r#"`test with "quotes" and other chars`"#;
1326        let result = extract_subtest_name(input_raw_with_quotes);
1327        assert_eq!(
1328            result,
1329            Some(r#"test_with_\"quotes\"_and_other_chars"#.to_string())
1330        );
1331
1332        let input_multiline = r#"`subtest with
1333        multiline
1334        backticks`"#;
1335        let result = extract_subtest_name(input_multiline);
1336        assert_eq!(
1337            result,
1338            Some(r#"subtest_with_________multiline_________backticks"#.to_string())
1339        );
1340
1341        let input_with_double_quotes = r#"`test with "double quotes"`"#;
1342        let result = extract_subtest_name(input_with_double_quotes);
1343        assert_eq!(result, Some(r#"test_with_\"double_quotes\""#.to_string()));
1344    }
1345}