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: &'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            language_server_settings(delegate, Self::SERVER_NAME, cx).and_then(|s| s.binary.clone())
 75        });
 76
 77        match configured_binary {
 78            Ok(Some(BinarySettings {
 79                path: Some(path),
 80                arguments,
 81                ..
 82            })) => Some(LanguageServerBinary {
 83                path: path.into(),
 84                arguments: arguments
 85                    .unwrap_or_default()
 86                    .iter()
 87                    .map(|arg| arg.into())
 88                    .collect(),
 89                env: None,
 90            }),
 91            Ok(Some(BinarySettings {
 92                path_lookup: Some(false),
 93                ..
 94            })) => None,
 95            _ => {
 96                let env = delegate.shell_env().await;
 97                let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
 98                Some(LanguageServerBinary {
 99                    path,
100                    arguments: server_binary_arguments(),
101                    env: Some(env),
102                })
103            }
104        }
105    }
106
107    fn will_fetch_server(
108        &self,
109        delegate: &Arc<dyn LspAdapterDelegate>,
110        cx: &mut AsyncAppContext,
111    ) -> Option<Task<Result<()>>> {
112        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
113
114        const NOTIFICATION_MESSAGE: &str =
115            "Could not install the Go language server `gopls`, because `go` was not found.";
116
117        let delegate = delegate.clone();
118        Some(cx.spawn(|cx| async move {
119            let install_output = process::Command::new("go").args(["version"]).output().await;
120            if install_output.is_err() {
121                if DID_SHOW_NOTIFICATION
122                    .compare_exchange(false, true, SeqCst, SeqCst)
123                    .is_ok()
124                {
125                    cx.update(|cx| {
126                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
127                    })?
128                }
129                return Err(anyhow!("cannot install gopls"));
130            }
131            Ok(())
132        }))
133    }
134
135    async fn fetch_server_binary(
136        &self,
137        version: Box<dyn 'static + Send + Any>,
138        container_dir: PathBuf,
139        delegate: &dyn LspAdapterDelegate,
140    ) -> Result<LanguageServerBinary> {
141        let version = version.downcast::<Option<String>>().unwrap();
142        let this = *self;
143
144        if let Some(version) = *version {
145            let binary_path = container_dir.join(format!("gopls_{version}"));
146            if let Ok(metadata) = fs::metadata(&binary_path).await {
147                if metadata.is_file() {
148                    remove_matching(&container_dir, |entry| {
149                        entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
150                    })
151                    .await;
152
153                    return Ok(LanguageServerBinary {
154                        path: binary_path.to_path_buf(),
155                        arguments: server_binary_arguments(),
156                        env: None,
157                    });
158                }
159            }
160        } else if let Some(path) = this
161            .cached_server_binary(container_dir.clone(), delegate)
162            .await
163        {
164            return Ok(path);
165        }
166
167        let gobin_dir = container_dir.join("gobin");
168        fs::create_dir_all(&gobin_dir).await?;
169        let install_output = process::Command::new("go")
170            .env("GO111MODULE", "on")
171            .env("GOBIN", &gobin_dir)
172            .args(["install", "golang.org/x/tools/gopls@latest"])
173            .output()
174            .await?;
175
176        if !install_output.status.success() {
177            log::error!(
178                "failed to install gopls via `go install`. stdout: {:?}, stderr: {:?}",
179                String::from_utf8_lossy(&install_output.stdout),
180                String::from_utf8_lossy(&install_output.stderr)
181            );
182
183            return Err(anyhow!("failed to install gopls with `go install`. Is `go` installed and in the PATH? Check logs for more information."));
184        }
185
186        let installed_binary_path = gobin_dir.join("gopls");
187        let version_output = process::Command::new(&installed_binary_path)
188            .arg("version")
189            .output()
190            .await
191            .context("failed to run installed gopls binary")?;
192        let version_stdout = str::from_utf8(&version_output.stdout)
193            .context("gopls version produced invalid utf8 output")?;
194        let version = GOPLS_VERSION_REGEX
195            .find(version_stdout)
196            .with_context(|| format!("failed to parse golps version output '{version_stdout}'"))?
197            .as_str();
198        let binary_path = container_dir.join(format!("gopls_{version}"));
199        fs::rename(&installed_binary_path, &binary_path).await?;
200
201        Ok(LanguageServerBinary {
202            path: binary_path.to_path_buf(),
203            arguments: server_binary_arguments(),
204            env: None,
205        })
206    }
207
208    async fn cached_server_binary(
209        &self,
210        container_dir: PathBuf,
211        _: &dyn LspAdapterDelegate,
212    ) -> Option<LanguageServerBinary> {
213        get_cached_server_binary(container_dir).await
214    }
215
216    async fn installation_test_binary(
217        &self,
218        container_dir: PathBuf,
219    ) -> Option<LanguageServerBinary> {
220        get_cached_server_binary(container_dir)
221            .await
222            .map(|mut binary| {
223                binary.arguments = vec!["--help".into()];
224                binary
225            })
226    }
227
228    async fn initialization_options(
229        self: Arc<Self>,
230        _: &Arc<dyn LspAdapterDelegate>,
231    ) -> Result<Option<serde_json::Value>> {
232        Ok(Some(json!({
233            "usePlaceholders": true,
234            "hints": {
235                "assignVariableTypes": true,
236                "compositeLiteralFields": true,
237                "compositeLiteralTypes": true,
238                "constantValues": true,
239                "functionTypeParameters": true,
240                "parameterNames": true,
241                "rangeVariableTypes": true
242            }
243        })))
244    }
245
246    async fn label_for_completion(
247        &self,
248        completion: &lsp::CompletionItem,
249        language: &Arc<Language>,
250    ) -> Option<CodeLabel> {
251        let label = &completion.label;
252
253        // Gopls returns nested fields and methods as completions.
254        // To syntax highlight these, combine their final component
255        // with their detail.
256        let name_offset = label.rfind('.').unwrap_or(0);
257
258        match completion.kind.zip(completion.detail.as_ref()) {
259            Some((lsp::CompletionItemKind::MODULE, detail)) => {
260                let text = format!("{label} {detail}");
261                let source = Rope::from(format!("import {text}").as_str());
262                let runs = language.highlight_text(&source, 7..7 + text.len());
263                return Some(CodeLabel {
264                    text,
265                    runs,
266                    filter_range: 0..label.len(),
267                });
268            }
269            Some((
270                lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
271                detail,
272            )) => {
273                let text = format!("{label} {detail}");
274                let source =
275                    Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
276                let runs = adjust_runs(
277                    name_offset,
278                    language.highlight_text(&source, 4..4 + text.len()),
279                );
280                return Some(CodeLabel {
281                    text,
282                    runs,
283                    filter_range: 0..label.len(),
284                });
285            }
286            Some((lsp::CompletionItemKind::STRUCT, _)) => {
287                let text = format!("{label} struct {{}}");
288                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
289                let runs = adjust_runs(
290                    name_offset,
291                    language.highlight_text(&source, 5..5 + text.len()),
292                );
293                return Some(CodeLabel {
294                    text,
295                    runs,
296                    filter_range: 0..label.len(),
297                });
298            }
299            Some((lsp::CompletionItemKind::INTERFACE, _)) => {
300                let text = format!("{label} interface {{}}");
301                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
302                let runs = adjust_runs(
303                    name_offset,
304                    language.highlight_text(&source, 5..5 + text.len()),
305                );
306                return Some(CodeLabel {
307                    text,
308                    runs,
309                    filter_range: 0..label.len(),
310                });
311            }
312            Some((lsp::CompletionItemKind::FIELD, detail)) => {
313                let text = format!("{label} {detail}");
314                let source =
315                    Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
316                let runs = adjust_runs(
317                    name_offset,
318                    language.highlight_text(&source, 16..16 + text.len()),
319                );
320                return Some(CodeLabel {
321                    text,
322                    runs,
323                    filter_range: 0..label.len(),
324                });
325            }
326            Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
327                if let Some(signature) = detail.strip_prefix("func") {
328                    let text = format!("{label}{signature}");
329                    let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
330                    let runs = adjust_runs(
331                        name_offset,
332                        language.highlight_text(&source, 5..5 + text.len()),
333                    );
334                    return Some(CodeLabel {
335                        filter_range: 0..label.len(),
336                        text,
337                        runs,
338                    });
339                }
340            }
341            _ => {}
342        }
343        None
344    }
345
346    async fn label_for_symbol(
347        &self,
348        name: &str,
349        kind: lsp::SymbolKind,
350        language: &Arc<Language>,
351    ) -> Option<CodeLabel> {
352        let (text, filter_range, display_range) = match kind {
353            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
354                let text = format!("func {} () {{}}", name);
355                let filter_range = 5..5 + name.len();
356                let display_range = 0..filter_range.end;
357                (text, filter_range, display_range)
358            }
359            lsp::SymbolKind::STRUCT => {
360                let text = format!("type {} struct {{}}", name);
361                let filter_range = 5..5 + name.len();
362                let display_range = 0..text.len();
363                (text, filter_range, display_range)
364            }
365            lsp::SymbolKind::INTERFACE => {
366                let text = format!("type {} interface {{}}", name);
367                let filter_range = 5..5 + name.len();
368                let display_range = 0..text.len();
369                (text, filter_range, display_range)
370            }
371            lsp::SymbolKind::CLASS => {
372                let text = format!("type {} T", name);
373                let filter_range = 5..5 + name.len();
374                let display_range = 0..filter_range.end;
375                (text, filter_range, display_range)
376            }
377            lsp::SymbolKind::CONSTANT => {
378                let text = format!("const {} = nil", name);
379                let filter_range = 6..6 + name.len();
380                let display_range = 0..filter_range.end;
381                (text, filter_range, display_range)
382            }
383            lsp::SymbolKind::VARIABLE => {
384                let text = format!("var {} = nil", name);
385                let filter_range = 4..4 + name.len();
386                let display_range = 0..filter_range.end;
387                (text, filter_range, display_range)
388            }
389            lsp::SymbolKind::MODULE => {
390                let text = format!("package {}", name);
391                let filter_range = 8..8 + name.len();
392                let display_range = 0..filter_range.end;
393                (text, filter_range, display_range)
394            }
395            _ => return None,
396        };
397
398        Some(CodeLabel {
399            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
400            text: text[display_range].to_string(),
401            filter_range,
402        })
403    }
404}
405
406async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
407    maybe!(async {
408        let mut last_binary_path = None;
409        let mut entries = fs::read_dir(&container_dir).await?;
410        while let Some(entry) = entries.next().await {
411            let entry = entry?;
412            if entry.file_type().await?.is_file()
413                && entry
414                    .file_name()
415                    .to_str()
416                    .map_or(false, |name| name.starts_with("gopls_"))
417            {
418                last_binary_path = Some(entry.path());
419            }
420        }
421
422        if let Some(path) = last_binary_path {
423            Ok(LanguageServerBinary {
424                path,
425                arguments: server_binary_arguments(),
426                env: None,
427            })
428        } else {
429            Err(anyhow!("no cached binary"))
430        }
431    })
432    .await
433    .log_err()
434}
435
436fn adjust_runs(
437    delta: usize,
438    mut runs: Vec<(Range<usize>, HighlightId)>,
439) -> Vec<(Range<usize>, HighlightId)> {
440    for (range, _) in &mut runs {
441        range.start += delta;
442        range.end += delta;
443    }
444    runs
445}
446
447pub(crate) struct GoContextProvider;
448
449const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
450const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
451    VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
452
453impl ContextProvider for GoContextProvider {
454    fn build_context(
455        &self,
456        variables: &TaskVariables,
457        location: &Location,
458        _: Option<&HashMap<String, String>>,
459        cx: &mut gpui::AppContext,
460    ) -> Result<TaskVariables> {
461        let local_abs_path = location
462            .buffer
463            .read(cx)
464            .file()
465            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
466
467        let go_package_variable = local_abs_path
468            .as_deref()
469            .and_then(|local_abs_path| local_abs_path.parent())
470            .map(|buffer_dir| {
471                // Prefer the relative form `./my-nested-package/is-here` over
472                // absolute path, because it's more readable in the modal, but
473                // the absolute path also works.
474                let package_name = variables
475                    .get(&VariableName::WorktreeRoot)
476                    .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())
477                    .map(|relative_pkg_dir| {
478                        if relative_pkg_dir.as_os_str().is_empty() {
479                            ".".into()
480                        } else {
481                            format!("./{}", relative_pkg_dir.to_string_lossy())
482                        }
483                    })
484                    .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
485
486                (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string())
487            });
488
489        let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
490
491        let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
492            .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
493
494        Ok(TaskVariables::from_iter(
495            [go_package_variable, go_subtest_variable]
496                .into_iter()
497                .flatten(),
498        ))
499    }
500
501    fn associated_tasks(
502        &self,
503        _: Option<Arc<dyn language::File>>,
504        _: &AppContext,
505    ) -> Option<TaskTemplates> {
506        let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
507            None
508        } else {
509            Some("$ZED_DIRNAME".to_string())
510        };
511
512        Some(TaskTemplates(vec![
513            TaskTemplate {
514                label: format!(
515                    "go test {} -run {}",
516                    GO_PACKAGE_TASK_VARIABLE.template_value(),
517                    VariableName::Symbol.template_value(),
518                ),
519                command: "go".into(),
520                args: vec![
521                    "test".into(),
522                    GO_PACKAGE_TASK_VARIABLE.template_value(),
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(), GO_PACKAGE_TASK_VARIABLE.template_value()],
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                    GO_PACKAGE_TASK_VARIABLE.template_value(),
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.into());
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}