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 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 server_path = container_dir.join(SERVER_PATH);
305 anyhow::ensure!(
306 server_path.exists(),
307 "missing executable in directory {server_path:?}"
308 );
309 Ok(LanguageServerBinary {
310 path: node.binary_path().await?,
311 env: None,
312 arguments: server_binary_arguments(&server_path),
313 })
314 })
315 .await
316 .log_err()
317}
318
319pub struct NodeVersionAdapter;
320
321impl NodeVersionAdapter {
322 const SERVER_NAME: LanguageServerName =
323 LanguageServerName::new_static("package-version-server");
324}
325
326impl LspInstaller for NodeVersionAdapter {
327 type BinaryVersion = GitHubLspBinaryVersion;
328
329 async fn fetch_latest_server_version(
330 &self,
331 delegate: &dyn LspAdapterDelegate,
332 _: bool,
333 _: &mut AsyncApp,
334 ) -> Result<GitHubLspBinaryVersion> {
335 let release = latest_github_release(
336 "zed-industries/package-version-server",
337 true,
338 false,
339 delegate.http_client(),
340 )
341 .await?;
342 let os = match consts::OS {
343 "macos" => "apple-darwin",
344 "linux" => "unknown-linux-gnu",
345 "windows" => "pc-windows-msvc",
346 other => bail!("Running on unsupported os: {other}"),
347 };
348 let suffix = if consts::OS == "windows" {
349 ".zip"
350 } else {
351 ".tar.gz"
352 };
353 let asset_name = format!("{}-{}-{os}{suffix}", Self::SERVER_NAME, consts::ARCH);
354 let asset = release
355 .assets
356 .iter()
357 .find(|asset| asset.name == asset_name)
358 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
359 Ok(GitHubLspBinaryVersion {
360 name: release.tag_name,
361 url: asset.browser_download_url.clone(),
362 digest: asset.digest.clone(),
363 })
364 }
365
366 async fn check_if_user_installed(
367 &self,
368 delegate: &dyn LspAdapterDelegate,
369 _: Option<Toolchain>,
370 _: &AsyncApp,
371 ) -> Option<LanguageServerBinary> {
372 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
373 Some(LanguageServerBinary {
374 path,
375 env: None,
376 arguments: Default::default(),
377 })
378 }
379
380 async fn fetch_server_binary(
381 &self,
382 latest_version: GitHubLspBinaryVersion,
383 container_dir: PathBuf,
384 delegate: &dyn LspAdapterDelegate,
385 ) -> Result<LanguageServerBinary> {
386 let version = &latest_version;
387 let destination_path = container_dir.join(format!(
388 "{}-{}{}",
389 Self::SERVER_NAME,
390 version.name,
391 std::env::consts::EXE_SUFFIX
392 ));
393 let destination_container_path =
394 container_dir.join(format!("{}-{}-tmp", Self::SERVER_NAME, version.name));
395 if fs::metadata(&destination_path).await.is_err() {
396 let mut response = delegate
397 .http_client()
398 .get(&version.url, Default::default(), true)
399 .await
400 .context("downloading release")?;
401 if version.url.ends_with(".zip") {
402 extract_zip(&destination_container_path, response.body_mut()).await?;
403 } else if version.url.ends_with(".tar.gz") {
404 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
405 let archive = Archive::new(decompressed_bytes);
406 archive.unpack(&destination_container_path).await?;
407 }
408
409 fs::copy(
410 destination_container_path.join(format!(
411 "{}{}",
412 Self::SERVER_NAME,
413 std::env::consts::EXE_SUFFIX
414 )),
415 &destination_path,
416 )
417 .await?;
418 remove_matching(&container_dir, |entry| entry != destination_path).await;
419 }
420 Ok(LanguageServerBinary {
421 path: destination_path,
422 env: None,
423 arguments: Default::default(),
424 })
425 }
426
427 async fn cached_server_binary(
428 &self,
429 container_dir: PathBuf,
430 _delegate: &dyn LspAdapterDelegate,
431 ) -> Option<LanguageServerBinary> {
432 get_cached_version_server_binary(container_dir).await
433 }
434}
435
436#[async_trait(?Send)]
437impl LspAdapter for NodeVersionAdapter {
438 fn name(&self) -> LanguageServerName {
439 Self::SERVER_NAME
440 }
441}
442
443async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
444 maybe!(async {
445 let mut last = None;
446 let mut entries = fs::read_dir(&container_dir).await?;
447 while let Some(entry) = entries.next().await {
448 last = Some(entry?.path());
449 }
450
451 anyhow::Ok(LanguageServerBinary {
452 path: last.context("no cached binary")?,
453 env: None,
454 arguments: Default::default(),
455 })
456 })
457 .await
458 .log_err()
459}