go.rs

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