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: "go".into(),
522 args: vec![
523 "test".into(),
524 GO_PACKAGE_TASK_VARIABLE.template_value(),
525 "-run".into(),
526 format!("^{}\\$", VariableName::Symbol.template_value(),),
527 ],
528 tags: vec!["go-test".to_owned()],
529 cwd: package_cwd.clone(),
530 ..TaskTemplate::default()
531 },
532 TaskTemplate {
533 label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
534 command: "go".into(),
535 args: vec!["test".into(), GO_PACKAGE_TASK_VARIABLE.template_value()],
536 cwd: package_cwd.clone(),
537 ..TaskTemplate::default()
538 },
539 TaskTemplate {
540 label: "go test ./...".into(),
541 command: "go".into(),
542 args: vec!["test".into(), "./...".into()],
543 cwd: package_cwd.clone(),
544 ..TaskTemplate::default()
545 },
546 TaskTemplate {
547 label: format!(
548 "go test {} -v -run {}/{}",
549 GO_PACKAGE_TASK_VARIABLE.template_value(),
550 VariableName::Symbol.template_value(),
551 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
552 ),
553 command: "go".into(),
554 args: vec![
555 "test".into(),
556 "-v".into(),
557 "-run".into(),
558 format!(
559 "^{}\\$/^{}\\$",
560 VariableName::Symbol.template_value(),
561 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
562 ),
563 ],
564 cwd: package_cwd.clone(),
565 tags: vec!["go-subtest".to_owned()],
566 ..TaskTemplate::default()
567 },
568 TaskTemplate {
569 label: format!(
570 "go test {} -bench {}",
571 GO_PACKAGE_TASK_VARIABLE.template_value(),
572 VariableName::Symbol.template_value()
573 ),
574 command: "go".into(),
575 args: vec![
576 "test".into(),
577 GO_PACKAGE_TASK_VARIABLE.template_value(),
578 "-benchmem".into(),
579 "-run=^$".into(),
580 "-bench".into(),
581 format!("^{}\\$", VariableName::Symbol.template_value()),
582 ],
583 cwd: package_cwd.clone(),
584 tags: vec!["go-benchmark".to_owned()],
585 ..TaskTemplate::default()
586 },
587 TaskTemplate {
588 label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
589 command: "go".into(),
590 args: vec!["run".into(), ".".into()],
591 cwd: package_cwd.clone(),
592 tags: vec!["go-main".to_owned()],
593 ..TaskTemplate::default()
594 },
595 ]))
596 }
597}
598
599fn extract_subtest_name(input: &str) -> Option<String> {
600 let replaced_spaces = input.trim_matches('"').replace(' ', "_");
601
602 Some(
603 GO_ESCAPE_SUBTEST_NAME_REGEX
604 .replace_all(&replaced_spaces, |caps: ®ex::Captures| {
605 format!("\\{}", &caps[0])
606 })
607 .to_string(),
608 )
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::language;
615 use gpui::Hsla;
616 use theme::SyntaxTheme;
617
618 #[gpui::test]
619 async fn test_go_label_for_completion() {
620 let adapter = Arc::new(GoLspAdapter);
621 let language = language("go", tree_sitter_go::language());
622
623 let theme = SyntaxTheme::new_test([
624 ("type", Hsla::default()),
625 ("keyword", Hsla::default()),
626 ("function", Hsla::default()),
627 ("number", Hsla::default()),
628 ("property", Hsla::default()),
629 ]);
630 language.set_theme(&theme);
631
632 let grammar = language.grammar().unwrap();
633 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
634 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
635 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
636 let highlight_number = grammar.highlight_id_for_name("number").unwrap();
637
638 assert_eq!(
639 adapter
640 .label_for_completion(
641 &lsp::CompletionItem {
642 kind: Some(lsp::CompletionItemKind::FUNCTION),
643 label: "Hello".to_string(),
644 detail: Some("func(a B) c.D".to_string()),
645 ..Default::default()
646 },
647 &language
648 )
649 .await,
650 Some(CodeLabel {
651 text: "Hello(a B) c.D".to_string(),
652 filter_range: 0..5,
653 runs: vec![
654 (0..5, highlight_function),
655 (8..9, highlight_type),
656 (13..14, highlight_type),
657 ],
658 })
659 );
660
661 // Nested methods
662 assert_eq!(
663 adapter
664 .label_for_completion(
665 &lsp::CompletionItem {
666 kind: Some(lsp::CompletionItemKind::METHOD),
667 label: "one.two.Three".to_string(),
668 detail: Some("func() [3]interface{}".to_string()),
669 ..Default::default()
670 },
671 &language
672 )
673 .await,
674 Some(CodeLabel {
675 text: "one.two.Three() [3]interface{}".to_string(),
676 filter_range: 0..13,
677 runs: vec![
678 (8..13, highlight_function),
679 (17..18, highlight_number),
680 (19..28, highlight_keyword),
681 ],
682 })
683 );
684
685 // Nested fields
686 assert_eq!(
687 adapter
688 .label_for_completion(
689 &lsp::CompletionItem {
690 kind: Some(lsp::CompletionItemKind::FIELD),
691 label: "two.Three".to_string(),
692 detail: Some("a.Bcd".to_string()),
693 ..Default::default()
694 },
695 &language
696 )
697 .await,
698 Some(CodeLabel {
699 text: "two.Three a.Bcd".to_string(),
700 filter_range: 0..9,
701 runs: vec![(12..15, highlight_type)],
702 })
703 );
704 }
705}