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