1use anyhow::{anyhow, Context, Result};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use futures::{io::BufReader, StreamExt};
6use gpui::{AsyncAppContext, Task};
7use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
8use lsp::LanguageServerBinary;
9use smol::fs;
10use std::env::consts::{ARCH, OS};
11use std::ffi::OsString;
12use std::sync::Arc;
13use std::{any::Any, path::PathBuf};
14use util::async_maybe;
15use util::github::latest_github_release;
16use util::{github::GitHubLspBinaryVersion, ResultExt};
17
18pub struct ZlsAdapter;
19
20#[async_trait]
21impl LspAdapter for ZlsAdapter {
22 fn name(&self) -> LanguageServerName {
23 LanguageServerName("zls".into())
24 }
25
26 fn short_name(&self) -> &'static str {
27 "zls"
28 }
29
30 async fn fetch_latest_server_version(
31 &self,
32 delegate: &dyn LspAdapterDelegate,
33 ) -> Result<Box<dyn 'static + Send + Any>> {
34 let release =
35 latest_github_release("zigtools/zls", true, false, delegate.http_client()).await?;
36 let asset_name = format!("zls-{ARCH}-{OS}.tar.gz");
37 let asset = release
38 .assets
39 .iter()
40 .find(|asset| asset.name == asset_name)
41 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
42 let version = GitHubLspBinaryVersion {
43 name: release.tag_name,
44 url: asset.browser_download_url.clone(),
45 };
46
47 Ok(Box::new(version) as Box<_>)
48 }
49
50 fn check_if_user_installed(
51 &self,
52 delegate: &Arc<dyn LspAdapterDelegate>,
53 cx: &mut AsyncAppContext,
54 ) -> Option<Task<Option<LanguageServerBinary>>> {
55 let delegate = delegate.clone();
56
57 Some(cx.spawn(|cx| async move {
58 match cx.update(|cx| delegate.which_command(OsString::from("zls"), cx)) {
59 Ok(task) => task.await.map(|(path, env)| LanguageServerBinary {
60 path,
61 arguments: vec![],
62 env: Some(env),
63 }),
64 Err(_) => None,
65 }
66 }))
67 }
68
69 async fn fetch_server_binary(
70 &self,
71 version: Box<dyn 'static + Send + Any>,
72 container_dir: PathBuf,
73 delegate: &dyn LspAdapterDelegate,
74 ) -> Result<LanguageServerBinary> {
75 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
76 let binary_path = container_dir.join("bin/zls");
77
78 if fs::metadata(&binary_path).await.is_err() {
79 let mut response = delegate
80 .http_client()
81 .get(&version.url, Default::default(), true)
82 .await
83 .context("error downloading release")?;
84 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
85 let archive = Archive::new(decompressed_bytes);
86 archive.unpack(container_dir).await?;
87 }
88
89 // todo!("windows")
90 #[cfg(not(windows))]
91 {
92 fs::set_permissions(
93 &binary_path,
94 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
95 )
96 .await?;
97 }
98 Ok(LanguageServerBinary {
99 path: binary_path,
100 env: None,
101 arguments: vec![],
102 })
103 }
104
105 async fn cached_server_binary(
106 &self,
107 container_dir: PathBuf,
108 _: &dyn LspAdapterDelegate,
109 ) -> Option<LanguageServerBinary> {
110 get_cached_server_binary(container_dir).await
111 }
112
113 async fn installation_test_binary(
114 &self,
115 container_dir: PathBuf,
116 ) -> Option<LanguageServerBinary> {
117 get_cached_server_binary(container_dir)
118 .await
119 .map(|mut binary| {
120 binary.arguments = vec!["--help".into()];
121 binary
122 })
123 }
124}
125
126async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
127 async_maybe!({
128 let mut last_binary_path = None;
129 let mut entries = fs::read_dir(&container_dir).await?;
130 while let Some(entry) = entries.next().await {
131 let entry = entry?;
132 if entry.file_type().await?.is_file()
133 && entry
134 .file_name()
135 .to_str()
136 .map_or(false, |name| name == "zls")
137 {
138 last_binary_path = Some(entry.path());
139 }
140 }
141
142 if let Some(path) = last_binary_path {
143 Ok(LanguageServerBinary {
144 path,
145 env: None,
146 arguments: Vec::new(),
147 })
148 } else {
149 Err(anyhow!("no cached binary"))
150 }
151 })
152 .await
153 .log_err()
154}