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::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: &regex::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}