1use anyhow::{Context as _, Result, bail};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use collections::HashMap;
6use futures::StreamExt;
7use gpui::{App, AsyncApp, Task};
8use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
9use language::{
10 ContextProvider, LanguageName, LocalFile as _, LspAdapter, LspAdapterDelegate, LspInstaller,
11 Toolchain,
12};
13use lsp::{LanguageServerBinary, LanguageServerName};
14use node_runtime::{NodeRuntime, VersionStrategy};
15use project::lsp_store::language_server_settings;
16use serde_json::{Value, json};
17use smol::{
18 fs::{self},
19 io::BufReader,
20};
21use std::{
22 env::consts,
23 ffi::OsString,
24 path::{Path, PathBuf},
25 str::FromStr,
26 sync::Arc,
27};
28use task::{TaskTemplate, TaskTemplates, VariableName};
29use util::{
30 ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into,
31 rel_path::RelPath,
32};
33
34use crate::PackageJsonData;
35
36const SERVER_PATH: &str =
37 "node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
38
39pub(crate) struct JsonTaskProvider;
40
41impl ContextProvider for JsonTaskProvider {
42 fn associated_tasks(
43 &self,
44 file: Option<Arc<dyn language::File>>,
45 cx: &App,
46 ) -> gpui::Task<Option<TaskTemplates>> {
47 let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
48 return Task::ready(None);
49 };
50 let is_package_json = file.path.ends_with(RelPath::unix("package.json").unwrap());
51 let is_composer_json = file.path.ends_with(RelPath::unix("composer.json").unwrap());
52 if !is_package_json && !is_composer_json {
53 return Task::ready(None);
54 }
55
56 cx.spawn(async move |cx| {
57 let contents = file
58 .worktree
59 .update(cx, |this, cx| this.load_file(&file.path, cx))
60 .ok()?
61 .await
62 .ok()?;
63 let path = cx.update(|cx| file.abs_path(cx)).ok()?.as_path().into();
64
65 let task_templates = if is_package_json {
66 let package_json = serde_json_lenient::from_str::<
67 HashMap<String, serde_json_lenient::Value>,
68 >(&contents.text)
69 .ok()?;
70 let package_json = PackageJsonData::new(path, package_json);
71 let command = package_json.package_manager.unwrap_or("npm").to_owned();
72 package_json
73 .scripts
74 .into_iter()
75 .map(|(_, key)| TaskTemplate {
76 label: format!("run {key}"),
77 command: command.clone(),
78 args: vec!["run".into(), key],
79 cwd: Some(VariableName::Dirname.template_value()),
80 ..TaskTemplate::default()
81 })
82 .chain([TaskTemplate {
83 label: "package script $ZED_CUSTOM_script".to_owned(),
84 command: command.clone(),
85 args: vec![
86 "run".into(),
87 VariableName::Custom("script".into()).template_value(),
88 ],
89 cwd: Some(VariableName::Dirname.template_value()),
90 tags: vec!["package-script".into()],
91 ..TaskTemplate::default()
92 }])
93 .collect()
94 } else if is_composer_json {
95 serde_json_lenient::Value::from_str(&contents.text)
96 .ok()?
97 .get("scripts")?
98 .as_object()?
99 .keys()
100 .map(|key| TaskTemplate {
101 label: format!("run {key}"),
102 command: "composer".to_owned(),
103 args: vec!["-d".into(), "$ZED_DIRNAME".into(), key.into()],
104 ..TaskTemplate::default()
105 })
106 .chain([TaskTemplate {
107 label: "composer script $ZED_CUSTOM_script".to_owned(),
108 command: "composer".to_owned(),
109 args: vec![
110 "-d".into(),
111 "$ZED_DIRNAME".into(),
112 VariableName::Custom("script".into()).template_value(),
113 ],
114 tags: vec!["composer-script".into()],
115 ..TaskTemplate::default()
116 }])
117 .collect()
118 } else {
119 vec![]
120 };
121
122 Some(TaskTemplates(task_templates))
123 })
124 }
125}
126
127fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
128 vec![server_path.into(), "--stdio".into()]
129}
130
131pub struct JsonLspAdapter {
132 node: NodeRuntime,
133}
134
135impl JsonLspAdapter {
136 const PACKAGE_NAME: &str = "vscode-langservers-extracted";
137
138 pub const fn new(node: NodeRuntime) -> Self {
139 Self { node }
140 }
141}
142
143impl LspInstaller for JsonLspAdapter {
144 type BinaryVersion = String;
145
146 async fn fetch_latest_server_version(
147 &self,
148 _: &dyn LspAdapterDelegate,
149 _: bool,
150 _: &mut AsyncApp,
151 ) -> Result<String> {
152 self.node
153 .npm_package_latest_version(Self::PACKAGE_NAME)
154 .await
155 }
156
157 async fn check_if_user_installed(
158 &self,
159 delegate: &dyn LspAdapterDelegate,
160 _: Option<Toolchain>,
161 _: &AsyncApp,
162 ) -> Option<LanguageServerBinary> {
163 let path = delegate
164 .which("vscode-json-language-server".as_ref())
165 .await?;
166 let env = delegate.shell_env().await;
167
168 Some(LanguageServerBinary {
169 path,
170 env: Some(env),
171 arguments: vec!["--stdio".into()],
172 })
173 }
174
175 async fn check_if_version_installed(
176 &self,
177 version: &String,
178 container_dir: &PathBuf,
179 _: &dyn LspAdapterDelegate,
180 ) -> Option<LanguageServerBinary> {
181 let server_path = container_dir.join(SERVER_PATH);
182
183 let should_install_language_server = self
184 .node
185 .should_install_npm_package(
186 Self::PACKAGE_NAME,
187 &server_path,
188 container_dir,
189 VersionStrategy::Latest(version),
190 )
191 .await;
192
193 if should_install_language_server {
194 None
195 } else {
196 Some(LanguageServerBinary {
197 path: self.node.binary_path().await.ok()?,
198 env: None,
199 arguments: server_binary_arguments(&server_path),
200 })
201 }
202 }
203
204 async fn fetch_server_binary(
205 &self,
206 latest_version: String,
207 container_dir: PathBuf,
208 _: &dyn LspAdapterDelegate,
209 ) -> Result<LanguageServerBinary> {
210 let server_path = container_dir.join(SERVER_PATH);
211
212 self.node
213 .npm_install_packages(
214 &container_dir,
215 &[(Self::PACKAGE_NAME, latest_version.as_str())],
216 )
217 .await?;
218
219 Ok(LanguageServerBinary {
220 path: self.node.binary_path().await?,
221 env: None,
222 arguments: server_binary_arguments(&server_path),
223 })
224 }
225
226 async fn cached_server_binary(
227 &self,
228 container_dir: PathBuf,
229 _: &dyn LspAdapterDelegate,
230 ) -> Option<LanguageServerBinary> {
231 get_cached_server_binary(container_dir, &self.node).await
232 }
233}
234
235#[async_trait(?Send)]
236impl LspAdapter for JsonLspAdapter {
237 fn name(&self) -> LanguageServerName {
238 LanguageServerName("json-language-server".into())
239 }
240
241 async fn initialization_options(
242 self: Arc<Self>,
243 _: &Arc<dyn LspAdapterDelegate>,
244 ) -> Result<Option<serde_json::Value>> {
245 Ok(Some(json!({
246 "provideFormatter": true
247 })))
248 }
249
250 async fn workspace_configuration(
251 self: Arc<Self>,
252 delegate: &Arc<dyn LspAdapterDelegate>,
253 _: Option<Toolchain>,
254 cx: &mut AsyncApp,
255 ) -> Result<Value> {
256 let mut config = cx.update(|cx| {
257 let schemas = json_schema_store::all_schema_file_associations(cx);
258
259 // This can be viewed via `dev: open language server logs` -> `json-language-server` ->
260 // `Server Info`
261 serde_json::json!({
262 "json": {
263 "format": {
264 "enable": true,
265 },
266 "validate": {
267 "enable": true,
268 },
269 "schemas": schemas
270 }
271 })
272 })?;
273 let project_options = cx.update(|cx| {
274 language_server_settings(delegate.as_ref(), &self.name(), cx)
275 .and_then(|s| s.settings.clone())
276 })?;
277
278 if let Some(override_options) = project_options {
279 merge_json_value_into(override_options, &mut config);
280 }
281
282 Ok(config)
283 }
284
285 fn language_ids(&self) -> HashMap<LanguageName, String> {
286 [
287 (LanguageName::new("JSON"), "json".into()),
288 (LanguageName::new("JSONC"), "jsonc".into()),
289 ]
290 .into_iter()
291 .collect()
292 }
293
294 fn is_primary_zed_json_schema_adapter(&self) -> bool {
295 true
296 }
297}
298
299async fn get_cached_server_binary(
300 container_dir: PathBuf,
301 node: &NodeRuntime,
302) -> Option<LanguageServerBinary> {
303 maybe!(async {
304 let mut last_version_dir = None;
305 let mut entries = fs::read_dir(&container_dir).await?;
306 while let Some(entry) = entries.next().await {
307 let entry = entry?;
308 if entry.file_type().await?.is_dir() {
309 last_version_dir = Some(entry.path());
310 }
311 }
312
313 let last_version_dir = last_version_dir.context("no cached binary")?;
314 let server_path = last_version_dir.join(SERVER_PATH);
315 anyhow::ensure!(
316 server_path.exists(),
317 "missing executable in directory {last_version_dir:?}"
318 );
319 Ok(LanguageServerBinary {
320 path: node.binary_path().await?,
321 env: None,
322 arguments: server_binary_arguments(&server_path),
323 })
324 })
325 .await
326 .log_err()
327}
328
329pub struct NodeVersionAdapter;
330
331impl NodeVersionAdapter {
332 const SERVER_NAME: LanguageServerName =
333 LanguageServerName::new_static("package-version-server");
334}
335
336impl LspInstaller for NodeVersionAdapter {
337 type BinaryVersion = GitHubLspBinaryVersion;
338
339 async fn fetch_latest_server_version(
340 &self,
341 delegate: &dyn LspAdapterDelegate,
342 _: bool,
343 _: &mut AsyncApp,
344 ) -> Result<GitHubLspBinaryVersion> {
345 let release = latest_github_release(
346 "zed-industries/package-version-server",
347 true,
348 false,
349 delegate.http_client(),
350 )
351 .await?;
352 let os = match consts::OS {
353 "macos" => "apple-darwin",
354 "linux" => "unknown-linux-gnu",
355 "windows" => "pc-windows-msvc",
356 other => bail!("Running on unsupported os: {other}"),
357 };
358 let suffix = if consts::OS == "windows" {
359 ".zip"
360 } else {
361 ".tar.gz"
362 };
363 let asset_name = format!("{}-{}-{os}{suffix}", Self::SERVER_NAME, consts::ARCH);
364 let asset = release
365 .assets
366 .iter()
367 .find(|asset| asset.name == asset_name)
368 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
369 Ok(GitHubLspBinaryVersion {
370 name: release.tag_name,
371 url: asset.browser_download_url.clone(),
372 digest: asset.digest.clone(),
373 })
374 }
375
376 async fn check_if_user_installed(
377 &self,
378 delegate: &dyn LspAdapterDelegate,
379 _: Option<Toolchain>,
380 _: &AsyncApp,
381 ) -> Option<LanguageServerBinary> {
382 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
383 Some(LanguageServerBinary {
384 path,
385 env: None,
386 arguments: Default::default(),
387 })
388 }
389
390 async fn fetch_server_binary(
391 &self,
392 latest_version: GitHubLspBinaryVersion,
393 container_dir: PathBuf,
394 delegate: &dyn LspAdapterDelegate,
395 ) -> Result<LanguageServerBinary> {
396 let version = &latest_version;
397 let destination_path = container_dir.join(format!(
398 "{}-{}{}",
399 Self::SERVER_NAME,
400 version.name,
401 std::env::consts::EXE_SUFFIX
402 ));
403 let destination_container_path =
404 container_dir.join(format!("{}-{}-tmp", Self::SERVER_NAME, version.name));
405 if fs::metadata(&destination_path).await.is_err() {
406 let mut response = delegate
407 .http_client()
408 .get(&version.url, Default::default(), true)
409 .await
410 .context("downloading release")?;
411 if version.url.ends_with(".zip") {
412 extract_zip(&destination_container_path, response.body_mut()).await?;
413 } else if version.url.ends_with(".tar.gz") {
414 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
415 let archive = Archive::new(decompressed_bytes);
416 archive.unpack(&destination_container_path).await?;
417 }
418
419 fs::copy(
420 destination_container_path.join(format!(
421 "{}{}",
422 Self::SERVER_NAME,
423 std::env::consts::EXE_SUFFIX
424 )),
425 &destination_path,
426 )
427 .await?;
428 remove_matching(&container_dir, |entry| entry != destination_path).await;
429 }
430 Ok(LanguageServerBinary {
431 path: destination_path,
432 env: None,
433 arguments: Default::default(),
434 })
435 }
436
437 async fn cached_server_binary(
438 &self,
439 container_dir: PathBuf,
440 _delegate: &dyn LspAdapterDelegate,
441 ) -> Option<LanguageServerBinary> {
442 get_cached_version_server_binary(container_dir).await
443 }
444}
445
446#[async_trait(?Send)]
447impl LspAdapter for NodeVersionAdapter {
448 fn name(&self) -> LanguageServerName {
449 Self::SERVER_NAME
450 }
451}
452
453async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
454 maybe!(async {
455 let mut last = None;
456 let mut entries = fs::read_dir(&container_dir).await?;
457 while let Some(entry) = entries.next().await {
458 last = Some(entry?.path());
459 }
460
461 anyhow::Ok(LanguageServerBinary {
462 path: last.context("no cached binary")?,
463 env: None,
464 arguments: Default::default(),
465 })
466 })
467 .await
468 .log_err()
469}