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