go.rs

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