go.rs

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