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