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