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_smol_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_smol_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_smol_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 name: &str,
338 kind: lsp::SymbolKind,
339 language: &Arc<Language>,
340 ) -> Option<CodeLabel> {
341 let (text, filter_range, display_range) = match 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_testify_suite_detection(cx: &mut TestAppContext) {
852 let language = language("go", tree_sitter_go::LANGUAGE.into());
853
854 let testify_suite = r#"
855 package main
856
857 import (
858 "testing"
859
860 "github.com/stretchr/testify/suite"
861 )
862
863 type ExampleSuite struct {
864 suite.Suite
865 }
866
867 func TestExampleSuite(t *testing.T) {
868 suite.Run(t, new(ExampleSuite))
869 }
870
871 func (s *ExampleSuite) TestSomething_Success() {
872 // test code
873 }
874 "#;
875
876 let buffer = cx
877 .new(|cx| crate::Buffer::local(testify_suite, cx).with_language(language.clone(), cx));
878 cx.executor().run_until_parked();
879
880 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
881 let snapshot = buffer.snapshot();
882 snapshot.runnable_ranges(0..testify_suite.len()).collect()
883 });
884
885 let tag_strings: Vec<String> = runnables
886 .iter()
887 .flat_map(|r| &r.runnable.tags)
888 .map(|tag| tag.0.to_string())
889 .collect();
890
891 assert!(
892 tag_strings.contains(&"go-test".to_string()),
893 "Should find go-test tag, found: {:?}",
894 tag_strings
895 );
896 assert!(
897 tag_strings.contains(&"go-testify-suite".to_string()),
898 "Should find go-testify-suite tag, found: {:?}",
899 tag_strings
900 );
901 }
902
903 #[gpui::test]
904 fn test_go_runnable_detection(cx: &mut TestAppContext) {
905 let language = language("go", tree_sitter_go::LANGUAGE.into());
906
907 let interpreted_string_subtest = r#"
908 package main
909
910 import "testing"
911
912 func TestExample(t *testing.T) {
913 t.Run("subtest with double quotes", func(t *testing.T) {
914 // test code
915 })
916 }
917 "#;
918
919 let raw_string_subtest = r#"
920 package main
921
922 import "testing"
923
924 func TestExample(t *testing.T) {
925 t.Run(`subtest with
926 multiline
927 backticks`, func(t *testing.T) {
928 // test code
929 })
930 }
931 "#;
932
933 let buffer = cx.new(|cx| {
934 crate::Buffer::local(interpreted_string_subtest, cx).with_language(language.clone(), cx)
935 });
936 cx.executor().run_until_parked();
937
938 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
939 let snapshot = buffer.snapshot();
940 snapshot
941 .runnable_ranges(0..interpreted_string_subtest.len())
942 .collect()
943 });
944
945 let tag_strings: Vec<String> = runnables
946 .iter()
947 .flat_map(|r| &r.runnable.tags)
948 .map(|tag| tag.0.to_string())
949 .collect();
950
951 assert!(
952 tag_strings.contains(&"go-test".to_string()),
953 "Should find go-test tag, found: {:?}",
954 tag_strings
955 );
956 assert!(
957 tag_strings.contains(&"go-subtest".to_string()),
958 "Should find go-subtest tag, found: {:?}",
959 tag_strings
960 );
961
962 let buffer = cx.new(|cx| {
963 crate::Buffer::local(raw_string_subtest, cx).with_language(language.clone(), cx)
964 });
965 cx.executor().run_until_parked();
966
967 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
968 let snapshot = buffer.snapshot();
969 snapshot
970 .runnable_ranges(0..raw_string_subtest.len())
971 .collect()
972 });
973
974 let tag_strings: Vec<String> = runnables
975 .iter()
976 .flat_map(|r| &r.runnable.tags)
977 .map(|tag| tag.0.to_string())
978 .collect();
979
980 assert!(
981 tag_strings.contains(&"go-test".to_string()),
982 "Should find go-test tag, found: {:?}",
983 tag_strings
984 );
985 assert!(
986 tag_strings.contains(&"go-subtest".to_string()),
987 "Should find go-subtest tag, found: {:?}",
988 tag_strings
989 );
990 }
991
992 #[gpui::test]
993 fn test_go_example_test_detection(cx: &mut TestAppContext) {
994 let language = language("go", tree_sitter_go::LANGUAGE.into());
995
996 let example_test = r#"
997 package main
998
999 import "fmt"
1000
1001 func Example() {
1002 fmt.Println("Hello, world!")
1003 // Output: Hello, world!
1004 }
1005 "#;
1006
1007 let buffer =
1008 cx.new(|cx| crate::Buffer::local(example_test, cx).with_language(language.clone(), cx));
1009 cx.executor().run_until_parked();
1010
1011 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1012 let snapshot = buffer.snapshot();
1013 snapshot.runnable_ranges(0..example_test.len()).collect()
1014 });
1015
1016 let tag_strings: Vec<String> = runnables
1017 .iter()
1018 .flat_map(|r| &r.runnable.tags)
1019 .map(|tag| tag.0.to_string())
1020 .collect();
1021
1022 assert!(
1023 tag_strings.contains(&"go-example".to_string()),
1024 "Should find go-example tag, found: {:?}",
1025 tag_strings
1026 );
1027 }
1028
1029 #[gpui::test]
1030 fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
1031 let language = language("go", tree_sitter_go::LANGUAGE.into());
1032
1033 let table_test = r#"
1034 package main
1035
1036 import "testing"
1037
1038 func TestExample(t *testing.T) {
1039 _ = "some random string"
1040
1041 testCases := []struct{
1042 name string
1043 anotherStr string
1044 }{
1045 {
1046 name: "test case 1",
1047 anotherStr: "foo",
1048 },
1049 {
1050 name: "test case 2",
1051 anotherStr: "bar",
1052 },
1053 {
1054 name: "test case 3",
1055 anotherStr: "baz",
1056 },
1057 }
1058
1059 notATableTest := []struct{
1060 name string
1061 }{
1062 {
1063 name: "some string",
1064 },
1065 {
1066 name: "some other string",
1067 },
1068 }
1069
1070 for _, tc := range testCases {
1071 t.Run(tc.name, func(t *testing.T) {
1072 // test code here
1073 })
1074 }
1075 }
1076 "#;
1077
1078 let buffer =
1079 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1080 cx.executor().run_until_parked();
1081
1082 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1083 let snapshot = buffer.snapshot();
1084 snapshot.runnable_ranges(0..table_test.len()).collect()
1085 });
1086
1087 let tag_strings: Vec<String> = runnables
1088 .iter()
1089 .flat_map(|r| &r.runnable.tags)
1090 .map(|tag| tag.0.to_string())
1091 .collect();
1092
1093 assert!(
1094 tag_strings.contains(&"go-test".to_string()),
1095 "Should find go-test tag, found: {:?}",
1096 tag_strings
1097 );
1098 assert!(
1099 tag_strings.contains(&"go-table-test-case".to_string()),
1100 "Should find go-table-test-case tag, found: {:?}",
1101 tag_strings
1102 );
1103
1104 let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1105 // This is currently broken; see #39148
1106 // let go_table_test_count = tag_strings
1107 // .iter()
1108 // .filter(|&tag| tag == "go-table-test-case")
1109 // .count();
1110
1111 assert!(
1112 go_test_count == 1,
1113 "Should find exactly 1 go-test, found: {}",
1114 go_test_count
1115 );
1116 // assert!(
1117 // go_table_test_count == 3,
1118 // "Should find exactly 3 go-table-test-case, found: {}",
1119 // go_table_test_count
1120 // );
1121 }
1122
1123 #[gpui::test]
1124 fn test_go_table_test_slice_without_explicit_variable_detection(cx: &mut TestAppContext) {
1125 let language = language("go", tree_sitter_go::LANGUAGE.into());
1126
1127 let table_test = r#"
1128 package main
1129
1130 import "testing"
1131
1132 func TestExample(t *testing.T) {
1133 for _, tc := range []struct{
1134 name string
1135 anotherStr string
1136 }{
1137 {
1138 name: "test case 1",
1139 anotherStr: "foo",
1140 },
1141 {
1142 name: "test case 2",
1143 anotherStr: "bar",
1144 },
1145 {
1146 name: "test case 3",
1147 anotherStr: "baz",
1148 },
1149 } {
1150 t.Run(tc.name, func(t *testing.T) {
1151 // test code here
1152 })
1153 }
1154 }
1155 "#;
1156
1157 let buffer =
1158 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1159 cx.executor().run_until_parked();
1160
1161 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1162 let snapshot = buffer.snapshot();
1163 snapshot.runnable_ranges(0..table_test.len()).collect()
1164 });
1165
1166 let tag_strings: Vec<String> = runnables
1167 .iter()
1168 .flat_map(|r| &r.runnable.tags)
1169 .map(|tag| tag.0.to_string())
1170 .collect();
1171
1172 assert!(
1173 tag_strings.contains(&"go-test".to_string()),
1174 "Should find go-test tag, found: {:?}",
1175 tag_strings
1176 );
1177 assert!(
1178 tag_strings.contains(&"go-table-test-case-without-explicit-variable".to_string()),
1179 "Should find go-table-test-case-without-explicit-variable tag, found: {:?}",
1180 tag_strings
1181 );
1182
1183 let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1184
1185 assert!(
1186 go_test_count == 1,
1187 "Should find exactly 1 go-test, found: {}",
1188 go_test_count
1189 );
1190 }
1191
1192 #[gpui::test]
1193 fn test_go_table_test_map_without_explicit_variable_detection(cx: &mut TestAppContext) {
1194 let language = language("go", tree_sitter_go::LANGUAGE.into());
1195
1196 let table_test = r#"
1197 package main
1198
1199 import "testing"
1200
1201 func TestExample(t *testing.T) {
1202 for name, tc := range map[string]struct {
1203 someStr string
1204 fail bool
1205 }{
1206 "test failure": {
1207 someStr: "foo",
1208 fail: true,
1209 },
1210 "test success": {
1211 someStr: "bar",
1212 fail: false,
1213 },
1214 } {
1215 t.Run(name, func(t *testing.T) {
1216 // test code here
1217 })
1218 }
1219 }
1220 "#;
1221
1222 let buffer =
1223 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1224 cx.executor().run_until_parked();
1225
1226 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1227 let snapshot = buffer.snapshot();
1228 snapshot.runnable_ranges(0..table_test.len()).collect()
1229 });
1230
1231 let tag_strings: Vec<String> = runnables
1232 .iter()
1233 .flat_map(|r| &r.runnable.tags)
1234 .map(|tag| tag.0.to_string())
1235 .collect();
1236
1237 assert!(
1238 tag_strings.contains(&"go-test".to_string()),
1239 "Should find go-test tag, found: {:?}",
1240 tag_strings
1241 );
1242 assert!(
1243 tag_strings.contains(&"go-table-test-case-without-explicit-variable".to_string()),
1244 "Should find go-table-test-case-without-explicit-variable tag, found: {:?}",
1245 tag_strings
1246 );
1247
1248 let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1249 let go_table_test_count = tag_strings
1250 .iter()
1251 .filter(|&tag| tag == "go-table-test-case-without-explicit-variable")
1252 .count();
1253
1254 assert!(
1255 go_test_count == 1,
1256 "Should find exactly 1 go-test, found: {}",
1257 go_test_count
1258 );
1259 assert!(
1260 go_table_test_count == 2,
1261 "Should find exactly 2 go-table-test-case-without-explicit-variable, found: {}",
1262 go_table_test_count
1263 );
1264 }
1265
1266 #[gpui::test]
1267 fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
1268 let language = language("go", tree_sitter_go::LANGUAGE.into());
1269
1270 let table_test = r#"
1271 package main
1272
1273 func Example() {
1274 _ = "some random string"
1275
1276 notATableTest := []struct{
1277 name string
1278 }{
1279 {
1280 name: "some string",
1281 },
1282 {
1283 name: "some other string",
1284 },
1285 }
1286 }
1287 "#;
1288
1289 let buffer =
1290 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1291 cx.executor().run_until_parked();
1292
1293 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1294 let snapshot = buffer.snapshot();
1295 snapshot.runnable_ranges(0..table_test.len()).collect()
1296 });
1297
1298 let tag_strings: Vec<String> = runnables
1299 .iter()
1300 .flat_map(|r| &r.runnable.tags)
1301 .map(|tag| tag.0.to_string())
1302 .collect();
1303
1304 assert!(
1305 !tag_strings.contains(&"go-test".to_string()),
1306 "Should find go-test tag, found: {:?}",
1307 tag_strings
1308 );
1309 assert!(
1310 !tag_strings.contains(&"go-table-test-case".to_string()),
1311 "Should find go-table-test-case tag, found: {:?}",
1312 tag_strings
1313 );
1314 }
1315
1316 #[gpui::test]
1317 fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
1318 let language = language("go", tree_sitter_go::LANGUAGE.into());
1319
1320 let table_test = r#"
1321 package main
1322
1323 import "testing"
1324
1325 func TestExample(t *testing.T) {
1326 _ = "some random string"
1327
1328 testCases := map[string]struct {
1329 someStr string
1330 fail bool
1331 }{
1332 "test failure": {
1333 someStr: "foo",
1334 fail: true,
1335 },
1336 "test success": {
1337 someStr: "bar",
1338 fail: false,
1339 },
1340 }
1341
1342 notATableTest := map[string]struct {
1343 someStr string
1344 }{
1345 "some string": {
1346 someStr: "foo",
1347 },
1348 "some other string": {
1349 someStr: "bar",
1350 },
1351 }
1352
1353 for name, tc := range testCases {
1354 t.Run(name, func(t *testing.T) {
1355 // test code here
1356 })
1357 }
1358 }
1359 "#;
1360
1361 let buffer =
1362 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1363 cx.executor().run_until_parked();
1364
1365 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1366 let snapshot = buffer.snapshot();
1367 snapshot.runnable_ranges(0..table_test.len()).collect()
1368 });
1369
1370 let tag_strings: Vec<String> = runnables
1371 .iter()
1372 .flat_map(|r| &r.runnable.tags)
1373 .map(|tag| tag.0.to_string())
1374 .collect();
1375
1376 assert!(
1377 tag_strings.contains(&"go-test".to_string()),
1378 "Should find go-test tag, found: {:?}",
1379 tag_strings
1380 );
1381 assert!(
1382 tag_strings.contains(&"go-table-test-case".to_string()),
1383 "Should find go-table-test-case tag, found: {:?}",
1384 tag_strings
1385 );
1386
1387 let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1388 let go_table_test_count = tag_strings
1389 .iter()
1390 .filter(|&tag| tag == "go-table-test-case")
1391 .count();
1392
1393 assert!(
1394 go_test_count == 1,
1395 "Should find exactly 1 go-test, found: {}",
1396 go_test_count
1397 );
1398 assert!(
1399 go_table_test_count == 2,
1400 "Should find exactly 2 go-table-test-case, found: {}",
1401 go_table_test_count
1402 );
1403 }
1404
1405 #[gpui::test]
1406 fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
1407 let language = language("go", tree_sitter_go::LANGUAGE.into());
1408
1409 let table_test = r#"
1410 package main
1411
1412 func Example() {
1413 _ = "some random string"
1414
1415 notATableTest := map[string]struct {
1416 someStr string
1417 }{
1418 "some string": {
1419 someStr: "foo",
1420 },
1421 "some other string": {
1422 someStr: "bar",
1423 },
1424 }
1425 }
1426 "#;
1427
1428 let buffer =
1429 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1430 cx.executor().run_until_parked();
1431
1432 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1433 let snapshot = buffer.snapshot();
1434 snapshot.runnable_ranges(0..table_test.len()).collect()
1435 });
1436
1437 let tag_strings: Vec<String> = runnables
1438 .iter()
1439 .flat_map(|r| &r.runnable.tags)
1440 .map(|tag| tag.0.to_string())
1441 .collect();
1442
1443 assert!(
1444 !tag_strings.contains(&"go-test".to_string()),
1445 "Should find go-test tag, found: {:?}",
1446 tag_strings
1447 );
1448 assert!(
1449 !tag_strings.contains(&"go-table-test-case".to_string()),
1450 "Should find go-table-test-case tag, found: {:?}",
1451 tag_strings
1452 );
1453 }
1454
1455 #[test]
1456 fn test_extract_subtest_name() {
1457 // Interpreted string literal
1458 let input_double_quoted = r#""subtest with double quotes""#;
1459 let result = extract_subtest_name(input_double_quoted);
1460 assert_eq!(result, Some(r#"subtest_with_double_quotes"#.to_string()));
1461
1462 let input_double_quoted_with_backticks = r#""test with `backticks` inside""#;
1463 let result = extract_subtest_name(input_double_quoted_with_backticks);
1464 assert_eq!(result, Some(r#"test_with_`backticks`_inside"#.to_string()));
1465
1466 // Raw string literal
1467 let input_with_backticks = r#"`subtest with backticks`"#;
1468 let result = extract_subtest_name(input_with_backticks);
1469 assert_eq!(result, Some(r#"subtest_with_backticks"#.to_string()));
1470
1471 let input_raw_with_quotes = r#"`test with "quotes" and other chars`"#;
1472 let result = extract_subtest_name(input_raw_with_quotes);
1473 assert_eq!(
1474 result,
1475 Some(r#"test_with_\"quotes\"_and_other_chars"#.to_string())
1476 );
1477
1478 let input_multiline = r#"`subtest with
1479 multiline
1480 backticks`"#;
1481 let result = extract_subtest_name(input_multiline);
1482 assert_eq!(
1483 result,
1484 Some(r#"subtest_with_________multiline_________backticks"#.to_string())
1485 );
1486
1487 let input_with_double_quotes = r#"`test with "double quotes"`"#;
1488 let result = extract_subtest_name(input_with_double_quotes);
1489 assert_eq!(result, Some(r#"test_with_\"double_quotes\""#.to_string()));
1490 }
1491}