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