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