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