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