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