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