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