Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:33:28.862Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

go.rs

2141 lines · 72.9 KB · rust
1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::{App, AsyncApp, Entity, Task};
6use http_client::github::latest_github_release;
7pub use language::*;
8use language::{
9    LanguageName, LanguageToolchainStore, LspAdapterDelegate, LspInstaller,
10    language_settings::LanguageSettings,
11};
12use lsp::{LanguageServerBinary, LanguageServerName};
13
14use project::lsp_store::language_server_settings;
15use regex::Regex;
16use serde_json::{Value, json};
17use settings::SemanticTokenRules;
18use smol::fs;
19use std::{
20    borrow::Cow,
21    ffi::{OsStr, OsString},
22    future::Future,
23    ops::Range,
24    path::{Path, PathBuf},
25    process::Output,
26    str,
27    sync::{
28        Arc, LazyLock,
29        atomic::{AtomicBool, Ordering::SeqCst},
30    },
31};
32use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
33use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
34
35pub(crate) fn semantic_token_rules() -> SemanticTokenRules {
36    let content = grammars::get_file("go/semantic_token_rules.json")
37        .expect("missing go/semantic_token_rules.json");
38    let json = std::str::from_utf8(&content.data).expect("invalid utf-8 in semantic_token_rules");
39    settings::parse_json_with_comments::<SemanticTokenRules>(json)
40        .expect("failed to parse go semantic_token_rules.json")
41}
42
43fn server_binary_arguments() -> Vec<OsString> {
44    vec!["-mode=stdio".into()]
45}
46
47#[derive(Copy, Clone)]
48pub struct GoLspAdapter;
49
50impl GoLspAdapter {
51    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls");
52}
53
54static VERSION_REGEX: LazyLock<Regex> =
55    LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create VERSION_REGEX"));
56
57static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
58    Regex::new(r#"[.*+?^${}()|\[\]\\"']"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")
59});
60
61const BINARY: &str = if cfg!(target_os = "windows") {
62    "gopls.exe"
63} else {
64    "gopls"
65};
66
67impl LspInstaller for GoLspAdapter {
68    type BinaryVersion = Option<String>;
69
70    async fn fetch_latest_server_version(
71        &self,
72        delegate: &Arc<dyn LspAdapterDelegate>,
73        _: bool,
74        cx: &mut AsyncApp,
75    ) -> Result<Option<String>> {
76        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
77
78        const NOTIFICATION_MESSAGE: &str =
79            "Could not install the Go language server `gopls`, because `go` was not found.";
80
81        if delegate.which("go".as_ref()).await.is_none() {
82            if DID_SHOW_NOTIFICATION
83                .compare_exchange(false, true, SeqCst, SeqCst)
84                .is_ok()
85            {
86                cx.update(|cx| {
87                    delegate.show_notification(NOTIFICATION_MESSAGE, cx);
88                });
89            }
90            anyhow::bail!(
91                "Could not install the Go language server `gopls`, because `go` was not found."
92            );
93        }
94
95        let release =
96            latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
97        let version: Option<String> = release.tag_name.strip_prefix("gopls/v").map(str::to_string);
98        if version.is_none() {
99            log::warn!(
100                "couldn't infer gopls version from GitHub release tag name '{}'",
101                release.tag_name
102            );
103        }
104        Ok(version)
105    }
106
107    async fn check_if_user_installed(
108        &self,
109        delegate: &Arc<dyn LspAdapterDelegate>,
110        _: Option<Toolchain>,
111        _: &AsyncApp,
112    ) -> Option<LanguageServerBinary> {
113        let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
114        Some(LanguageServerBinary {
115            path,
116            arguments: server_binary_arguments(),
117            env: None,
118        })
119    }
120
121    fn fetch_server_binary(
122        &self,
123        version: Option<String>,
124        container_dir: PathBuf,
125        delegate: &Arc<dyn LspAdapterDelegate>,
126    ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
127        let delegate = delegate.clone();
128
129        async move {
130            let go = delegate.which("go".as_ref()).await.unwrap_or("go".into());
131            let go_version_output = util::command::new_command(&go)
132                .args(["version"])
133                .output()
134                .await
135                .context("failed to get go version via `go version` command`")?;
136            let go_version = parse_version_output(&go_version_output)?;
137
138            if let Some(version) = version {
139                let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));
140                if let Ok(metadata) = fs::metadata(&binary_path).await
141                    && metadata.is_file()
142                {
143                    remove_matching(&container_dir, |entry| {
144                        entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
145                    })
146                    .await;
147
148                    return Ok(LanguageServerBinary {
149                        path: binary_path.to_path_buf(),
150                        arguments: server_binary_arguments(),
151                        env: None,
152                    });
153                }
154            } else if let Some(path) = get_cached_server_binary(&container_dir).await {
155                return Ok(path);
156            }
157
158            let gobin_dir = container_dir.join("gobin");
159            fs::create_dir_all(&gobin_dir).await?;
160            let install_output = util::command::new_command(go)
161                .env("GO111MODULE", "on")
162                .env("GOBIN", &gobin_dir)
163                .args(["install", "golang.org/x/tools/gopls@latest"])
164                .output()
165                .await?;
166
167            if !install_output.status.success() {
168                log::error!(
169                    "failed to install gopls via `go install`. stdout: {:?}, stderr: {:?}",
170                    String::from_utf8_lossy(&install_output.stdout),
171                    String::from_utf8_lossy(&install_output.stderr)
172                );
173                anyhow::bail!(
174                    "failed to install gopls with `go install`. Is `go` installed and in the PATH? Check logs for more information."
175                );
176            }
177
178            let installed_binary_path = gobin_dir.join(BINARY);
179            let version_output = util::command::new_command(&installed_binary_path)
180                .arg("version")
181                .output()
182                .await
183                .context("failed to run installed gopls binary")?;
184            let gopls_version = parse_version_output(&version_output)?;
185            let binary_path = container_dir.join(format!("gopls_{gopls_version}_go_{go_version}"));
186            fs::rename(&installed_binary_path, &binary_path).await?;
187
188            Ok(LanguageServerBinary {
189                path: binary_path.to_path_buf(),
190                arguments: server_binary_arguments(),
191                env: None,
192            })
193        }
194    }
195
196    async fn cached_server_binary(
197        &self,
198        container_dir: PathBuf,
199        _: &dyn LspAdapterDelegate,
200    ) -> Option<LanguageServerBinary> {
201        get_cached_server_binary(&container_dir).await
202    }
203}
204
205#[async_trait(?Send)]
206impl LspAdapter for GoLspAdapter {
207    fn name(&self) -> LanguageServerName {
208        Self::SERVER_NAME
209    }
210
211    async fn initialization_options(
212        self: Arc<Self>,
213        delegate: &Arc<dyn LspAdapterDelegate>,
214        cx: &mut AsyncApp,
215    ) -> Result<Option<serde_json::Value>> {
216        let semantic_tokens_enabled = cx.update(|cx| {
217            LanguageSettings::resolve(None, Some(&LanguageName::new("Go")), cx)
218                .semantic_tokens
219                .enabled()
220        });
221
222        let mut default_config = json!({
223            "usePlaceholders": false,
224            "hints": {
225                "assignVariableTypes": true,
226                "compositeLiteralFields": true,
227                "compositeLiteralTypes": true,
228                "constantValues": true,
229                "functionTypeParameters": true,
230                "parameterNames": true,
231                "rangeVariableTypes": true
232            },
233            "codelenses": {
234                "test": true
235            },
236            "semanticTokens": semantic_tokens_enabled
237        });
238
239        let project_initialization_options = cx.update(|cx| {
240            language_server_settings(delegate.as_ref(), &self.name(), cx)
241                .and_then(|s| s.initialization_options.clone())
242        });
243
244        if let Some(override_options) = project_initialization_options {
245            merge_json_value_into(override_options, &mut default_config);
246        }
247
248        Ok(Some(default_config))
249    }
250
251    async fn workspace_configuration(
252        self: Arc<Self>,
253        delegate: &Arc<dyn LspAdapterDelegate>,
254        _: Option<Toolchain>,
255        _: Option<lsp::Uri>,
256        cx: &mut AsyncApp,
257    ) -> Result<Value> {
258        Ok(cx
259            .update(|cx| {
260                language_server_settings(delegate.as_ref(), &self.name(), cx)
261                    .and_then(|settings| settings.settings.clone())
262            })
263            .unwrap_or_default())
264    }
265
266    async fn label_for_completion(
267        &self,
268        completion: &lsp::CompletionItem,
269        language: &Arc<Language>,
270    ) -> Option<CodeLabel> {
271        let label = &completion.label;
272
273        // Gopls returns nested fields and methods as completions.
274        // To syntax highlight these, combine their final component
275        // with their detail.
276        let name_offset = label.rfind('.').unwrap_or(0);
277
278        match completion.kind.zip(completion.detail.as_ref()) {
279            Some((lsp::CompletionItemKind::MODULE, detail)) => {
280                let text = format!("{label} {detail}");
281                let source = Rope::from(format!("import {text}").as_str());
282                let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
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((
294                lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
295                detail,
296            )) => {
297                let text = format!("{label} {detail}");
298                let source =
299                    Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
300                let runs = adjust_runs(
301                    name_offset,
302                    language.highlight_text(&source, 4..4 + text[name_offset..].len()),
303                );
304                let filter_range = completion
305                    .filter_text
306                    .as_deref()
307                    .and_then(|filter_text| {
308                        text.find(filter_text)
309                            .map(|start| start..start + filter_text.len())
310                    })
311                    .unwrap_or(0..label.len());
312                return Some(CodeLabel::new(text, filter_range, runs));
313            }
314            Some((lsp::CompletionItemKind::STRUCT, _)) => {
315                let text = format!("{label} struct {{}}");
316                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
317                let runs = adjust_runs(
318                    name_offset,
319                    language.highlight_text(&source, 5..5 + text[name_offset..].len()),
320                );
321                let filter_range = completion
322                    .filter_text
323                    .as_deref()
324                    .and_then(|filter_text| {
325                        text.find(filter_text)
326                            .map(|start| start..start + filter_text.len())
327                    })
328                    .unwrap_or(0..label.len());
329                return Some(CodeLabel::new(text, filter_range, runs));
330            }
331            Some((lsp::CompletionItemKind::INTERFACE, _)) => {
332                let text = format!("{label} interface {{}}");
333                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
334                let runs = adjust_runs(
335                    name_offset,
336                    language.highlight_text(&source, 5..5 + text[name_offset..].len()),
337                );
338                let filter_range = completion
339                    .filter_text
340                    .as_deref()
341                    .and_then(|filter_text| {
342                        text.find(filter_text)
343                            .map(|start| start..start + filter_text.len())
344                    })
345                    .unwrap_or(0..label.len());
346                return Some(CodeLabel::new(text, filter_range, runs));
347            }
348            Some((lsp::CompletionItemKind::FIELD, detail)) => {
349                let text = format!("{label} {detail}");
350                let source =
351                    Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
352                let runs = adjust_runs(
353                    name_offset,
354                    language.highlight_text(&source, 16..16 + text[name_offset..].len()),
355                );
356                let filter_range = completion
357                    .filter_text
358                    .as_deref()
359                    .and_then(|filter_text| {
360                        text.find(filter_text)
361                            .map(|start| start..start + filter_text.len())
362                    })
363                    .unwrap_or(0..label.len());
364                return Some(CodeLabel::new(text, filter_range, runs));
365            }
366            Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
367                if let Some(signature) = detail.strip_prefix("func") {
368                    let text = format!("{label}{signature}");
369                    let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
370                    let runs = adjust_runs(
371                        name_offset,
372                        language.highlight_text(&source, 5..5 + text[name_offset..].len()),
373                    );
374                    let filter_range = completion
375                        .filter_text
376                        .as_deref()
377                        .and_then(|filter_text| {
378                            text.find(filter_text)
379                                .map(|start| start..start + filter_text.len())
380                        })
381                        .unwrap_or(0..label.len());
382                    return Some(CodeLabel::new(text, filter_range, runs));
383                }
384            }
385            _ => {}
386        }
387        None
388    }
389
390    async fn label_for_symbol(
391        &self,
392        symbol: &language::Symbol,
393        language: &Arc<Language>,
394    ) -> Option<CodeLabel> {
395        let name = &symbol.name;
396        let (text, filter_range, display_range) = match symbol.kind {
397            language::SymbolKind::Method | language::SymbolKind::Function => {
398                let text = format!("func {} () {{}}", name);
399                let filter_range = 5..5 + name.len();
400                let display_range = 0..filter_range.end;
401                (text, filter_range, display_range)
402            }
403            language::SymbolKind::Struct => {
404                let text = format!("type {} struct {{}}", name);
405                let filter_range = 5..5 + name.len();
406                let display_range = 0..text.len();
407                (text, filter_range, display_range)
408            }
409            language::SymbolKind::Interface => {
410                let text = format!("type {} interface {{}}", name);
411                let filter_range = 5..5 + name.len();
412                let display_range = 0..text.len();
413                (text, filter_range, display_range)
414            }
415            language::SymbolKind::Class => {
416                let text = format!("type {} T", name);
417                let filter_range = 5..5 + name.len();
418                let display_range = 0..filter_range.end;
419                (text, filter_range, display_range)
420            }
421            language::SymbolKind::Constant => {
422                let text = format!("const {} = nil", name);
423                let filter_range = 6..6 + name.len();
424                let display_range = 0..filter_range.end;
425                (text, filter_range, display_range)
426            }
427            language::SymbolKind::Variable => {
428                let text = format!("var {} = nil", name);
429                let filter_range = 4..4 + name.len();
430                let display_range = 0..filter_range.end;
431                (text, filter_range, display_range)
432            }
433            language::SymbolKind::Module => {
434                let text = format!("package {}", name);
435                let filter_range = 8..8 + name.len();
436                let display_range = 0..filter_range.end;
437                (text, filter_range, display_range)
438            }
439            _ => return None,
440        };
441
442        Some(CodeLabel::new(
443            text[display_range.clone()].to_string(),
444            filter_range,
445            language.highlight_text(&text.as_str().into(), display_range),
446        ))
447    }
448
449    fn client_command(
450        &self,
451        command_name: &str,
452        arguments: &[serde_json::Value],
453    ) -> Option<ClientCommand> {
454        if let "gopls.run_tests" = command_name {
455            let template = go_test_task_template(arguments.first()?)?;
456            Some(ClientCommand::ScheduleTask(template))
457        } else {
458            None
459        }
460    }
461
462    fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
463        static REGEX: LazyLock<Regex> =
464            LazyLock::new(|| Regex::new(r"(?m)\n\s*").expect("Failed to create REGEX"));
465        Some(REGEX.replace_all(message, "\n\n").to_string())
466    }
467}
468
469fn json_string_array(value: &serde_json::Value, key: &str) -> Vec<String> {
470    value
471        .get(key)
472        .and_then(|v| v.as_array())
473        .map(|arr| {
474            arr.iter()
475                .filter_map(|v| v.as_str().map(String::from))
476                .collect()
477        })
478        .unwrap_or_default()
479}
480
481fn go_test_task_template(arg: &serde_json::Value) -> Option<task::TaskTemplate> {
482    let tests = json_string_array(arg, "Tests");
483    let benchmarks = json_string_array(arg, "Benchmarks");
484    if tests.is_empty() && benchmarks.is_empty() {
485        return None;
486    }
487
488    let mut go_args = vec!["test".to_string(), "-test.fullpath=true".to_string()];
489
490    if tests.is_empty() {
491        go_args.push("-benchmem".to_string());
492        go_args.push("-run=^$".to_string());
493    } else {
494        go_args.push("-timeout".to_string());
495        go_args.push("30s".to_string());
496        go_args.push("-run".to_string());
497        if tests.len() == 1 {
498            go_args.push(format!("^{}$", tests[0]));
499        } else {
500            go_args.push(format!("^({})$", tests.join("|")));
501        }
502    }
503
504    if !benchmarks.is_empty() {
505        go_args.push("-bench".to_string());
506        if benchmarks.len() == 1 {
507            go_args.push(format!("^{}$", benchmarks[0]));
508        } else {
509            go_args.push(format!("^({})$", benchmarks.join("|")));
510        }
511    }
512
513    go_args.push(".".to_string());
514
515    let label = if !tests.is_empty() {
516        format!("go test {}", tests.join(", "))
517    } else {
518        format!("go bench {}", benchmarks.join(", "))
519    };
520
521    let cwd = arg
522        .get("URI")
523        .and_then(|v| v.as_str())
524        .and_then(|uri| uri.strip_prefix("file://"))
525        .and_then(|path| std::path::Path::new(path).parent())
526        .map(|p| p.to_string_lossy().into_owned());
527
528    Some(task::TaskTemplate {
529        label,
530        command: "go".to_string(),
531        args: go_args,
532        cwd,
533        ..task::TaskTemplate::default()
534    })
535}
536
537fn parse_version_output(output: &Output) -> Result<&str> {
538    let version_stdout =
539        str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?;
540
541    let version = VERSION_REGEX
542        .find(version_stdout)
543        .with_context(|| format!("failed to parse version output '{version_stdout}'"))?
544        .as_str();
545
546    Ok(version)
547}
548
549async fn get_cached_server_binary(container_dir: &Path) -> Option<LanguageServerBinary> {
550    maybe!(async {
551        let mut last_binary_path = None;
552        let mut entries = fs::read_dir(container_dir).await?;
553        while let Some(entry) = entries.next().await {
554            let entry = entry?;
555            if entry.file_type().await?.is_file()
556                && entry
557                    .file_name()
558                    .to_str()
559                    .is_some_and(|name| name.starts_with("gopls_"))
560            {
561                last_binary_path = Some(entry.path());
562            }
563        }
564
565        let path = last_binary_path.context("no cached binary")?;
566        anyhow::Ok(LanguageServerBinary {
567            path,
568            arguments: server_binary_arguments(),
569            env: None,
570        })
571    })
572    .await
573    .log_err()
574}
575
576fn adjust_runs(
577    delta: usize,
578    mut runs: Vec<(Range<usize>, HighlightId)>,
579) -> Vec<(Range<usize>, HighlightId)> {
580    for (range, _) in &mut runs {
581        range.start += delta;
582        range.end += delta;
583    }
584    runs
585}
586
587pub(crate) struct GoContextProvider;
588
589pub(crate) struct GoRunnableResolver;
590
591impl RunnableResolver for GoRunnableResolver {
592    fn resolve(
593        &self,
594        local_captures: &[RunnableMatchCapture],
595        shared_captures: &[RunnableMatchCapture],
596        buffer: &BufferSnapshot,
597    ) -> Option<ResolvedRunnable> {
598        const FIELD_CHECK: &str = "_field_check";
599        const FIELD_NAME: &str = "_field_name";
600        const TABLE_TEST_CASE_NAME: &str = "_table_test_case_name";
601
602        // A row may declare several string fields (e.g. `{ name: "x", label: "y" }`), so
603        // the query emits one `@_field_name` + `@run` pair per field, in source order.
604        //
605        // When the loop body calls `t.Run(tc.<field>, ...)`, `@_field_check` names that
606        // field; pick the pair whose `@_field_name` matches it. Without a `@_field_check`
607        // (e.g. map-keyed tables) the first pair wins.
608        let reference_text = shared_captures
609            .iter()
610            .find(|capture| capture.name() == Some(FIELD_CHECK))
611            .map(|capture| buffer.text_for_range(capture.range()).collect::<String>());
612        let pair_index = match &reference_text {
613            Some(reference) => local_captures
614                .iter()
615                .filter(|capture| capture.name() == Some(FIELD_NAME))
616                .position(|capture| buffer.text_for_range(capture.range()).equals_str(reference))?,
617            None => 0,
618        };
619
620        // `@run` and `@_table_test_case_name` tag the same string literal, so the chosen
621        // run's text is the case name.
622        let run_capture = local_captures
623            .iter()
624            .filter(|capture| capture.is_run())
625            .nth(pair_index)?;
626        Some(ResolvedRunnable {
627            run_range: run_capture.range(),
628            extra_captures: smallvec::smallvec![(
629                TABLE_TEST_CASE_NAME.to_string(),
630                buffer.text_for_range(run_capture.range()).collect(),
631            )],
632        })
633    }
634}
635
636const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
637const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
638    VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
639const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
640    VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
641const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName =
642    VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME"));
643const GO_SUITE_NAME_TASK_VARIABLE: VariableName =
644    VariableName::Custom(Cow::Borrowed("GO_SUITE_NAME"));
645
646impl ContextProvider for GoContextProvider {
647    fn build_context(
648        &self,
649        variables: &TaskVariables,
650        location: ContextLocation<'_>,
651        _: Option<HashMap<String, String>>,
652        _: Arc<dyn LanguageToolchainStore>,
653        cx: &mut gpui::App,
654    ) -> Task<Result<TaskVariables>> {
655        let local_abs_path = location
656            .file_location
657            .buffer
658            .read(cx)
659            .file()
660            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
661
662        let go_package_variable = local_abs_path
663            .as_deref()
664            .and_then(|local_abs_path| local_abs_path.parent())
665            .map(|buffer_dir| {
666                // Prefer the relative form `./my-nested-package/is-here` over
667                // absolute path, because it's more readable in the modal, but
668                // the absolute path also works.
669                let package_name = variables
670                    .get(&VariableName::WorktreeRoot)
671                    .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())
672                    .map(|relative_pkg_dir| {
673                        if relative_pkg_dir.as_os_str().is_empty() {
674                            ".".into()
675                        } else {
676                            format!("./{}", relative_pkg_dir.to_string_lossy())
677                        }
678                    })
679                    .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
680
681                (GO_PACKAGE_TASK_VARIABLE.clone(), package_name)
682            });
683
684        let go_module_root_variable = local_abs_path
685            .as_deref()
686            .and_then(|local_abs_path| local_abs_path.parent())
687            .map(|buffer_dir| {
688                // Walk dirtree up until getting the first go.mod file
689                let module_dir = buffer_dir
690                    .ancestors()
691                    .find(|dir| dir.join("go.mod").is_file())
692                    .map(|dir| dir.to_string_lossy().into_owned())
693                    .unwrap_or_else(|| ".".to_string());
694
695                (GO_MODULE_ROOT_TASK_VARIABLE.clone(), module_dir)
696            });
697
698        let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
699
700        let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
701            .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
702
703        let _table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed(
704            "_table_test_case_name",
705        )));
706
707        let go_table_test_case_variable = _table_test_case_name
708            .and_then(extract_subtest_name)
709            .map(|case_name| (GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.clone(), case_name));
710
711        let _suite_name = variables.get(&VariableName::Custom(Cow::Borrowed("_suite_name")));
712
713        let go_suite_variable = _suite_name
714            .and_then(extract_subtest_name)
715            .map(|suite_name| (GO_SUITE_NAME_TASK_VARIABLE.clone(), suite_name));
716
717        Task::ready(Ok(TaskVariables::from_iter(
718            [
719                go_package_variable,
720                go_subtest_variable,
721                go_table_test_case_variable,
722                go_suite_variable,
723                go_module_root_variable,
724            ]
725            .into_iter()
726            .flatten(),
727        )))
728    }
729
730    fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
731        let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
732            None
733        } else {
734            Some("$ZED_DIRNAME".to_string())
735        };
736        let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
737
738        Task::ready(Some(TaskTemplates(vec![
739            TaskTemplate {
740                label: format!(
741                    "go test {} -v -run Test{}/{}",
742                    GO_PACKAGE_TASK_VARIABLE.template_value(),
743                    GO_SUITE_NAME_TASK_VARIABLE.template_value(),
744                    VariableName::Symbol.template_value(),
745                ),
746                command: "go".into(),
747                args: vec![
748                    "test".into(),
749                    "-v".into(),
750                    "-run".into(),
751                    format!(
752                        "\\^Test{}\\$/\\^{}\\$",
753                        GO_SUITE_NAME_TASK_VARIABLE.template_value(),
754                        VariableName::Symbol.template_value(),
755                    ),
756                ],
757                cwd: package_cwd.clone(),
758                tags: vec!["go-testify-suite".to_owned()],
759                ..TaskTemplate::default()
760            },
761            TaskTemplate {
762                label: format!(
763                    "go test {} -v -run {}/{}",
764                    GO_PACKAGE_TASK_VARIABLE.template_value(),
765                    VariableName::Symbol.template_value(),
766                    GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
767                ),
768                command: "go".into(),
769                args: vec![
770                    "test".into(),
771                    "-v".into(),
772                    "-run".into(),
773                    format!(
774                        "\\^{}\\$/\\^{}\\$",
775                        VariableName::Symbol.template_value(),
776                        GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
777                    ),
778                ],
779                cwd: package_cwd.clone(),
780                tags: vec![
781                    "go-table-test-case".to_owned(),
782                    "go-table-test-case-without-explicit-variable".to_owned(),
783                ],
784                ..TaskTemplate::default()
785            },
786            TaskTemplate {
787                label: format!(
788                    "go test {} -run {}",
789                    GO_PACKAGE_TASK_VARIABLE.template_value(),
790                    VariableName::Symbol.template_value(),
791                ),
792                command: "go".into(),
793                args: vec![
794                    "test".into(),
795                    "-run".into(),
796                    format!("\\^{}\\$", VariableName::Symbol.template_value(),),
797                ],
798                tags: vec!["go-test".to_owned()],
799                cwd: package_cwd.clone(),
800                ..TaskTemplate::default()
801            },
802            TaskTemplate {
803                label: format!(
804                    "go test {} -run {}",
805                    GO_PACKAGE_TASK_VARIABLE.template_value(),
806                    VariableName::Symbol.template_value(),
807                ),
808                command: "go".into(),
809                args: vec![
810                    "test".into(),
811                    "-run".into(),
812                    format!("\\^{}\\$", VariableName::Symbol.template_value(),),
813                ],
814                tags: vec!["go-example".to_owned()],
815                cwd: package_cwd.clone(),
816                ..TaskTemplate::default()
817            },
818            TaskTemplate {
819                label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
820                command: "go".into(),
821                args: vec!["test".into()],
822                cwd: package_cwd.clone(),
823                ..TaskTemplate::default()
824            },
825            TaskTemplate {
826                label: "go test ./...".into(),
827                command: "go".into(),
828                args: vec!["test".into(), "./...".into()],
829                cwd: module_cwd.clone(),
830                ..TaskTemplate::default()
831            },
832            TaskTemplate {
833                label: format!(
834                    "go test {} -v -run {}/{}",
835                    GO_PACKAGE_TASK_VARIABLE.template_value(),
836                    VariableName::Symbol.template_value(),
837                    GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
838                ),
839                command: "go".into(),
840                args: vec![
841                    "test".into(),
842                    "-v".into(),
843                    "-run".into(),
844                    format!(
845                        "\\^{}\\$/\\^{}\\$",
846                        VariableName::Symbol.template_value(),
847                        GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
848                    ),
849                ],
850                cwd: package_cwd.clone(),
851                tags: vec!["go-subtest".to_owned()],
852                ..TaskTemplate::default()
853            },
854            TaskTemplate {
855                label: format!(
856                    "go test {} -bench {}",
857                    GO_PACKAGE_TASK_VARIABLE.template_value(),
858                    VariableName::Symbol.template_value()
859                ),
860                command: "go".into(),
861                args: vec![
862                    "test".into(),
863                    "-benchmem".into(),
864                    "-run='^$'".into(),
865                    "-bench".into(),
866                    format!("\\^{}\\$", VariableName::Symbol.template_value()),
867                ],
868                cwd: package_cwd.clone(),
869                tags: vec!["go-benchmark".to_owned()],
870                ..TaskTemplate::default()
871            },
872            TaskTemplate {
873                label: format!(
874                    "go test {} -fuzz=Fuzz -run {}",
875                    GO_PACKAGE_TASK_VARIABLE.template_value(),
876                    VariableName::Symbol.template_value(),
877                ),
878                command: "go".into(),
879                args: vec![
880                    "test".into(),
881                    "-fuzz=Fuzz".into(),
882                    "-run".into(),
883                    format!("\\^{}\\$", VariableName::Symbol.template_value(),),
884                ],
885                tags: vec!["go-fuzz".to_owned()],
886                cwd: package_cwd.clone(),
887                ..TaskTemplate::default()
888            },
889            TaskTemplate {
890                label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
891                command: "go".into(),
892                args: vec!["run".into(), ".".into()],
893                cwd: package_cwd.clone(),
894                tags: vec!["go-main".to_owned()],
895                ..TaskTemplate::default()
896            },
897            TaskTemplate {
898                label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
899                command: "go".into(),
900                args: vec!["generate".into()],
901                cwd: package_cwd,
902                tags: vec!["go-generate".to_owned()],
903                ..TaskTemplate::default()
904            },
905            TaskTemplate {
906                label: "go generate ./...".into(),
907                command: "go".into(),
908                args: vec!["generate".into(), "./...".into()],
909                cwd: module_cwd,
910                ..TaskTemplate::default()
911            },
912        ])))
913    }
914
915    fn runnable_resolver(&self) -> Option<Arc<dyn RunnableResolver>> {
916        Some(Arc::new(GoRunnableResolver))
917    }
918}
919
920fn extract_subtest_name(input: &str) -> Option<String> {
921    let content = if input.starts_with('`') && input.ends_with('`') {
922        input.trim_matches('`')
923    } else {
924        input.trim_matches('"')
925    };
926
927    let processed = content
928        .chars()
929        .map(|c| if c.is_whitespace() { '_' } else { c })
930        .collect::<String>();
931
932    Some(
933        GO_ESCAPE_SUBTEST_NAME_REGEX
934            .replace_all(&processed, |caps: &regex::Captures| {
935                format!("\\{}", &caps[0])
936            })
937            .to_string(),
938    )
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944    use crate::language;
945    use gpui::{AppContext, Hsla, TestAppContext};
946    use task::TaskContext;
947    use theme::SyntaxTheme;
948    use unindent::Unindent as _;
949
950    fn go_language() -> Arc<Language> {
951        let language = language("go", tree_sitter_go::LANGUAGE.into());
952        Arc::new(
953            Arc::try_unwrap(language)
954                .unwrap()
955                .with_context_provider(Some(Arc::new(GoContextProvider))),
956        )
957    }
958
959    #[gpui::test]
960    async fn test_go_label_for_completion() {
961        let adapter = Arc::new(GoLspAdapter);
962        let language = go_language();
963
964        let theme = SyntaxTheme::new_test([
965            ("type", Hsla::default()),
966            ("keyword", Hsla::default()),
967            ("function", Hsla::default()),
968            ("number", Hsla::default()),
969            ("property", Hsla::default()),
970        ]);
971        language.set_theme(&theme);
972
973        let grammar = language.grammar().unwrap();
974        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
975        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
976        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
977        let highlight_number = grammar.highlight_id_for_name("number").unwrap();
978        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
979
980        assert_eq!(
981            adapter
982                .label_for_completion(
983                    &lsp::CompletionItem {
984                        kind: Some(lsp::CompletionItemKind::FUNCTION),
985                        label: "Hello".to_string(),
986                        detail: Some("func(a B) c.D".to_string()),
987                        ..Default::default()
988                    },
989                    &language
990                )
991                .await,
992            Some(CodeLabel::new(
993                "Hello(a B) c.D".to_string(),
994                0..5,
995                vec![
996                    (0..5, highlight_function),
997                    (8..9, highlight_type),
998                    (13..14, highlight_type),
999                ]
1000            ))
1001        );
1002
1003        // Nested methods
1004        assert_eq!(
1005            adapter
1006                .label_for_completion(
1007                    &lsp::CompletionItem {
1008                        kind: Some(lsp::CompletionItemKind::METHOD),
1009                        label: "one.two.Three".to_string(),
1010                        detail: Some("func() [3]interface{}".to_string()),
1011                        ..Default::default()
1012                    },
1013                    &language
1014                )
1015                .await,
1016            Some(CodeLabel::new(
1017                "one.two.Three() [3]interface{}".to_string(),
1018                0..13,
1019                vec![
1020                    (8..13, highlight_function),
1021                    (17..18, highlight_number),
1022                    (19..28, highlight_keyword),
1023                ],
1024            ))
1025        );
1026
1027        // Nested fields
1028        assert_eq!(
1029            adapter
1030                .label_for_completion(
1031                    &lsp::CompletionItem {
1032                        kind: Some(lsp::CompletionItemKind::FIELD),
1033                        label: "two.Three".to_string(),
1034                        detail: Some("a.Bcd".to_string()),
1035                        ..Default::default()
1036                    },
1037                    &language
1038                )
1039                .await,
1040            Some(CodeLabel::new(
1041                "two.Three a.Bcd".to_string(),
1042                0..9,
1043                vec![(4..9, highlight_field), (12..15, highlight_type)],
1044            ))
1045        );
1046    }
1047
1048    #[gpui::test]
1049    fn test_go_test_main_ignored(cx: &mut TestAppContext) {
1050        let language = go_language();
1051
1052        let example_test = r#"
1053        package main
1054
1055        func TestMain(m *testing.M) {
1056            os.Exit(m.Run())
1057        }
1058        "#;
1059
1060        let buffer =
1061            cx.new(|cx| crate::Buffer::local(example_test, cx).with_language(language.clone(), cx));
1062        cx.executor().run_until_parked();
1063
1064        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1065            let snapshot = buffer.snapshot();
1066            snapshot.runnable_ranges(0..example_test.len()).collect()
1067        });
1068
1069        let tag_strings: Vec<String> = runnables
1070            .iter()
1071            .flat_map(|r| &r.runnable.tags)
1072            .map(|tag| tag.0.to_string())
1073            .collect();
1074
1075        assert!(
1076            !tag_strings.contains(&"go-test".to_string()),
1077            "Should NOT find go-test tag, found: {:?}",
1078            tag_strings
1079        );
1080    }
1081
1082    #[gpui::test]
1083    fn test_testify_suite_detection(cx: &mut TestAppContext) {
1084        let language = go_language();
1085
1086        let testify_suite = r#"
1087        package main
1088
1089        import (
1090            "testing"
1091
1092            "github.com/stretchr/testify/suite"
1093        )
1094
1095        type ExampleSuite struct {
1096            suite.Suite
1097        }
1098
1099        func TestExampleSuite(t *testing.T) {
1100            suite.Run(t, new(ExampleSuite))
1101        }
1102
1103        func (s *ExampleSuite) TestSomething_Success() {
1104            // test code
1105        }
1106        "#;
1107
1108        let buffer = cx
1109            .new(|cx| crate::Buffer::local(testify_suite, cx).with_language(language.clone(), cx));
1110        cx.executor().run_until_parked();
1111
1112        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1113            let snapshot = buffer.snapshot();
1114            snapshot.runnable_ranges(0..testify_suite.len()).collect()
1115        });
1116
1117        let tag_strings: Vec<String> = runnables
1118            .iter()
1119            .flat_map(|r| &r.runnable.tags)
1120            .map(|tag| tag.0.to_string())
1121            .collect();
1122
1123        assert!(
1124            tag_strings.contains(&"go-test".to_string()),
1125            "Should find go-test tag, found: {:?}",
1126            tag_strings
1127        );
1128        assert!(
1129            tag_strings.contains(&"go-testify-suite".to_string()),
1130            "Should find go-testify-suite tag, found: {:?}",
1131            tag_strings
1132        );
1133    }
1134
1135    #[gpui::test]
1136    fn test_go_runnable_detection(cx: &mut TestAppContext) {
1137        let language = go_language();
1138
1139        let interpreted_string_subtest = r#"
1140        package main
1141
1142        import "testing"
1143
1144        func TestExample(t *testing.T) {
1145            t.Run("subtest with double quotes", func(t *testing.T) {
1146                // test code
1147            })
1148        }
1149        "#;
1150
1151        let raw_string_subtest = r#"
1152        package main
1153
1154        import "testing"
1155
1156        func TestExample(t *testing.T) {
1157            t.Run(`subtest with
1158            multiline
1159            backticks`, func(t *testing.T) {
1160                // test code
1161            })
1162        }
1163        "#;
1164
1165        let buffer = cx.new(|cx| {
1166            crate::Buffer::local(interpreted_string_subtest, cx).with_language(language.clone(), cx)
1167        });
1168        cx.executor().run_until_parked();
1169
1170        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1171            let snapshot = buffer.snapshot();
1172            snapshot
1173                .runnable_ranges(0..interpreted_string_subtest.len())
1174                .collect()
1175        });
1176
1177        let tag_strings: Vec<String> = runnables
1178            .iter()
1179            .flat_map(|r| &r.runnable.tags)
1180            .map(|tag| tag.0.to_string())
1181            .collect();
1182
1183        assert!(
1184            tag_strings.contains(&"go-test".to_string()),
1185            "Should find go-test tag, found: {:?}",
1186            tag_strings
1187        );
1188        assert!(
1189            tag_strings.contains(&"go-subtest".to_string()),
1190            "Should find go-subtest tag, found: {:?}",
1191            tag_strings
1192        );
1193
1194        let buffer = cx.new(|cx| {
1195            crate::Buffer::local(raw_string_subtest, cx).with_language(language.clone(), cx)
1196        });
1197        cx.executor().run_until_parked();
1198
1199        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1200            let snapshot = buffer.snapshot();
1201            snapshot
1202                .runnable_ranges(0..raw_string_subtest.len())
1203                .collect()
1204        });
1205
1206        let tag_strings: Vec<String> = runnables
1207            .iter()
1208            .flat_map(|r| &r.runnable.tags)
1209            .map(|tag| tag.0.to_string())
1210            .collect();
1211
1212        assert!(
1213            tag_strings.contains(&"go-test".to_string()),
1214            "Should find go-test tag, found: {:?}",
1215            tag_strings
1216        );
1217        assert!(
1218            tag_strings.contains(&"go-subtest".to_string()),
1219            "Should find go-subtest tag, found: {:?}",
1220            tag_strings
1221        );
1222    }
1223
1224    #[gpui::test]
1225    async fn test_go_test_templates_run_arg_is_shell_escaped(cx: &mut TestAppContext) {
1226        let templates = cx
1227            .update(|cx| GoContextProvider.associated_tasks(None, cx))
1228            .await
1229            .expect("Go context provider returns associated tasks");
1230
1231        // `resolve_task` returns `None` for any `ZED_` variable a template
1232        // references but the context omits, so supply all of them.
1233        let context = TaskContext {
1234            cwd: None,
1235            task_variables: TaskVariables::from_iter([
1236                (VariableName::Symbol, "TestFoo".to_string()),
1237                (GO_SUBTEST_NAME_TASK_VARIABLE, "simple_subtest".to_string()),
1238                (
1239                    GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE,
1240                    "table_case".to_string(),
1241                ),
1242                (GO_SUITE_NAME_TASK_VARIABLE, "Suite".to_string()),
1243                (GO_PACKAGE_TASK_VARIABLE, ".".to_string()),
1244                (VariableName::Dirname, "/tmp".to_string()),
1245            ]),
1246            project_env: HashMap::default(),
1247        };
1248
1249        // `go-benchmark` is excluded: its `-run='^$'` form intentionally quotes.
1250        let escaped_run_arg_tags = [
1251            "go-test",
1252            "go-example",
1253            "go-subtest",
1254            "go-fuzz",
1255            "go-testify-suite",
1256            "go-table-test-case",
1257        ];
1258
1259        for tag in escaped_run_arg_tags {
1260            let template = templates
1261                .0
1262                .iter()
1263                .find(|template| template.tags.iter().any(|template_tag| template_tag == tag))
1264                .unwrap_or_else(|| panic!("`{tag}` task template exists"));
1265
1266            let resolved = template
1267                .resolve_task("go", &context)
1268                .unwrap_or_else(|| panic!("`{tag}` template resolves"));
1269
1270            let run_index = resolved
1271                .resolved
1272                .args
1273                .iter()
1274                .position(|arg| arg == "-run")
1275                .unwrap_or_else(|| panic!("`{tag}` resolved args contain a `-run` flag"));
1276            let run_arg = &resolved.resolved.args[run_index + 1];
1277
1278            assert!(
1279                !run_arg.contains('\''),
1280                "`{tag}` -run arg must not shell-quote the regex; single quotes leak \
1281                 literally into Delve's regex and break Debug Test (#53230), got {run_arg:?}"
1282            );
1283            assert!(
1284                run_arg.starts_with("\\^") && run_arg.ends_with("\\$"),
1285                "`{tag}` -run arg must escape its regex anchors as \\^...\\$ so the shell \
1286                 and GoLocator both strip them, got {run_arg:?}"
1287            );
1288        }
1289    }
1290
1291    #[gpui::test]
1292    fn test_go_example_test_detection(cx: &mut TestAppContext) {
1293        let language = go_language();
1294
1295        let example_test = r#"
1296        package main
1297
1298        import "fmt"
1299
1300        func Example() {
1301            fmt.Println("Hello, world!")
1302            // Output: Hello, world!
1303        }
1304        "#;
1305
1306        let buffer =
1307            cx.new(|cx| crate::Buffer::local(example_test, cx).with_language(language.clone(), cx));
1308        cx.executor().run_until_parked();
1309
1310        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1311            let snapshot = buffer.snapshot();
1312            snapshot.runnable_ranges(0..example_test.len()).collect()
1313        });
1314
1315        let tag_strings: Vec<String> = runnables
1316            .iter()
1317            .flat_map(|r| &r.runnable.tags)
1318            .map(|tag| tag.0.to_string())
1319            .collect();
1320
1321        assert!(
1322            tag_strings.contains(&"go-example".to_string()),
1323            "Should find go-example tag, found: {:?}",
1324            tag_strings
1325        );
1326    }
1327
1328    #[gpui::test]
1329    fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
1330        let language = go_language();
1331
1332        let table_test = r#"
1333        package main
1334
1335        import "testing"
1336
1337        func TestExample(t *testing.T) {
1338            _ = "some random string"
1339
1340            testCases := []struct{
1341                name string
1342                anotherStr string
1343            }{
1344                {
1345                    name: "test case 1",
1346                    anotherStr: "foo",
1347                },
1348                {
1349                    name: "test case 2",
1350                    anotherStr: "bar",
1351                },
1352                {
1353                    name: "test case 3",
1354                    anotherStr: "baz",
1355                },
1356            }
1357
1358            notATableTest := []struct{
1359                name string
1360            }{
1361                {
1362                    name: "some string",
1363                },
1364                {
1365                    name: "some other string",
1366                },
1367            }
1368
1369            for _, tc := range testCases {
1370                t.Run(tc.name, func(t *testing.T) {
1371                    // test code here
1372                })
1373            }
1374        }
1375        "#;
1376
1377        let buffer =
1378            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1379        cx.executor().run_until_parked();
1380
1381        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1382            let snapshot = buffer.snapshot();
1383            snapshot.runnable_ranges(0..table_test.len()).collect()
1384        });
1385
1386        let tag_strings: Vec<String> = runnables
1387            .iter()
1388            .flat_map(|r| &r.runnable.tags)
1389            .map(|tag| tag.0.to_string())
1390            .collect();
1391
1392        assert!(
1393            tag_strings.contains(&"go-test".to_string()),
1394            "Should find go-test tag, found: {:?}",
1395            tag_strings
1396        );
1397        assert!(
1398            tag_strings.contains(&"go-table-test-case".to_string()),
1399            "Should find go-table-test-case tag, found: {:?}",
1400            tag_strings
1401        );
1402
1403        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1404        let go_table_test_count = tag_strings
1405            .iter()
1406            .filter(|&tag| tag == "go-table-test-case")
1407            .count();
1408
1409        assert!(
1410            go_test_count == 1,
1411            "Should find exactly 1 go-test, found: {}",
1412            go_test_count
1413        );
1414        assert!(
1415            go_table_test_count == 3,
1416            "Should find exactly 3 go-table-test-case, found: {}",
1417            go_table_test_count
1418        );
1419
1420        let Some(first_case_offset) = table_test.find("anotherStr: \"foo\"") else {
1421            panic!("missing first table test case");
1422        };
1423        let first_case_offset = first_case_offset + "anotherStr".len();
1424        let first_case_runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1425            let snapshot = buffer.snapshot();
1426            snapshot
1427                .runnable_ranges(first_case_offset..first_case_offset)
1428                .collect()
1429        });
1430        let table_test_case_names: Vec<_> = first_case_runnables
1431            .iter()
1432            .filter(|runnable| {
1433                runnable
1434                    .runnable
1435                    .tags
1436                    .iter()
1437                    .any(|tag| tag.0 == "go-table-test-case")
1438            })
1439            .filter_map(|runnable| runnable.extra_captures.get("_table_test_case_name"))
1440            .collect();
1441
1442        assert_eq!(
1443            table_test_case_names,
1444            vec!["\"test case 1\""],
1445            "Should only return the table test case containing the requested range"
1446        );
1447    }
1448
1449    #[gpui::test]
1450    fn test_go_table_test_slice_without_explicit_variable_detection(cx: &mut TestAppContext) {
1451        let language = go_language();
1452
1453        let table_test = r#"
1454        package main
1455
1456        import "testing"
1457
1458        func TestExample(t *testing.T) {
1459            for _, tc := range []struct{
1460                name string
1461                anotherStr string
1462            }{
1463                {
1464                    name: "test case 1",
1465                    anotherStr: "foo",
1466                },
1467                {
1468                    name: "test case 2",
1469                    anotherStr: "bar",
1470                },
1471                {
1472                    name: "test case 3",
1473                    anotherStr: "baz",
1474                },
1475            } {
1476                t.Run(tc.name, func(t *testing.T) {
1477                    // test code here
1478                })
1479            }
1480        }
1481        "#;
1482
1483        let buffer =
1484            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1485        cx.executor().run_until_parked();
1486
1487        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1488            let snapshot = buffer.snapshot();
1489            snapshot.runnable_ranges(0..table_test.len()).collect()
1490        });
1491
1492        let tag_strings: Vec<String> = runnables
1493            .iter()
1494            .flat_map(|r| &r.runnable.tags)
1495            .map(|tag| tag.0.to_string())
1496            .collect();
1497
1498        assert!(
1499            tag_strings.contains(&"go-test".to_string()),
1500            "Should find go-test tag, found: {:?}",
1501            tag_strings
1502        );
1503        assert!(
1504            tag_strings.contains(&"go-table-test-case-without-explicit-variable".to_string()),
1505            "Should find go-table-test-case-without-explicit-variable tag, found: {:?}",
1506            tag_strings
1507        );
1508
1509        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1510        let go_table_test_count = tag_strings
1511            .iter()
1512            .filter(|&tag| tag == "go-table-test-case-without-explicit-variable")
1513            .count();
1514
1515        assert!(
1516            go_test_count == 1,
1517            "Should find exactly 1 go-test, found: {}",
1518            go_test_count
1519        );
1520        assert!(
1521            go_table_test_count == 3,
1522            "Should find exactly 3 go-table-test-case-without-explicit-variable, found: {}",
1523            go_table_test_count
1524        );
1525    }
1526
1527    #[gpui::test]
1528    fn test_go_table_test_map_without_explicit_variable_detection(cx: &mut TestAppContext) {
1529        let language = go_language();
1530
1531        let table_test = r#"
1532        package main
1533
1534        import "testing"
1535
1536        func TestExample(t *testing.T) {
1537            for name, tc := range map[string]struct {
1538          		someStr string
1539          		fail    bool
1540           	}{
1541          		"test failure": {
1542         			someStr: "foo",
1543         			fail:    true,
1544          		},
1545          		"test success": {
1546         			someStr: "bar",
1547         			fail:    false,
1548          		},
1549           	} {
1550                t.Run(name, func(t *testing.T) {
1551                    // test code here
1552                })
1553            }
1554        }
1555        "#;
1556
1557        let buffer =
1558            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1559        cx.executor().run_until_parked();
1560
1561        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1562            let snapshot = buffer.snapshot();
1563            snapshot.runnable_ranges(0..table_test.len()).collect()
1564        });
1565
1566        let tag_strings: Vec<String> = runnables
1567            .iter()
1568            .flat_map(|r| &r.runnable.tags)
1569            .map(|tag| tag.0.to_string())
1570            .collect();
1571
1572        assert!(
1573            tag_strings.contains(&"go-test".to_string()),
1574            "Should find go-test tag, found: {:?}",
1575            tag_strings
1576        );
1577        assert!(
1578            tag_strings.contains(&"go-table-test-case-without-explicit-variable".to_string()),
1579            "Should find go-table-test-case-without-explicit-variable tag, found: {:?}",
1580            tag_strings
1581        );
1582
1583        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1584        let go_table_test_count = tag_strings
1585            .iter()
1586            .filter(|&tag| tag == "go-table-test-case-without-explicit-variable")
1587            .count();
1588
1589        assert!(
1590            go_test_count == 1,
1591            "Should find exactly 1 go-test, found: {}",
1592            go_test_count
1593        );
1594        assert!(
1595            go_table_test_count == 2,
1596            "Should find exactly 2 go-table-test-case-without-explicit-variable, found: {}",
1597            go_table_test_count
1598        );
1599    }
1600
1601    #[gpui::test]
1602    fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
1603        let language = go_language();
1604
1605        let table_test = r#"
1606        package main
1607
1608        func Example() {
1609            _ = "some random string"
1610
1611            notATableTest := []struct{
1612                name string
1613            }{
1614                {
1615                    name: "some string",
1616                },
1617                {
1618                    name: "some other string",
1619                },
1620            }
1621        }
1622        "#;
1623
1624        let buffer =
1625            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1626        cx.executor().run_until_parked();
1627
1628        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1629            let snapshot = buffer.snapshot();
1630            snapshot.runnable_ranges(0..table_test.len()).collect()
1631        });
1632
1633        let tag_strings: Vec<String> = runnables
1634            .iter()
1635            .flat_map(|r| &r.runnable.tags)
1636            .map(|tag| tag.0.to_string())
1637            .collect();
1638
1639        assert!(
1640            !tag_strings.contains(&"go-test".to_string()),
1641            "Should find go-test tag, found: {:?}",
1642            tag_strings
1643        );
1644        assert!(
1645            !tag_strings.contains(&"go-table-test-case".to_string()),
1646            "Should find go-table-test-case tag, found: {:?}",
1647            tag_strings
1648        );
1649    }
1650
1651    #[gpui::test]
1652    fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
1653        let language = go_language();
1654
1655        let table_test = r#"
1656        package main
1657
1658        import "testing"
1659
1660        func TestExample(t *testing.T) {
1661            _ = "some random string"
1662
1663           	testCases := map[string]struct {
1664          		someStr string
1665          		fail    bool
1666           	}{
1667          		"test failure": {
1668         			someStr: "foo",
1669         			fail:    true,
1670          		},
1671          		"test success": {
1672         			someStr: "bar",
1673         			fail:    false,
1674          		},
1675           	}
1676
1677           	notATableTest := map[string]struct {
1678          		someStr string
1679           	}{
1680          		"some string": {
1681         			someStr: "foo",
1682          		},
1683          		"some other string": {
1684         			someStr: "bar",
1685          		},
1686           	}
1687
1688            for name, tc := range testCases {
1689                t.Run(name, func(t *testing.T) {
1690                    // test code here
1691                })
1692            }
1693        }
1694        "#;
1695
1696        let buffer =
1697            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1698        cx.executor().run_until_parked();
1699
1700        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1701            let snapshot = buffer.snapshot();
1702            snapshot.runnable_ranges(0..table_test.len()).collect()
1703        });
1704
1705        let tag_strings: Vec<String> = runnables
1706            .iter()
1707            .flat_map(|r| &r.runnable.tags)
1708            .map(|tag| tag.0.to_string())
1709            .collect();
1710
1711        assert!(
1712            tag_strings.contains(&"go-test".to_string()),
1713            "Should find go-test tag, found: {:?}",
1714            tag_strings
1715        );
1716        assert!(
1717            tag_strings.contains(&"go-table-test-case".to_string()),
1718            "Should find go-table-test-case tag, found: {:?}",
1719            tag_strings
1720        );
1721
1722        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1723        let go_table_test_count = tag_strings
1724            .iter()
1725            .filter(|&tag| tag == "go-table-test-case")
1726            .count();
1727
1728        assert!(
1729            go_test_count == 1,
1730            "Should find exactly 1 go-test, found: {}",
1731            go_test_count
1732        );
1733        assert!(
1734            go_table_test_count == 2,
1735            "Should find exactly 2 go-table-test-case, found: {}",
1736            go_table_test_count
1737        );
1738    }
1739
1740    #[gpui::test]
1741    fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
1742        let language = go_language();
1743
1744        let table_test = r#"
1745        package main
1746
1747        func Example() {
1748            _ = "some random string"
1749
1750           	notATableTest := map[string]struct {
1751          		someStr string
1752           	}{
1753          		"some string": {
1754         			someStr: "foo",
1755          		},
1756          		"some other string": {
1757         			someStr: "bar",
1758          		},
1759           	}
1760        }
1761        "#;
1762
1763        let buffer =
1764            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1765        cx.executor().run_until_parked();
1766
1767        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1768            let snapshot = buffer.snapshot();
1769            snapshot.runnable_ranges(0..table_test.len()).collect()
1770        });
1771
1772        let tag_strings: Vec<String> = runnables
1773            .iter()
1774            .flat_map(|r| &r.runnable.tags)
1775            .map(|tag| tag.0.to_string())
1776            .collect();
1777
1778        assert!(
1779            !tag_strings.contains(&"go-test".to_string()),
1780            "Should find go-test tag, found: {:?}",
1781            tag_strings
1782        );
1783        assert!(
1784            !tag_strings.contains(&"go-table-test-case".to_string()),
1785            "Should find go-table-test-case tag, found: {:?}",
1786            tag_strings
1787        );
1788    }
1789
1790    #[gpui::test]
1791    fn test_go_table_test_stress(cx: &mut TestAppContext) {
1792        let language = go_language();
1793
1794        let mut entries = String::new();
1795        for i in 0..100 {
1796            entries.push_str(&format!(
1797                "                {{ name: \"case {}\", value: {} }},\n",
1798                i, i
1799            ));
1800        }
1801        let table_test = format!(
1802            r#"
1803        package main
1804
1805        import "testing"
1806
1807        func TestStress(t *testing.T) {{
1808            testCases := []struct{{
1809                name  string
1810                value int
1811            }}{{
1812{entries}            }}
1813
1814            for _, tc := range testCases {{
1815                t.Run(tc.name, func(t *testing.T) {{
1816                    _ = tc.value
1817                }})
1818            }}
1819        }}
1820        "#,
1821            entries = entries
1822        );
1823
1824        let buffer = cx.new(|cx| {
1825            crate::Buffer::local(table_test.clone(), cx).with_language(language.clone(), cx)
1826        });
1827        cx.executor().run_until_parked();
1828
1829        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1830            let snapshot = buffer.snapshot();
1831            snapshot.runnable_ranges(0..table_test.len()).collect()
1832        });
1833
1834        let tag_strings: Vec<String> = runnables
1835            .iter()
1836            .flat_map(|r| &r.runnable.tags)
1837            .map(|tag| tag.0.to_string())
1838            .collect();
1839
1840        let go_table_test_count = tag_strings
1841            .iter()
1842            .filter(|&tag| tag == "go-table-test-case")
1843            .count();
1844
1845        assert_eq!(
1846            go_table_test_count, 100,
1847            "Should emit one go-table-test-case per row (got {}); tree-sitter match_limit overflow has regressed",
1848            go_table_test_count
1849        );
1850    }
1851
1852    #[gpui::test]
1853    fn test_go_table_test_mismatched_field(cx: &mut TestAppContext) {
1854        let language = go_language();
1855
1856        let table_test = r#"
1857        package main
1858
1859        import "testing"
1860
1861        func TestMismatchedField(t *testing.T) {
1862            testCases := []struct{
1863                name string
1864            }{
1865                { name: "test case 1" },
1866                { name: "test case 2" },
1867            }
1868
1869            for _, tc := range testCases {
1870                t.Run(tc.desc, func(t *testing.T) {
1871                    // test code here
1872                })
1873            }
1874        }
1875        "#;
1876
1877        let buffer =
1878            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1879        cx.executor().run_until_parked();
1880
1881        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1882            let snapshot = buffer.snapshot();
1883            snapshot.runnable_ranges(0..table_test.len()).collect()
1884        });
1885
1886        let tag_strings: Vec<String> = runnables
1887            .iter()
1888            .flat_map(|r| &r.runnable.tags)
1889            .map(|tag| tag.0.to_string())
1890            .collect();
1891
1892        let go_table_test_count = tag_strings
1893            .iter()
1894            .filter(|&tag| tag == "go-table-test-case")
1895            .count();
1896
1897        assert_eq!(
1898            go_table_test_count, 0,
1899            "Should not emit table-test runnables when t.Run uses a missing row field"
1900        );
1901    }
1902
1903    #[gpui::test]
1904    fn test_go_table_test_slice_picks_correct_field_when_not_first(cx: &mut TestAppContext) {
1905        // The subtest-name field `name` is declared AFTER `anotherStr`, but `t.Run(tc.name, ...)`
1906        // still selects on `name`. The resolver must match `@_field_check` text to the right
1907        // `@_field_name` regardless of source order; "first string field wins" would be a bug.
1908        let language = go_language();
1909
1910        let table_test = r#"
1911        package main
1912
1913        import "testing"
1914
1915        func TestExample(t *testing.T) {
1916            testCases := []struct{
1917                anotherStr string
1918                name       string
1919            }{
1920                {
1921                    anotherStr: "alpha",
1922                    name:       "case alpha",
1923                },
1924                {
1925                    anotherStr: "beta",
1926                    name:       "case beta",
1927                },
1928            }
1929
1930            for _, tc := range testCases {
1931                t.Run(tc.name, func(t *testing.T) {
1932                    _ = tc.anotherStr
1933                })
1934            }
1935        }
1936        "#;
1937
1938        let buffer =
1939            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1940        cx.executor().run_until_parked();
1941
1942        let case_offset = table_test
1943            .find("anotherStr: \"alpha\"")
1944            .expect("source should contain the first case body");
1945        let first_case_runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1946            let snapshot = buffer.snapshot();
1947            snapshot.runnable_ranges(case_offset..case_offset).collect()
1948        });
1949
1950        let case_names: Vec<_> = first_case_runnables
1951            .iter()
1952            .filter(|runnable| {
1953                runnable
1954                    .runnable
1955                    .tags
1956                    .iter()
1957                    .any(|tag| tag.0 == "go-table-test-case")
1958            })
1959            .filter_map(|runnable| runnable.extra_captures.get("_table_test_case_name"))
1960            .collect();
1961
1962        assert_eq!(
1963            case_names,
1964            vec!["\"case alpha\""],
1965            "Resolver should pick the field matching `tc.name`, not the first string field"
1966        );
1967    }
1968
1969    #[gpui::test]
1970    fn test_go_table_test_map_extras_include_case_name(cx: &mut TestAppContext) {
1971        let language = go_language();
1972
1973        let table_test = r#"
1974        package main
1975
1976        import "testing"
1977
1978        func TestExample(t *testing.T) {
1979            testCases := map[string]struct {
1980                fail bool
1981            }{
1982                "test failure": {fail: true},
1983                "test success": {fail: false},
1984            }
1985
1986            for name, tc := range testCases {
1987                t.Run(name, func(t *testing.T) {
1988                    _ = tc.fail
1989                })
1990            }
1991        }
1992        "#;
1993
1994        let buffer =
1995            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1996        cx.executor().run_until_parked();
1997
1998        let all_runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1999            let snapshot = buffer.snapshot();
2000            snapshot.runnable_ranges(0..table_test.len()).collect()
2001        });
2002        let all_case_names: Vec<_> = all_runnables
2003            .iter()
2004            .filter(|runnable| {
2005                runnable
2006                    .runnable
2007                    .tags
2008                    .iter()
2009                    .any(|tag| tag.0 == "go-table-test-case")
2010            })
2011            .filter_map(|runnable| runnable.extra_captures.get("_table_test_case_name"))
2012            .cloned()
2013            .collect();
2014        assert_eq!(
2015            all_case_names,
2016            vec!["\"test failure\"", "\"test success\""],
2017            "Map-based table tests should surface each row's key as `_table_test_case_name`"
2018        );
2019    }
2020
2021    #[gpui::test]
2022    fn test_go_outline_includes_methods_with_receiver_forms(cx: &mut TestAppContext) {
2023        let language = go_language();
2024
2025        let source = r#"
2026        package main
2027
2028        type v2 struct{}
2029
2030        func (v2) BrokenMethod() {
2031            println("start")
2032        }
2033
2034        func (_ v2) UnderscoreReceiverMethod() {
2035            println("start")
2036        }
2037
2038        func (v v2) NamedReceiverMethod() {
2039            println("start")
2040        }
2041
2042        func (v *v2) PointerReceiverMethod() {
2043            println("start")
2044        }
2045
2046        func WorkingFunction() {
2047            println("start")
2048        }
2049        "#
2050        .unindent();
2051
2052        let buffer =
2053            cx.new(|cx| crate::Buffer::local(source.clone(), cx).with_language(language, cx));
2054        let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2055        let outline = snapshot.outline(None);
2056
2057        assert_eq!(
2058            outline
2059                .items
2060                .iter()
2061                .map(|item| item.text.as_str())
2062                .collect::<Vec<_>>(),
2063            &[
2064                "type v2",
2065                "func (v2) BrokenMethod",
2066                "func (_ v2) UnderscoreReceiverMethod",
2067                "func (v v2) NamedReceiverMethod",
2068                "func (v *v2) PointerReceiverMethod",
2069                "func WorkingFunction",
2070            ]
2071        );
2072
2073        for (method_name, expected_symbol) in [
2074            ("BrokenMethod", "func (v2) BrokenMethod"),
2075            (
2076                "UnderscoreReceiverMethod",
2077                "func (_ v2) UnderscoreReceiverMethod",
2078            ),
2079            ("NamedReceiverMethod", "func (v v2) NamedReceiverMethod"),
2080            (
2081                "PointerReceiverMethod",
2082                "func (v *v2) PointerReceiverMethod",
2083            ),
2084            ("WorkingFunction", "func WorkingFunction"),
2085        ] {
2086            let method_position = source
2087                .find(&format!("{method_name}()"))
2088                .expect("method should exist in source");
2089            let body_position = source[method_position..]
2090                .find("println")
2091                .map(|body_offset| method_position + body_offset)
2092                .expect("method should contain a body");
2093            let symbols = snapshot.symbols_containing(body_position, None);
2094            assert_eq!(
2095                symbols
2096                    .last()
2097                    .map(|item| item.text.as_str())
2098                    .expect("method should have an outline symbol"),
2099                expected_symbol
2100            );
2101        }
2102    }
2103
2104    #[test]
2105    fn test_extract_subtest_name() {
2106        // Interpreted string literal
2107        let input_double_quoted = r#""subtest with double quotes""#;
2108        let result = extract_subtest_name(input_double_quoted);
2109        assert_eq!(result, Some(r#"subtest_with_double_quotes"#.to_string()));
2110
2111        let input_double_quoted_with_backticks = r#""test with `backticks` inside""#;
2112        let result = extract_subtest_name(input_double_quoted_with_backticks);
2113        assert_eq!(result, Some(r#"test_with_`backticks`_inside"#.to_string()));
2114
2115        // Raw string literal
2116        let input_with_backticks = r#"`subtest with backticks`"#;
2117        let result = extract_subtest_name(input_with_backticks);
2118        assert_eq!(result, Some(r#"subtest_with_backticks"#.to_string()));
2119
2120        let input_raw_with_quotes = r#"`test with "quotes" and other chars`"#;
2121        let result = extract_subtest_name(input_raw_with_quotes);
2122        assert_eq!(
2123            result,
2124            Some(r#"test_with_\"quotes\"_and_other_chars"#.to_string())
2125        );
2126
2127        let input_multiline = r#"`subtest with
2128        multiline
2129        backticks`"#;
2130        let result = extract_subtest_name(input_multiline);
2131        assert_eq!(
2132            result,
2133            Some(r#"subtest_with_________multiline_________backticks"#.to_string())
2134        );
2135
2136        let input_with_double_quotes = r#"`test with "double quotes"`"#;
2137        let result = extract_subtest_name(input_with_double_quotes);
2138        assert_eq!(result, Some(r#"test_with_\"double_quotes\""#.to_string()));
2139    }
2140}
2141
Served at tenant.openagents/omega Member data and write actions are omitted.