1use anyhow::{anyhow, bail, Context, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::AppContext;
6use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
7use lsp::{CodeActionKind, LanguageServerBinary};
8use schemars::JsonSchema;
9use serde_derive::{Deserialize, Serialize};
10use serde_json::json;
11use settings::{Settings, SettingsSources};
12use smol::{fs, fs::File};
13use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
14use util::{
15 fs::remove_matching,
16 github::{latest_github_release, GitHubLspBinaryVersion},
17 maybe, ResultExt,
18};
19
20#[derive(Clone, Serialize, Deserialize, JsonSchema)]
21pub struct DenoSettings {
22 pub enable: bool,
23}
24
25#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
26pub struct DenoSettingsContent {
27 enable: Option<bool>,
28}
29
30impl Settings for DenoSettings {
31 const KEY: Option<&'static str> = Some("deno");
32
33 type FileContent = DenoSettingsContent;
34
35 fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
36 sources.json_merge()
37 }
38}
39
40fn deno_server_binary_arguments() -> Vec<OsString> {
41 vec!["lsp".into()]
42}
43
44pub struct DenoLspAdapter {}
45
46impl DenoLspAdapter {
47 pub fn new() -> Self {
48 DenoLspAdapter {}
49 }
50}
51
52#[async_trait(?Send)]
53impl LspAdapter for DenoLspAdapter {
54 fn name(&self) -> LanguageServerName {
55 LanguageServerName("deno-language-server".into())
56 }
57
58 async fn fetch_latest_server_version(
59 &self,
60 delegate: &dyn LspAdapterDelegate,
61 ) -> Result<Box<dyn 'static + Send + Any>> {
62 let release =
63 latest_github_release("denoland/deno", true, false, delegate.http_client()).await?;
64 let os = match consts::OS {
65 "macos" => "apple-darwin",
66 "linux" => "unknown-linux-gnu",
67 "windows" => "pc-windows-msvc",
68 other => bail!("Running on unsupported os: {other}"),
69 };
70 let asset_name = format!("deno-{}-{os}.zip", consts::ARCH);
71 let asset = release
72 .assets
73 .iter()
74 .find(|asset| asset.name == asset_name)
75 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
76 let version = GitHubLspBinaryVersion {
77 name: release.tag_name,
78 url: asset.browser_download_url.clone(),
79 };
80 Ok(Box::new(version) as Box<_>)
81 }
82
83 async fn fetch_server_binary(
84 &self,
85 version: Box<dyn 'static + Send + Any>,
86 container_dir: PathBuf,
87 delegate: &dyn LspAdapterDelegate,
88 ) -> Result<LanguageServerBinary> {
89 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
90 let zip_path = container_dir.join(format!("deno_{}.zip", version.name));
91 let version_dir = container_dir.join(format!("deno_{}", version.name));
92 let binary_path = version_dir.join("deno");
93
94 if fs::metadata(&binary_path).await.is_err() {
95 let mut response = delegate
96 .http_client()
97 .get(&version.url, Default::default(), true)
98 .await
99 .context("error downloading release")?;
100 let mut file = File::create(&zip_path).await?;
101 if !response.status().is_success() {
102 Err(anyhow!(
103 "download failed with status {}",
104 response.status().to_string()
105 ))?;
106 }
107 futures::io::copy(response.body_mut(), &mut file).await?;
108
109 let unzip_status = smol::process::Command::new("unzip")
110 .current_dir(&container_dir)
111 .arg(&zip_path)
112 .arg("-d")
113 .arg(&version_dir)
114 .output()
115 .await?
116 .status;
117 if !unzip_status.success() {
118 Err(anyhow!("failed to unzip deno archive"))?;
119 }
120
121 remove_matching(&container_dir, |entry| entry != version_dir).await;
122 }
123
124 Ok(LanguageServerBinary {
125 path: binary_path,
126 env: None,
127 arguments: deno_server_binary_arguments(),
128 })
129 }
130
131 async fn cached_server_binary(
132 &self,
133 container_dir: PathBuf,
134 _: &dyn LspAdapterDelegate,
135 ) -> Option<LanguageServerBinary> {
136 get_cached_server_binary(container_dir).await
137 }
138
139 async fn installation_test_binary(
140 &self,
141 container_dir: PathBuf,
142 ) -> Option<LanguageServerBinary> {
143 get_cached_server_binary(container_dir).await
144 }
145
146 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
147 Some(vec![
148 CodeActionKind::QUICKFIX,
149 CodeActionKind::REFACTOR,
150 CodeActionKind::REFACTOR_EXTRACT,
151 CodeActionKind::SOURCE,
152 ])
153 }
154
155 async fn label_for_completion(
156 &self,
157 item: &lsp::CompletionItem,
158 language: &Arc<language::Language>,
159 ) -> Option<language::CodeLabel> {
160 use lsp::CompletionItemKind as Kind;
161 let len = item.label.len();
162 let grammar = language.grammar()?;
163 let highlight_id = match item.kind? {
164 Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
165 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
166 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
167 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
168 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
169 _ => None,
170 }?;
171
172 let text = match &item.detail {
173 Some(detail) => format!("{} {}", item.label, detail),
174 None => item.label.clone(),
175 };
176
177 Some(language::CodeLabel {
178 text,
179 runs: vec![(0..len, highlight_id)],
180 filter_range: 0..len,
181 })
182 }
183
184 async fn initialization_options(
185 self: Arc<Self>,
186 _: &Arc<dyn LspAdapterDelegate>,
187 ) -> Result<Option<serde_json::Value>> {
188 Ok(Some(json!({
189 "provideFormatter": true,
190 })))
191 }
192
193 fn language_ids(&self) -> HashMap<String, String> {
194 HashMap::from_iter([
195 ("TypeScript".into(), "typescript".into()),
196 ("JavaScript".into(), "javascript".into()),
197 ("TSX".into(), "typescriptreact".into()),
198 ])
199 }
200}
201
202async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
203 maybe!(async {
204 let mut last = None;
205 let mut entries = fs::read_dir(&container_dir).await?;
206 while let Some(entry) = entries.next().await {
207 last = Some(entry?.path());
208 }
209
210 match last {
211 Some(path) if path.is_dir() => {
212 let binary = path.join("deno");
213 if fs::metadata(&binary).await.is_ok() {
214 return Ok(LanguageServerBinary {
215 path: binary,
216 env: None,
217 arguments: deno_server_binary_arguments(),
218 });
219 }
220 }
221 _ => {}
222 }
223
224 Err(anyhow!("no cached binary"))
225 })
226 .await
227 .log_err()
228}