go.rs

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