go.rs

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