Skip to repository content2141 lines · 72.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:35:12.228Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
go.rs
1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::{App, AsyncApp, Entity, Task};
6use http_client::github::latest_github_release;
7pub use language::*;
8use language::{
9 LanguageName, LanguageToolchainStore, LspAdapterDelegate, LspInstaller,
10 language_settings::LanguageSettings,
11};
12use lsp::{LanguageServerBinary, LanguageServerName};
13
14use project::lsp_store::language_server_settings;
15use regex::Regex;
16use serde_json::{Value, json};
17use settings::SemanticTokenRules;
18use smol::fs;
19use std::{
20 borrow::Cow,
21 ffi::{OsStr, OsString},
22 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: ®ex::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