1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui::{AsyncAppContext, Task};
5pub use language::*;
6use lazy_static::lazy_static;
7use lsp::LanguageServerBinary;
8use regex::Regex;
9use smol::{fs, process};
10use std::{
11 any::Any,
12 ffi::{OsStr, OsString},
13 ops::Range,
14 path::PathBuf,
15 str,
16 sync::{
17 atomic::{AtomicBool, Ordering::SeqCst},
18 Arc,
19 },
20};
21use util::{fs::remove_matching, github::latest_github_release, ResultExt};
22
23fn server_binary_arguments() -> Vec<OsString> {
24 vec!["-mode=stdio".into()]
25}
26
27#[derive(Copy, Clone)]
28pub struct GoLspAdapter;
29
30lazy_static! {
31 static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap();
32}
33
34#[async_trait]
35impl super::LspAdapter for GoLspAdapter {
36 async fn name(&self) -> LanguageServerName {
37 LanguageServerName("gopls".into())
38 }
39
40 fn short_name(&self) -> &'static str {
41 "gopls"
42 }
43
44 async fn fetch_latest_server_version(
45 &self,
46 delegate: &dyn LspAdapterDelegate,
47 ) -> Result<Box<dyn 'static + Send + Any>> {
48 let release = latest_github_release("golang/tools", false, delegate.http_client()).await?;
49 let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
50 if version.is_none() {
51 log::warn!(
52 "couldn't infer gopls version from github release name '{}'",
53 release.name
54 );
55 }
56 Ok(Box::new(version) as Box<_>)
57 }
58
59 fn will_fetch_server(
60 &self,
61 delegate: &Arc<dyn LspAdapterDelegate>,
62 cx: &mut AsyncAppContext,
63 ) -> Option<Task<Result<()>>> {
64 static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
65
66 const NOTIFICATION_MESSAGE: &str =
67 "Could not install the Go language server `gopls`, because `go` was not found.";
68
69 let delegate = delegate.clone();
70 Some(cx.spawn(|cx| async move {
71 let install_output = process::Command::new("go").args(["version"]).output().await;
72 if install_output.is_err() {
73 if DID_SHOW_NOTIFICATION
74 .compare_exchange(false, true, SeqCst, SeqCst)
75 .is_ok()
76 {
77 cx.update(|cx| {
78 delegate.show_notification(NOTIFICATION_MESSAGE, cx);
79 })?
80 }
81 return Err(anyhow!("cannot install gopls"));
82 }
83 Ok(())
84 }))
85 }
86
87 async fn fetch_server_binary(
88 &self,
89 version: Box<dyn 'static + Send + Any>,
90 container_dir: PathBuf,
91 delegate: &dyn LspAdapterDelegate,
92 ) -> Result<LanguageServerBinary> {
93 let version = version.downcast::<Option<String>>().unwrap();
94 let this = *self;
95
96 if let Some(version) = *version {
97 let binary_path = container_dir.join(&format!("gopls_{version}"));
98 if let Ok(metadata) = fs::metadata(&binary_path).await {
99 if metadata.is_file() {
100 remove_matching(&container_dir, |entry| {
101 entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
102 })
103 .await;
104
105 return Ok(LanguageServerBinary {
106 path: binary_path.to_path_buf(),
107 arguments: server_binary_arguments(),
108 });
109 }
110 }
111 } else if let Some(path) = this
112 .cached_server_binary(container_dir.clone(), delegate)
113 .await
114 {
115 return Ok(path);
116 }
117
118 let gobin_dir = container_dir.join("gobin");
119 fs::create_dir_all(&gobin_dir).await?;
120 let install_output = process::Command::new("go")
121 .env("GO111MODULE", "on")
122 .env("GOBIN", &gobin_dir)
123 .args(["install", "golang.org/x/tools/gopls@latest"])
124 .output()
125 .await?;
126 if !install_output.status.success() {
127 Err(anyhow!("failed to install gopls. Is go installed?"))?;
128 }
129
130 let installed_binary_path = gobin_dir.join("gopls");
131 let version_output = process::Command::new(&installed_binary_path)
132 .arg("version")
133 .output()
134 .await
135 .map_err(|e| anyhow!("failed to run installed gopls binary {:?}", e))?;
136 let version_stdout = str::from_utf8(&version_output.stdout)
137 .map_err(|_| anyhow!("gopls version produced invalid utf8"))?;
138 let version = GOPLS_VERSION_REGEX
139 .find(version_stdout)
140 .ok_or_else(|| anyhow!("failed to parse gopls version output"))?
141 .as_str();
142 let binary_path = container_dir.join(&format!("gopls_{version}"));
143 fs::rename(&installed_binary_path, &binary_path).await?;
144
145 Ok(LanguageServerBinary {
146 path: binary_path.to_path_buf(),
147 arguments: server_binary_arguments(),
148 })
149 }
150
151 async fn cached_server_binary(
152 &self,
153 container_dir: PathBuf,
154 _: &dyn LspAdapterDelegate,
155 ) -> Option<LanguageServerBinary> {
156 get_cached_server_binary(container_dir).await
157 }
158
159 async fn installation_test_binary(
160 &self,
161 container_dir: PathBuf,
162 ) -> Option<LanguageServerBinary> {
163 get_cached_server_binary(container_dir)
164 .await
165 .map(|mut binary| {
166 binary.arguments = vec!["--help".into()];
167 binary
168 })
169 }
170
171 async fn label_for_completion(
172 &self,
173 completion: &lsp::CompletionItem,
174 language: &Arc<Language>,
175 ) -> Option<CodeLabel> {
176 let label = &completion.label;
177
178 // Gopls returns nested fields and methods as completions.
179 // To syntax highlight these, combine their final component
180 // with their detail.
181 let name_offset = label.rfind('.').unwrap_or(0);
182
183 match completion.kind.zip(completion.detail.as_ref()) {
184 Some((lsp::CompletionItemKind::MODULE, detail)) => {
185 let text = format!("{label} {detail}");
186 let source = Rope::from(format!("import {text}").as_str());
187 let runs = language.highlight_text(&source, 7..7 + text.len());
188 return Some(CodeLabel {
189 text,
190 runs,
191 filter_range: 0..label.len(),
192 });
193 }
194 Some((
195 lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
196 detail,
197 )) => {
198 let text = format!("{label} {detail}");
199 let source =
200 Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
201 let runs = adjust_runs(
202 name_offset,
203 language.highlight_text(&source, 4..4 + text.len()),
204 );
205 return Some(CodeLabel {
206 text,
207 runs,
208 filter_range: 0..label.len(),
209 });
210 }
211 Some((lsp::CompletionItemKind::STRUCT, _)) => {
212 let text = format!("{label} struct {{}}");
213 let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
214 let runs = adjust_runs(
215 name_offset,
216 language.highlight_text(&source, 5..5 + text.len()),
217 );
218 return Some(CodeLabel {
219 text,
220 runs,
221 filter_range: 0..label.len(),
222 });
223 }
224 Some((lsp::CompletionItemKind::INTERFACE, _)) => {
225 let text = format!("{label} interface {{}}");
226 let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
227 let runs = adjust_runs(
228 name_offset,
229 language.highlight_text(&source, 5..5 + text.len()),
230 );
231 return Some(CodeLabel {
232 text,
233 runs,
234 filter_range: 0..label.len(),
235 });
236 }
237 Some((lsp::CompletionItemKind::FIELD, detail)) => {
238 let text = format!("{label} {detail}");
239 let source =
240 Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
241 let runs = adjust_runs(
242 name_offset,
243 language.highlight_text(&source, 16..16 + text.len()),
244 );
245 return Some(CodeLabel {
246 text,
247 runs,
248 filter_range: 0..label.len(),
249 });
250 }
251 Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
252 if let Some(signature) = detail.strip_prefix("func") {
253 let text = format!("{label}{signature}");
254 let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
255 let runs = adjust_runs(
256 name_offset,
257 language.highlight_text(&source, 5..5 + text.len()),
258 );
259 return Some(CodeLabel {
260 filter_range: 0..label.len(),
261 text,
262 runs,
263 });
264 }
265 }
266 _ => {}
267 }
268 None
269 }
270
271 async fn label_for_symbol(
272 &self,
273 name: &str,
274 kind: lsp::SymbolKind,
275 language: &Arc<Language>,
276 ) -> Option<CodeLabel> {
277 let (text, filter_range, display_range) = match kind {
278 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
279 let text = format!("func {} () {{}}", name);
280 let filter_range = 5..5 + name.len();
281 let display_range = 0..filter_range.end;
282 (text, filter_range, display_range)
283 }
284 lsp::SymbolKind::STRUCT => {
285 let text = format!("type {} struct {{}}", name);
286 let filter_range = 5..5 + name.len();
287 let display_range = 0..text.len();
288 (text, filter_range, display_range)
289 }
290 lsp::SymbolKind::INTERFACE => {
291 let text = format!("type {} interface {{}}", name);
292 let filter_range = 5..5 + name.len();
293 let display_range = 0..text.len();
294 (text, filter_range, display_range)
295 }
296 lsp::SymbolKind::CLASS => {
297 let text = format!("type {} T", name);
298 let filter_range = 5..5 + name.len();
299 let display_range = 0..filter_range.end;
300 (text, filter_range, display_range)
301 }
302 lsp::SymbolKind::CONSTANT => {
303 let text = format!("const {} = nil", name);
304 let filter_range = 6..6 + name.len();
305 let display_range = 0..filter_range.end;
306 (text, filter_range, display_range)
307 }
308 lsp::SymbolKind::VARIABLE => {
309 let text = format!("var {} = nil", name);
310 let filter_range = 4..4 + name.len();
311 let display_range = 0..filter_range.end;
312 (text, filter_range, display_range)
313 }
314 lsp::SymbolKind::MODULE => {
315 let text = format!("package {}", name);
316 let filter_range = 8..8 + name.len();
317 let display_range = 0..filter_range.end;
318 (text, filter_range, display_range)
319 }
320 _ => return None,
321 };
322
323 Some(CodeLabel {
324 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
325 text: text[display_range].to_string(),
326 filter_range,
327 })
328 }
329}
330
331async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
332 (|| async move {
333 let mut last_binary_path = None;
334 let mut entries = fs::read_dir(&container_dir).await?;
335 while let Some(entry) = entries.next().await {
336 let entry = entry?;
337 if entry.file_type().await?.is_file()
338 && entry
339 .file_name()
340 .to_str()
341 .map_or(false, |name| name.starts_with("gopls_"))
342 {
343 last_binary_path = Some(entry.path());
344 }
345 }
346
347 if let Some(path) = last_binary_path {
348 Ok(LanguageServerBinary {
349 path,
350 arguments: server_binary_arguments(),
351 })
352 } else {
353 Err(anyhow!("no cached binary"))
354 }
355 })()
356 .await
357 .log_err()
358}
359
360fn adjust_runs(
361 delta: usize,
362 mut runs: Vec<(Range<usize>, HighlightId)>,
363) -> Vec<(Range<usize>, HighlightId)> {
364 for (range, _) in &mut runs {
365 range.start += delta;
366 range.end += delta;
367 }
368 runs
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::languages::language;
375 use gpui::Hsla;
376 use theme::SyntaxTheme;
377
378 #[gpui::test]
379 async fn test_go_label_for_completion() {
380 let language = language(
381 "go",
382 tree_sitter_go::language(),
383 Some(Arc::new(GoLspAdapter)),
384 )
385 .await;
386
387 let theme = SyntaxTheme::new_test([
388 ("type", Hsla::default()),
389 ("keyword", Hsla::default()),
390 ("function", Hsla::default()),
391 ("number", Hsla::default()),
392 ("property", Hsla::default()),
393 ]);
394 language.set_theme(&theme);
395
396 let grammar = language.grammar().unwrap();
397 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
398 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
399 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
400 let highlight_number = grammar.highlight_id_for_name("number").unwrap();
401 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
402
403 assert_eq!(
404 language
405 .label_for_completion(&lsp::CompletionItem {
406 kind: Some(lsp::CompletionItemKind::FUNCTION),
407 label: "Hello".to_string(),
408 detail: Some("func(a B) c.D".to_string()),
409 ..Default::default()
410 })
411 .await,
412 Some(CodeLabel {
413 text: "Hello(a B) c.D".to_string(),
414 filter_range: 0..5,
415 runs: vec![
416 (0..5, highlight_function),
417 (8..9, highlight_type),
418 (13..14, highlight_type),
419 ],
420 })
421 );
422
423 // Nested methods
424 assert_eq!(
425 language
426 .label_for_completion(&lsp::CompletionItem {
427 kind: Some(lsp::CompletionItemKind::METHOD),
428 label: "one.two.Three".to_string(),
429 detail: Some("func() [3]interface{}".to_string()),
430 ..Default::default()
431 })
432 .await,
433 Some(CodeLabel {
434 text: "one.two.Three() [3]interface{}".to_string(),
435 filter_range: 0..13,
436 runs: vec![
437 (8..13, highlight_function),
438 (17..18, highlight_number),
439 (19..28, highlight_keyword),
440 ],
441 })
442 );
443
444 // Nested fields
445 assert_eq!(
446 language
447 .label_for_completion(&lsp::CompletionItem {
448 kind: Some(lsp::CompletionItemKind::FIELD),
449 label: "two.Three".to_string(),
450 detail: Some("a.Bcd".to_string()),
451 ..Default::default()
452 })
453 .await,
454 Some(CodeLabel {
455 text: "two.Three a.Bcd".to_string(),
456 filter_range: 0..9,
457 runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
458 })
459 );
460 }
461}