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!["go-table-test-case".to_owned()],
598 ..TaskTemplate::default()
599 },
600 TaskTemplate {
601 label: format!(
602 "go test {} -run {}",
603 GO_PACKAGE_TASK_VARIABLE.template_value(),
604 VariableName::Symbol.template_value(),
605 ),
606 command: "go".into(),
607 args: vec![
608 "test".into(),
609 "-run".into(),
610 format!("\\^{}\\$", VariableName::Symbol.template_value(),),
611 ],
612 tags: vec!["go-test".to_owned()],
613 cwd: package_cwd.clone(),
614 ..TaskTemplate::default()
615 },
616 TaskTemplate {
617 label: format!(
618 "go test {} -run {}",
619 GO_PACKAGE_TASK_VARIABLE.template_value(),
620 VariableName::Symbol.template_value(),
621 ),
622 command: "go".into(),
623 args: vec![
624 "test".into(),
625 "-run".into(),
626 format!("\\^{}\\$", VariableName::Symbol.template_value(),),
627 ],
628 tags: vec!["go-example".to_owned()],
629 cwd: package_cwd.clone(),
630 ..TaskTemplate::default()
631 },
632 TaskTemplate {
633 label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
634 command: "go".into(),
635 args: vec!["test".into()],
636 cwd: package_cwd.clone(),
637 ..TaskTemplate::default()
638 },
639 TaskTemplate {
640 label: "go test ./...".into(),
641 command: "go".into(),
642 args: vec!["test".into(), "./...".into()],
643 cwd: module_cwd.clone(),
644 ..TaskTemplate::default()
645 },
646 TaskTemplate {
647 label: format!(
648 "go test {} -v -run {}/{}",
649 GO_PACKAGE_TASK_VARIABLE.template_value(),
650 VariableName::Symbol.template_value(),
651 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
652 ),
653 command: "go".into(),
654 args: vec![
655 "test".into(),
656 "-v".into(),
657 "-run".into(),
658 format!(
659 "'^{}$/^{}$'",
660 VariableName::Symbol.template_value(),
661 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
662 ),
663 ],
664 cwd: package_cwd.clone(),
665 tags: vec!["go-subtest".to_owned()],
666 ..TaskTemplate::default()
667 },
668 TaskTemplate {
669 label: format!(
670 "go test {} -bench {}",
671 GO_PACKAGE_TASK_VARIABLE.template_value(),
672 VariableName::Symbol.template_value()
673 ),
674 command: "go".into(),
675 args: vec![
676 "test".into(),
677 "-benchmem".into(),
678 "-run='^$'".into(),
679 "-bench".into(),
680 format!("\\^{}\\$", VariableName::Symbol.template_value()),
681 ],
682 cwd: package_cwd.clone(),
683 tags: vec!["go-benchmark".to_owned()],
684 ..TaskTemplate::default()
685 },
686 TaskTemplate {
687 label: format!(
688 "go test {} -fuzz=Fuzz -run {}",
689 GO_PACKAGE_TASK_VARIABLE.template_value(),
690 VariableName::Symbol.template_value(),
691 ),
692 command: "go".into(),
693 args: vec![
694 "test".into(),
695 "-fuzz=Fuzz".into(),
696 "-run".into(),
697 format!("\\^{}\\$", VariableName::Symbol.template_value(),),
698 ],
699 tags: vec!["go-fuzz".to_owned()],
700 cwd: package_cwd.clone(),
701 ..TaskTemplate::default()
702 },
703 TaskTemplate {
704 label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
705 command: "go".into(),
706 args: vec!["run".into(), ".".into()],
707 cwd: package_cwd.clone(),
708 tags: vec!["go-main".to_owned()],
709 ..TaskTemplate::default()
710 },
711 TaskTemplate {
712 label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
713 command: "go".into(),
714 args: vec!["generate".into()],
715 cwd: package_cwd,
716 tags: vec!["go-generate".to_owned()],
717 ..TaskTemplate::default()
718 },
719 TaskTemplate {
720 label: "go generate ./...".into(),
721 command: "go".into(),
722 args: vec!["generate".into(), "./...".into()],
723 cwd: module_cwd,
724 ..TaskTemplate::default()
725 },
726 ])))
727 }
728}
729
730fn extract_subtest_name(input: &str) -> Option<String> {
731 let content = if input.starts_with('`') && input.ends_with('`') {
732 input.trim_matches('`')
733 } else {
734 input.trim_matches('"')
735 };
736
737 let processed = content
738 .chars()
739 .map(|c| if c.is_whitespace() { '_' } else { c })
740 .collect::<String>();
741
742 Some(
743 GO_ESCAPE_SUBTEST_NAME_REGEX
744 .replace_all(&processed, |caps: ®ex::Captures| {
745 format!("\\{}", &caps[0])
746 })
747 .to_string(),
748 )
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use crate::language;
755 use gpui::{AppContext, Hsla, TestAppContext};
756 use theme::SyntaxTheme;
757
758 #[gpui::test]
759 async fn test_go_label_for_completion() {
760 let adapter = Arc::new(GoLspAdapter);
761 let language = language("go", tree_sitter_go::LANGUAGE.into());
762
763 let theme = SyntaxTheme::new_test([
764 ("type", Hsla::default()),
765 ("keyword", Hsla::default()),
766 ("function", Hsla::default()),
767 ("number", Hsla::default()),
768 ("property", Hsla::default()),
769 ]);
770 language.set_theme(&theme);
771
772 let grammar = language.grammar().unwrap();
773 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
774 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
775 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
776 let highlight_number = grammar.highlight_id_for_name("number").unwrap();
777 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
778
779 assert_eq!(
780 adapter
781 .label_for_completion(
782 &lsp::CompletionItem {
783 kind: Some(lsp::CompletionItemKind::FUNCTION),
784 label: "Hello".to_string(),
785 detail: Some("func(a B) c.D".to_string()),
786 ..Default::default()
787 },
788 &language
789 )
790 .await,
791 Some(CodeLabel::new(
792 "Hello(a B) c.D".to_string(),
793 0..5,
794 vec![
795 (0..5, highlight_function),
796 (8..9, highlight_type),
797 (13..14, highlight_type),
798 ]
799 ))
800 );
801
802 // Nested methods
803 assert_eq!(
804 adapter
805 .label_for_completion(
806 &lsp::CompletionItem {
807 kind: Some(lsp::CompletionItemKind::METHOD),
808 label: "one.two.Three".to_string(),
809 detail: Some("func() [3]interface{}".to_string()),
810 ..Default::default()
811 },
812 &language
813 )
814 .await,
815 Some(CodeLabel::new(
816 "one.two.Three() [3]interface{}".to_string(),
817 0..13,
818 vec![
819 (8..13, highlight_function),
820 (17..18, highlight_number),
821 (19..28, highlight_keyword),
822 ],
823 ))
824 );
825
826 // Nested fields
827 assert_eq!(
828 adapter
829 .label_for_completion(
830 &lsp::CompletionItem {
831 kind: Some(lsp::CompletionItemKind::FIELD),
832 label: "two.Three".to_string(),
833 detail: Some("a.Bcd".to_string()),
834 ..Default::default()
835 },
836 &language
837 )
838 .await,
839 Some(CodeLabel::new(
840 "two.Three a.Bcd".to_string(),
841 0..9,
842 vec![(4..9, highlight_field), (12..15, highlight_type)],
843 ))
844 );
845 }
846
847 #[gpui::test]
848 fn test_testify_suite_detection(cx: &mut TestAppContext) {
849 let language = language("go", tree_sitter_go::LANGUAGE.into());
850
851 let testify_suite = r#"
852 package main
853
854 import (
855 "testing"
856
857 "github.com/stretchr/testify/suite"
858 )
859
860 type ExampleSuite struct {
861 suite.Suite
862 }
863
864 func TestExampleSuite(t *testing.T) {
865 suite.Run(t, new(ExampleSuite))
866 }
867
868 func (s *ExampleSuite) TestSomething_Success() {
869 // test code
870 }
871 "#;
872
873 let buffer = cx
874 .new(|cx| crate::Buffer::local(testify_suite, cx).with_language(language.clone(), cx));
875 cx.executor().run_until_parked();
876
877 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
878 let snapshot = buffer.snapshot();
879 snapshot.runnable_ranges(0..testify_suite.len()).collect()
880 });
881
882 let tag_strings: Vec<String> = runnables
883 .iter()
884 .flat_map(|r| &r.runnable.tags)
885 .map(|tag| tag.0.to_string())
886 .collect();
887
888 assert!(
889 tag_strings.contains(&"go-test".to_string()),
890 "Should find go-test tag, found: {:?}",
891 tag_strings
892 );
893 assert!(
894 tag_strings.contains(&"go-testify-suite".to_string()),
895 "Should find go-testify-suite tag, found: {:?}",
896 tag_strings
897 );
898 }
899
900 #[gpui::test]
901 fn test_go_runnable_detection(cx: &mut TestAppContext) {
902 let language = language("go", tree_sitter_go::LANGUAGE.into());
903
904 let interpreted_string_subtest = r#"
905 package main
906
907 import "testing"
908
909 func TestExample(t *testing.T) {
910 t.Run("subtest with double quotes", func(t *testing.T) {
911 // test code
912 })
913 }
914 "#;
915
916 let raw_string_subtest = r#"
917 package main
918
919 import "testing"
920
921 func TestExample(t *testing.T) {
922 t.Run(`subtest with
923 multiline
924 backticks`, func(t *testing.T) {
925 // test code
926 })
927 }
928 "#;
929
930 let buffer = cx.new(|cx| {
931 crate::Buffer::local(interpreted_string_subtest, cx).with_language(language.clone(), cx)
932 });
933 cx.executor().run_until_parked();
934
935 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
936 let snapshot = buffer.snapshot();
937 snapshot
938 .runnable_ranges(0..interpreted_string_subtest.len())
939 .collect()
940 });
941
942 let tag_strings: Vec<String> = runnables
943 .iter()
944 .flat_map(|r| &r.runnable.tags)
945 .map(|tag| tag.0.to_string())
946 .collect();
947
948 assert!(
949 tag_strings.contains(&"go-test".to_string()),
950 "Should find go-test tag, found: {:?}",
951 tag_strings
952 );
953 assert!(
954 tag_strings.contains(&"go-subtest".to_string()),
955 "Should find go-subtest tag, found: {:?}",
956 tag_strings
957 );
958
959 let buffer = cx.new(|cx| {
960 crate::Buffer::local(raw_string_subtest, cx).with_language(language.clone(), cx)
961 });
962 cx.executor().run_until_parked();
963
964 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
965 let snapshot = buffer.snapshot();
966 snapshot
967 .runnable_ranges(0..raw_string_subtest.len())
968 .collect()
969 });
970
971 let tag_strings: Vec<String> = runnables
972 .iter()
973 .flat_map(|r| &r.runnable.tags)
974 .map(|tag| tag.0.to_string())
975 .collect();
976
977 assert!(
978 tag_strings.contains(&"go-test".to_string()),
979 "Should find go-test tag, found: {:?}",
980 tag_strings
981 );
982 assert!(
983 tag_strings.contains(&"go-subtest".to_string()),
984 "Should find go-subtest tag, found: {:?}",
985 tag_strings
986 );
987 }
988
989 #[gpui::test]
990 fn test_go_example_test_detection(cx: &mut TestAppContext) {
991 let language = language("go", tree_sitter_go::LANGUAGE.into());
992
993 let example_test = r#"
994 package main
995
996 import "fmt"
997
998 func Example() {
999 fmt.Println("Hello, world!")
1000 // Output: Hello, world!
1001 }
1002 "#;
1003
1004 let buffer =
1005 cx.new(|cx| crate::Buffer::local(example_test, cx).with_language(language.clone(), cx));
1006 cx.executor().run_until_parked();
1007
1008 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1009 let snapshot = buffer.snapshot();
1010 snapshot.runnable_ranges(0..example_test.len()).collect()
1011 });
1012
1013 let tag_strings: Vec<String> = runnables
1014 .iter()
1015 .flat_map(|r| &r.runnable.tags)
1016 .map(|tag| tag.0.to_string())
1017 .collect();
1018
1019 assert!(
1020 tag_strings.contains(&"go-example".to_string()),
1021 "Should find go-example tag, found: {:?}",
1022 tag_strings
1023 );
1024 }
1025
1026 #[gpui::test]
1027 fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
1028 let language = language("go", tree_sitter_go::LANGUAGE.into());
1029
1030 let table_test = r#"
1031 package main
1032
1033 import "testing"
1034
1035 func TestExample(t *testing.T) {
1036 _ = "some random string"
1037
1038 testCases := []struct{
1039 name string
1040 anotherStr string
1041 }{
1042 {
1043 name: "test case 1",
1044 anotherStr: "foo",
1045 },
1046 {
1047 name: "test case 2",
1048 anotherStr: "bar",
1049 },
1050 {
1051 name: "test case 3",
1052 anotherStr: "baz",
1053 },
1054 }
1055
1056 notATableTest := []struct{
1057 name string
1058 }{
1059 {
1060 name: "some string",
1061 },
1062 {
1063 name: "some other string",
1064 },
1065 }
1066
1067 for _, tc := range testCases {
1068 t.Run(tc.name, func(t *testing.T) {
1069 // test code here
1070 })
1071 }
1072 }
1073 "#;
1074
1075 let buffer =
1076 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1077 cx.executor().run_until_parked();
1078
1079 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1080 let snapshot = buffer.snapshot();
1081 snapshot.runnable_ranges(0..table_test.len()).collect()
1082 });
1083
1084 let tag_strings: Vec<String> = runnables
1085 .iter()
1086 .flat_map(|r| &r.runnable.tags)
1087 .map(|tag| tag.0.to_string())
1088 .collect();
1089
1090 assert!(
1091 tag_strings.contains(&"go-test".to_string()),
1092 "Should find go-test tag, found: {:?}",
1093 tag_strings
1094 );
1095 assert!(
1096 tag_strings.contains(&"go-table-test-case".to_string()),
1097 "Should find go-table-test-case tag, found: {:?}",
1098 tag_strings
1099 );
1100
1101 let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1102 // This is currently broken; see #39148
1103 // let go_table_test_count = tag_strings
1104 // .iter()
1105 // .filter(|&tag| tag == "go-table-test-case")
1106 // .count();
1107
1108 assert!(
1109 go_test_count == 1,
1110 "Should find exactly 1 go-test, found: {}",
1111 go_test_count
1112 );
1113 // assert!(
1114 // go_table_test_count == 3,
1115 // "Should find exactly 3 go-table-test-case, found: {}",
1116 // go_table_test_count
1117 // );
1118 }
1119
1120 #[gpui::test]
1121 fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
1122 let language = language("go", tree_sitter_go::LANGUAGE.into());
1123
1124 let table_test = r#"
1125 package main
1126
1127 func Example() {
1128 _ = "some random string"
1129
1130 notATableTest := []struct{
1131 name string
1132 }{
1133 {
1134 name: "some string",
1135 },
1136 {
1137 name: "some other string",
1138 },
1139 }
1140 }
1141 "#;
1142
1143 let buffer =
1144 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1145 cx.executor().run_until_parked();
1146
1147 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1148 let snapshot = buffer.snapshot();
1149 snapshot.runnable_ranges(0..table_test.len()).collect()
1150 });
1151
1152 let tag_strings: Vec<String> = runnables
1153 .iter()
1154 .flat_map(|r| &r.runnable.tags)
1155 .map(|tag| tag.0.to_string())
1156 .collect();
1157
1158 assert!(
1159 !tag_strings.contains(&"go-test".to_string()),
1160 "Should find go-test tag, found: {:?}",
1161 tag_strings
1162 );
1163 assert!(
1164 !tag_strings.contains(&"go-table-test-case".to_string()),
1165 "Should find go-table-test-case tag, found: {:?}",
1166 tag_strings
1167 );
1168 }
1169
1170 #[gpui::test]
1171 fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
1172 let language = language("go", tree_sitter_go::LANGUAGE.into());
1173
1174 let table_test = r#"
1175 package main
1176
1177 import "testing"
1178
1179 func TestExample(t *testing.T) {
1180 _ = "some random string"
1181
1182 testCases := map[string]struct {
1183 someStr string
1184 fail bool
1185 }{
1186 "test failure": {
1187 someStr: "foo",
1188 fail: true,
1189 },
1190 "test success": {
1191 someStr: "bar",
1192 fail: false,
1193 },
1194 }
1195
1196 notATableTest := map[string]struct {
1197 someStr string
1198 }{
1199 "some string": {
1200 someStr: "foo",
1201 },
1202 "some other string": {
1203 someStr: "bar",
1204 },
1205 }
1206
1207 for name, tc := range testCases {
1208 t.Run(name, func(t *testing.T) {
1209 // test code here
1210 })
1211 }
1212 }
1213 "#;
1214
1215 let buffer =
1216 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1217 cx.executor().run_until_parked();
1218
1219 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1220 let snapshot = buffer.snapshot();
1221 snapshot.runnable_ranges(0..table_test.len()).collect()
1222 });
1223
1224 let tag_strings: Vec<String> = runnables
1225 .iter()
1226 .flat_map(|r| &r.runnable.tags)
1227 .map(|tag| tag.0.to_string())
1228 .collect();
1229
1230 assert!(
1231 tag_strings.contains(&"go-test".to_string()),
1232 "Should find go-test tag, found: {:?}",
1233 tag_strings
1234 );
1235 assert!(
1236 tag_strings.contains(&"go-table-test-case".to_string()),
1237 "Should find go-table-test-case tag, found: {:?}",
1238 tag_strings
1239 );
1240
1241 let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
1242 let go_table_test_count = tag_strings
1243 .iter()
1244 .filter(|&tag| tag == "go-table-test-case")
1245 .count();
1246
1247 assert!(
1248 go_test_count == 1,
1249 "Should find exactly 1 go-test, found: {}",
1250 go_test_count
1251 );
1252 assert!(
1253 go_table_test_count == 2,
1254 "Should find exactly 2 go-table-test-case, found: {}",
1255 go_table_test_count
1256 );
1257 }
1258
1259 #[gpui::test]
1260 fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
1261 let language = language("go", tree_sitter_go::LANGUAGE.into());
1262
1263 let table_test = r#"
1264 package main
1265
1266 func Example() {
1267 _ = "some random string"
1268
1269 notATableTest := map[string]struct {
1270 someStr string
1271 }{
1272 "some string": {
1273 someStr: "foo",
1274 },
1275 "some other string": {
1276 someStr: "bar",
1277 },
1278 }
1279 }
1280 "#;
1281
1282 let buffer =
1283 cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
1284 cx.executor().run_until_parked();
1285
1286 let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
1287 let snapshot = buffer.snapshot();
1288 snapshot.runnable_ranges(0..table_test.len()).collect()
1289 });
1290
1291 let tag_strings: Vec<String> = runnables
1292 .iter()
1293 .flat_map(|r| &r.runnable.tags)
1294 .map(|tag| tag.0.to_string())
1295 .collect();
1296
1297 assert!(
1298 !tag_strings.contains(&"go-test".to_string()),
1299 "Should find go-test tag, found: {:?}",
1300 tag_strings
1301 );
1302 assert!(
1303 !tag_strings.contains(&"go-table-test-case".to_string()),
1304 "Should find go-table-test-case tag, found: {:?}",
1305 tag_strings
1306 );
1307 }
1308
1309 #[test]
1310 fn test_extract_subtest_name() {
1311 // Interpreted string literal
1312 let input_double_quoted = r#""subtest with double quotes""#;
1313 let result = extract_subtest_name(input_double_quoted);
1314 assert_eq!(result, Some(r#"subtest_with_double_quotes"#.to_string()));
1315
1316 let input_double_quoted_with_backticks = r#""test with `backticks` inside""#;
1317 let result = extract_subtest_name(input_double_quoted_with_backticks);
1318 assert_eq!(result, Some(r#"test_with_`backticks`_inside"#.to_string()));
1319
1320 // Raw string literal
1321 let input_with_backticks = r#"`subtest with backticks`"#;
1322 let result = extract_subtest_name(input_with_backticks);
1323 assert_eq!(result, Some(r#"subtest_with_backticks"#.to_string()));
1324
1325 let input_raw_with_quotes = r#"`test with "quotes" and other chars`"#;
1326 let result = extract_subtest_name(input_raw_with_quotes);
1327 assert_eq!(
1328 result,
1329 Some(r#"test_with_\"quotes\"_and_other_chars"#.to_string())
1330 );
1331
1332 let input_multiline = r#"`subtest with
1333 multiline
1334 backticks`"#;
1335 let result = extract_subtest_name(input_multiline);
1336 assert_eq!(
1337 result,
1338 Some(r#"subtest_with_________multiline_________backticks"#.to_string())
1339 );
1340
1341 let input_with_double_quotes = r#"`test with "double quotes"`"#;
1342 let result = extract_subtest_name(input_with_double_quotes);
1343 assert_eq!(result, Some(r#"test_with_\"double_quotes\""#.to_string()));
1344 }
1345}