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