go.rs

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