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