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