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