1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui::{AppContext, AsyncAppContext, Task};
5use http_client::github::latest_github_release;
6pub use language::*;
7use lsp::LanguageServerBinary;
8use project::project_settings::{BinarySettings, ProjectSettings};
9use regex::Regex;
10use serde_json::json;
11use settings::Settings;
12use smol::{fs, process};
13use std::{
14 any::Any,
15 borrow::Cow,
16 ffi::{OsStr, OsString},
17 ops::Range,
18 path::PathBuf,
19 str,
20 sync::{
21 atomic::{AtomicBool, Ordering::SeqCst},
22 Arc, LazyLock,
23 },
24};
25use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
26use util::{fs::remove_matching, maybe, ResultExt};
27
28fn server_binary_arguments() -> Vec<OsString> {
29 vec!["-mode=stdio".into()]
30}
31
32#[derive(Copy, Clone)]
33pub struct GoLspAdapter;
34
35impl GoLspAdapter {
36 const SERVER_NAME: &'static str = "gopls";
37}
38
39static GOPLS_VERSION_REGEX: LazyLock<Regex> =
40 LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create GOPLS_VERSION_REGEX"));
41
42static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
43 Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")
44});
45
46#[async_trait(?Send)]
47impl super::LspAdapter for GoLspAdapter {
48 fn name(&self) -> LanguageServerName {
49 LanguageServerName(Self::SERVER_NAME.into())
50 }
51
52 async fn fetch_latest_server_version(
53 &self,
54 delegate: &dyn LspAdapterDelegate,
55 ) -> Result<Box<dyn 'static + Send + Any>> {
56 let release =
57 latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
58 let version: Option<String> = release.tag_name.strip_prefix("gopls/v").map(str::to_string);
59 if version.is_none() {
60 log::warn!(
61 "couldn't infer gopls version from GitHub release tag name '{}'",
62 release.tag_name
63 );
64 }
65 Ok(Box::new(version) as Box<_>)
66 }
67
68 async fn check_if_user_installed(
69 &self,
70 delegate: &dyn LspAdapterDelegate,
71 cx: &AsyncAppContext,
72 ) -> Option<LanguageServerBinary> {
73 let configured_binary = cx.update(|cx| {
74 ProjectSettings::get_global(cx)
75 .lsp
76 .get(Self::SERVER_NAME)
77 .and_then(|s| s.binary.clone())
78 });
79
80 match configured_binary {
81 Ok(Some(BinarySettings {
82 path: Some(path),
83 arguments,
84 ..
85 })) => Some(LanguageServerBinary {
86 path: path.into(),
87 arguments: arguments
88 .unwrap_or_default()
89 .iter()
90 .map(|arg| arg.into())
91 .collect(),
92 env: None,
93 }),
94 Ok(Some(BinarySettings {
95 path_lookup: Some(false),
96 ..
97 })) => None,
98 _ => {
99 let env = delegate.shell_env().await;
100 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
101 Some(LanguageServerBinary {
102 path,
103 arguments: server_binary_arguments(),
104 env: Some(env),
105 })
106 }
107 }
108 }
109
110 fn will_fetch_server(
111 &self,
112 delegate: &Arc<dyn LspAdapterDelegate>,
113 cx: &mut AsyncAppContext,
114 ) -> Option<Task<Result<()>>> {
115 static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
116
117 const NOTIFICATION_MESSAGE: &str =
118 "Could not install the Go language server `gopls`, because `go` was not found.";
119
120 let delegate = delegate.clone();
121 Some(cx.spawn(|cx| async move {
122 let install_output = process::Command::new("go").args(["version"]).output().await;
123 if install_output.is_err() {
124 if DID_SHOW_NOTIFICATION
125 .compare_exchange(false, true, SeqCst, SeqCst)
126 .is_ok()
127 {
128 cx.update(|cx| {
129 delegate.show_notification(NOTIFICATION_MESSAGE, cx);
130 })?
131 }
132 return Err(anyhow!("cannot install gopls"));
133 }
134 Ok(())
135 }))
136 }
137
138 async fn fetch_server_binary(
139 &self,
140 version: Box<dyn 'static + Send + Any>,
141 container_dir: PathBuf,
142 delegate: &dyn LspAdapterDelegate,
143 ) -> Result<LanguageServerBinary> {
144 let version = version.downcast::<Option<String>>().unwrap();
145 let this = *self;
146
147 if let Some(version) = *version {
148 let binary_path = container_dir.join(&format!("gopls_{version}"));
149 if let Ok(metadata) = fs::metadata(&binary_path).await {
150 if metadata.is_file() {
151 remove_matching(&container_dir, |entry| {
152 entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
153 })
154 .await;
155
156 return Ok(LanguageServerBinary {
157 path: binary_path.to_path_buf(),
158 arguments: server_binary_arguments(),
159 env: None,
160 });
161 }
162 }
163 } else if let Some(path) = this
164 .cached_server_binary(container_dir.clone(), delegate)
165 .await
166 {
167 return Ok(path);
168 }
169
170 let gobin_dir = container_dir.join("gobin");
171 fs::create_dir_all(&gobin_dir).await?;
172 let install_output = process::Command::new("go")
173 .env("GO111MODULE", "on")
174 .env("GOBIN", &gobin_dir)
175 .args(["install", "golang.org/x/tools/gopls@latest"])
176 .output()
177 .await?;
178
179 if !install_output.status.success() {
180 log::error!(
181 "failed to install gopls via `go install`. stdout: {:?}, stderr: {:?}",
182 String::from_utf8_lossy(&install_output.stdout),
183 String::from_utf8_lossy(&install_output.stderr)
184 );
185
186 return Err(anyhow!("failed to install gopls with `go install`. Is `go` installed and in the PATH? Check logs for more information."));
187 }
188
189 let installed_binary_path = gobin_dir.join("gopls");
190 let version_output = process::Command::new(&installed_binary_path)
191 .arg("version")
192 .output()
193 .await
194 .context("failed to run installed gopls binary")?;
195 let version_stdout = str::from_utf8(&version_output.stdout)
196 .context("gopls version produced invalid utf8 output")?;
197 let version = GOPLS_VERSION_REGEX
198 .find(version_stdout)
199 .with_context(|| format!("failed to parse golps version output '{version_stdout}'"))?
200 .as_str();
201 let binary_path = container_dir.join(&format!("gopls_{version}"));
202 fs::rename(&installed_binary_path, &binary_path).await?;
203
204 Ok(LanguageServerBinary {
205 path: binary_path.to_path_buf(),
206 arguments: server_binary_arguments(),
207 env: None,
208 })
209 }
210
211 async fn cached_server_binary(
212 &self,
213 container_dir: PathBuf,
214 _: &dyn LspAdapterDelegate,
215 ) -> Option<LanguageServerBinary> {
216 get_cached_server_binary(container_dir).await
217 }
218
219 async fn installation_test_binary(
220 &self,
221 container_dir: PathBuf,
222 ) -> Option<LanguageServerBinary> {
223 get_cached_server_binary(container_dir)
224 .await
225 .map(|mut binary| {
226 binary.arguments = vec!["--help".into()];
227 binary
228 })
229 }
230
231 async fn initialization_options(
232 self: Arc<Self>,
233 _: &Arc<dyn LspAdapterDelegate>,
234 ) -> Result<Option<serde_json::Value>> {
235 Ok(Some(json!({
236 "usePlaceholders": true,
237 "hints": {
238 "assignVariableTypes": true,
239 "compositeLiteralFields": true,
240 "compositeLiteralTypes": true,
241 "constantValues": true,
242 "functionTypeParameters": true,
243 "parameterNames": true,
244 "rangeVariableTypes": true
245 }
246 })))
247 }
248
249 async fn label_for_completion(
250 &self,
251 completion: &lsp::CompletionItem,
252 language: &Arc<Language>,
253 ) -> Option<CodeLabel> {
254 let label = &completion.label;
255
256 // Gopls returns nested fields and methods as completions.
257 // To syntax highlight these, combine their final component
258 // with their detail.
259 let name_offset = label.rfind('.').unwrap_or(0);
260
261 match completion.kind.zip(completion.detail.as_ref()) {
262 Some((lsp::CompletionItemKind::MODULE, detail)) => {
263 let text = format!("{label} {detail}");
264 let source = Rope::from(format!("import {text}").as_str());
265 let runs = language.highlight_text(&source, 7..7 + text.len());
266 return Some(CodeLabel {
267 text,
268 runs,
269 filter_range: 0..label.len(),
270 });
271 }
272 Some((
273 lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
274 detail,
275 )) => {
276 let text = format!("{label} {detail}");
277 let source =
278 Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
279 let runs = adjust_runs(
280 name_offset,
281 language.highlight_text(&source, 4..4 + text.len()),
282 );
283 return Some(CodeLabel {
284 text,
285 runs,
286 filter_range: 0..label.len(),
287 });
288 }
289 Some((lsp::CompletionItemKind::STRUCT, _)) => {
290 let text = format!("{label} struct {{}}");
291 let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
292 let runs = adjust_runs(
293 name_offset,
294 language.highlight_text(&source, 5..5 + text.len()),
295 );
296 return Some(CodeLabel {
297 text,
298 runs,
299 filter_range: 0..label.len(),
300 });
301 }
302 Some((lsp::CompletionItemKind::INTERFACE, _)) => {
303 let text = format!("{label} interface {{}}");
304 let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
305 let runs = adjust_runs(
306 name_offset,
307 language.highlight_text(&source, 5..5 + text.len()),
308 );
309 return Some(CodeLabel {
310 text,
311 runs,
312 filter_range: 0..label.len(),
313 });
314 }
315 Some((lsp::CompletionItemKind::FIELD, detail)) => {
316 let text = format!("{label} {detail}");
317 let source =
318 Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
319 let runs = adjust_runs(
320 name_offset,
321 language.highlight_text(&source, 16..16 + text.len()),
322 );
323 return Some(CodeLabel {
324 text,
325 runs,
326 filter_range: 0..label.len(),
327 });
328 }
329 Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
330 if let Some(signature) = detail.strip_prefix("func") {
331 let text = format!("{label}{signature}");
332 let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
333 let runs = adjust_runs(
334 name_offset,
335 language.highlight_text(&source, 5..5 + text.len()),
336 );
337 return Some(CodeLabel {
338 filter_range: 0..label.len(),
339 text,
340 runs,
341 });
342 }
343 }
344 _ => {}
345 }
346 None
347 }
348
349 async fn label_for_symbol(
350 &self,
351 name: &str,
352 kind: lsp::SymbolKind,
353 language: &Arc<Language>,
354 ) -> Option<CodeLabel> {
355 let (text, filter_range, display_range) = match kind {
356 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
357 let text = format!("func {} () {{}}", name);
358 let filter_range = 5..5 + name.len();
359 let display_range = 0..filter_range.end;
360 (text, filter_range, display_range)
361 }
362 lsp::SymbolKind::STRUCT => {
363 let text = format!("type {} struct {{}}", name);
364 let filter_range = 5..5 + name.len();
365 let display_range = 0..text.len();
366 (text, filter_range, display_range)
367 }
368 lsp::SymbolKind::INTERFACE => {
369 let text = format!("type {} interface {{}}", name);
370 let filter_range = 5..5 + name.len();
371 let display_range = 0..text.len();
372 (text, filter_range, display_range)
373 }
374 lsp::SymbolKind::CLASS => {
375 let text = format!("type {} T", 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::CONSTANT => {
381 let text = format!("const {} = nil", name);
382 let filter_range = 6..6 + name.len();
383 let display_range = 0..filter_range.end;
384 (text, filter_range, display_range)
385 }
386 lsp::SymbolKind::VARIABLE => {
387 let text = format!("var {} = nil", name);
388 let filter_range = 4..4 + name.len();
389 let display_range = 0..filter_range.end;
390 (text, filter_range, display_range)
391 }
392 lsp::SymbolKind::MODULE => {
393 let text = format!("package {}", name);
394 let filter_range = 8..8 + name.len();
395 let display_range = 0..filter_range.end;
396 (text, filter_range, display_range)
397 }
398 _ => return None,
399 };
400
401 Some(CodeLabel {
402 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
403 text: text[display_range].to_string(),
404 filter_range,
405 })
406 }
407}
408
409async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
410 maybe!(async {
411 let mut last_binary_path = None;
412 let mut entries = fs::read_dir(&container_dir).await?;
413 while let Some(entry) = entries.next().await {
414 let entry = entry?;
415 if entry.file_type().await?.is_file()
416 && entry
417 .file_name()
418 .to_str()
419 .map_or(false, |name| name.starts_with("gopls_"))
420 {
421 last_binary_path = Some(entry.path());
422 }
423 }
424
425 if let Some(path) = last_binary_path {
426 Ok(LanguageServerBinary {
427 path,
428 arguments: server_binary_arguments(),
429 env: None,
430 })
431 } else {
432 Err(anyhow!("no cached binary"))
433 }
434 })
435 .await
436 .log_err()
437}
438
439fn adjust_runs(
440 delta: usize,
441 mut runs: Vec<(Range<usize>, HighlightId)>,
442) -> Vec<(Range<usize>, HighlightId)> {
443 for (range, _) in &mut runs {
444 range.start += delta;
445 range.end += delta;
446 }
447 runs
448}
449
450pub(crate) struct GoContextProvider;
451
452const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
453const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
454 VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
455
456impl ContextProvider for GoContextProvider {
457 fn build_context(
458 &self,
459 variables: &TaskVariables,
460 location: &Location,
461 cx: &mut gpui::AppContext,
462 ) -> Result<TaskVariables> {
463 let local_abs_path = location
464 .buffer
465 .read(cx)
466 .file()
467 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
468
469 let go_package_variable = local_abs_path
470 .as_deref()
471 .and_then(|local_abs_path| local_abs_path.parent())
472 .map(|buffer_dir| {
473 // Prefer the relative form `./my-nested-package/is-here` over
474 // absolute path, because it's more readable in the modal, but
475 // the absolute path also works.
476 let package_name = variables
477 .get(&VariableName::WorktreeRoot)
478 .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())
479 .map(|relative_pkg_dir| {
480 if relative_pkg_dir.as_os_str().is_empty() {
481 ".".into()
482 } else {
483 format!("./{}", relative_pkg_dir.to_string_lossy())
484 }
485 })
486 .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
487
488 (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string())
489 });
490
491 let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
492
493 let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
494 .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
495
496 Ok(TaskVariables::from_iter(
497 [go_package_variable, go_subtest_variable]
498 .into_iter()
499 .flatten(),
500 ))
501 }
502
503 fn associated_tasks(
504 &self,
505 _: Option<Arc<dyn language::File>>,
506 _: &AppContext,
507 ) -> Option<TaskTemplates> {
508 let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
509 None
510 } else {
511 Some("$ZED_DIRNAME".to_string())
512 };
513
514 Some(TaskTemplates(vec![
515 TaskTemplate {
516 label: format!(
517 "go test {} -run {}",
518 GO_PACKAGE_TASK_VARIABLE.template_value(),
519 VariableName::Symbol.template_value(),
520 ),
521 command: format!("go test -run {}", VariableName::Symbol.template_value(),),
522 tags: vec!["go-test".to_owned()],
523 cwd: package_cwd.clone(),
524 ..TaskTemplate::default()
525 },
526 TaskTemplate {
527 label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
528 command: "go".into(),
529 args: vec!["test".into(), GO_PACKAGE_TASK_VARIABLE.template_value()],
530 cwd: package_cwd.clone(),
531 ..TaskTemplate::default()
532 },
533 TaskTemplate {
534 label: "go test ./...".into(),
535 command: "go".into(),
536 args: vec!["test".into(), "./...".into()],
537 cwd: package_cwd.clone(),
538 ..TaskTemplate::default()
539 },
540 TaskTemplate {
541 label: format!(
542 "go test {} -v -run {}/{}",
543 GO_PACKAGE_TASK_VARIABLE.template_value(),
544 VariableName::Symbol.template_value(),
545 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
546 ),
547 command: "go".into(),
548 args: vec![
549 "test".into(),
550 "-v".into(),
551 "-run".into(),
552 format!(
553 "^{}\\$/^{}\\$",
554 VariableName::Symbol.template_value(),
555 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
556 ),
557 ],
558 cwd: package_cwd.clone(),
559 tags: vec!["go-subtest".to_owned()],
560 ..TaskTemplate::default()
561 },
562 TaskTemplate {
563 label: format!(
564 "go test {} -bench {}",
565 GO_PACKAGE_TASK_VARIABLE.template_value(),
566 VariableName::Symbol.template_value()
567 ),
568 command: "go".into(),
569 args: vec![
570 "test".into(),
571 GO_PACKAGE_TASK_VARIABLE.template_value(),
572 "-benchmem".into(),
573 "-run=^$".into(),
574 "-bench".into(),
575 format!("^{}\\$", VariableName::Symbol.template_value()),
576 ],
577 cwd: package_cwd.clone(),
578 tags: vec!["go-benchmark".to_owned()],
579 ..TaskTemplate::default()
580 },
581 TaskTemplate {
582 label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
583 command: "go".into(),
584 args: vec!["run".into(), ".".into()],
585 cwd: package_cwd.clone(),
586 tags: vec!["go-main".to_owned()],
587 ..TaskTemplate::default()
588 },
589 ]))
590 }
591}
592
593fn extract_subtest_name(input: &str) -> Option<String> {
594 let replaced_spaces = input.trim_matches('"').replace(' ', "_");
595
596 Some(
597 GO_ESCAPE_SUBTEST_NAME_REGEX
598 .replace_all(&replaced_spaces, |caps: ®ex::Captures| {
599 format!("\\{}", &caps[0])
600 })
601 .to_string(),
602 )
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use crate::language;
609 use gpui::Hsla;
610 use theme::SyntaxTheme;
611
612 #[gpui::test]
613 async fn test_go_label_for_completion() {
614 let adapter = Arc::new(GoLspAdapter);
615 let language = language("go", tree_sitter_go::language());
616
617 let theme = SyntaxTheme::new_test([
618 ("type", Hsla::default()),
619 ("keyword", Hsla::default()),
620 ("function", Hsla::default()),
621 ("number", Hsla::default()),
622 ("property", Hsla::default()),
623 ]);
624 language.set_theme(&theme);
625
626 let grammar = language.grammar().unwrap();
627 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
628 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
629 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
630 let highlight_number = grammar.highlight_id_for_name("number").unwrap();
631
632 assert_eq!(
633 adapter
634 .label_for_completion(
635 &lsp::CompletionItem {
636 kind: Some(lsp::CompletionItemKind::FUNCTION),
637 label: "Hello".to_string(),
638 detail: Some("func(a B) c.D".to_string()),
639 ..Default::default()
640 },
641 &language
642 )
643 .await,
644 Some(CodeLabel {
645 text: "Hello(a B) c.D".to_string(),
646 filter_range: 0..5,
647 runs: vec![
648 (0..5, highlight_function),
649 (8..9, highlight_type),
650 (13..14, highlight_type),
651 ],
652 })
653 );
654
655 // Nested methods
656 assert_eq!(
657 adapter
658 .label_for_completion(
659 &lsp::CompletionItem {
660 kind: Some(lsp::CompletionItemKind::METHOD),
661 label: "one.two.Three".to_string(),
662 detail: Some("func() [3]interface{}".to_string()),
663 ..Default::default()
664 },
665 &language
666 )
667 .await,
668 Some(CodeLabel {
669 text: "one.two.Three() [3]interface{}".to_string(),
670 filter_range: 0..13,
671 runs: vec![
672 (8..13, highlight_function),
673 (17..18, highlight_number),
674 (19..28, highlight_keyword),
675 ],
676 })
677 );
678
679 // Nested fields
680 assert_eq!(
681 adapter
682 .label_for_completion(
683 &lsp::CompletionItem {
684 kind: Some(lsp::CompletionItemKind::FIELD),
685 label: "two.Three".to_string(),
686 detail: Some("a.Bcd".to_string()),
687 ..Default::default()
688 },
689 &language
690 )
691 .await,
692 Some(CodeLabel {
693 text: "two.Three a.Bcd".to_string(),
694 filter_range: 0..9,
695 runs: vec![(12..15, highlight_type)],
696 })
697 );
698 }
699}