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, Uri};
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 _: Option<Uri>,
255 cx: &mut AsyncApp,
256 ) -> Result<Value> {
257 let mut config = cx.update(|cx| {
258 let schemas = json_schema_store::all_schema_file_associations(cx);
259
260 // This can be viewed via `dev: open language server logs` -> `json-language-server` ->
261 // `Server Info`
262 serde_json::json!({
263 "json": {
264 "format": {
265 "enable": true,
266 },
267 "validate": {
268 "enable": true,
269 },
270 "schemas": schemas
271 }
272 })
273 })?;
274 let project_options = cx.update(|cx| {
275 language_server_settings(delegate.as_ref(), &self.name(), cx)
276 .and_then(|s| s.settings.clone())
277 })?;
278
279 if let Some(override_options) = project_options {
280 merge_json_value_into(override_options, &mut config);
281 }
282
283 Ok(config)
284 }
285
286 fn language_ids(&self) -> HashMap<LanguageName, String> {
287 [
288 (LanguageName::new("JSON"), "json".into()),
289 (LanguageName::new("JSONC"), "jsonc".into()),
290 ]
291 .into_iter()
292 .collect()
293 }
294
295 fn is_primary_zed_json_schema_adapter(&self) -> bool {
296 true
297 }
298}
299
300async fn get_cached_server_binary(
301 container_dir: PathBuf,
302 node: &NodeRuntime,
303) -> Option<LanguageServerBinary> {
304 maybe!(async {
305 let server_path = container_dir.join(SERVER_PATH);
306 anyhow::ensure!(
307 server_path.exists(),
308 "missing executable in directory {server_path:?}"
309 );
310 Ok(LanguageServerBinary {
311 path: node.binary_path().await?,
312 env: None,
313 arguments: server_binary_arguments(&server_path),
314 })
315 })
316 .await
317 .log_err()
318}
319
320pub struct NodeVersionAdapter;
321
322impl NodeVersionAdapter {
323 const SERVER_NAME: LanguageServerName =
324 LanguageServerName::new_static("package-version-server");
325}
326
327impl LspInstaller for NodeVersionAdapter {
328 type BinaryVersion = GitHubLspBinaryVersion;
329
330 async fn fetch_latest_server_version(
331 &self,
332 delegate: &dyn LspAdapterDelegate,
333 _: bool,
334 _: &mut AsyncApp,
335 ) -> Result<GitHubLspBinaryVersion> {
336 let release = latest_github_release(
337 "zed-industries/package-version-server",
338 true,
339 false,
340 delegate.http_client(),
341 )
342 .await?;
343 let os = match consts::OS {
344 "macos" => "apple-darwin",
345 "linux" => "unknown-linux-gnu",
346 "windows" => "pc-windows-msvc",
347 other => bail!("Running on unsupported os: {other}"),
348 };
349 let suffix = if consts::OS == "windows" {
350 ".zip"
351 } else {
352 ".tar.gz"
353 };
354 let asset_name = format!("{}-{}-{os}{suffix}", Self::SERVER_NAME, consts::ARCH);
355 let asset = release
356 .assets
357 .iter()
358 .find(|asset| asset.name == asset_name)
359 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
360 Ok(GitHubLspBinaryVersion {
361 name: release.tag_name,
362 url: asset.browser_download_url.clone(),
363 digest: asset.digest.clone(),
364 })
365 }
366
367 async fn check_if_user_installed(
368 &self,
369 delegate: &dyn LspAdapterDelegate,
370 _: Option<Toolchain>,
371 _: &AsyncApp,
372 ) -> Option<LanguageServerBinary> {
373 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
374 Some(LanguageServerBinary {
375 path,
376 env: None,
377 arguments: Default::default(),
378 })
379 }
380
381 async fn fetch_server_binary(
382 &self,
383 latest_version: GitHubLspBinaryVersion,
384 container_dir: PathBuf,
385 delegate: &dyn LspAdapterDelegate,
386 ) -> Result<LanguageServerBinary> {
387 let version = &latest_version;
388 let destination_path = container_dir.join(format!(
389 "{}-{}{}",
390 Self::SERVER_NAME,
391 version.name,
392 std::env::consts::EXE_SUFFIX
393 ));
394 let destination_container_path =
395 container_dir.join(format!("{}-{}-tmp", Self::SERVER_NAME, version.name));
396 if fs::metadata(&destination_path).await.is_err() {
397 let mut response = delegate
398 .http_client()
399 .get(&version.url, Default::default(), true)
400 .await
401 .context("downloading release")?;
402 if version.url.ends_with(".zip") {
403 extract_zip(&destination_container_path, response.body_mut()).await?;
404 } else if version.url.ends_with(".tar.gz") {
405 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
406 let archive = Archive::new(decompressed_bytes);
407 archive.unpack(&destination_container_path).await?;
408 }
409
410 fs::copy(
411 destination_container_path.join(format!(
412 "{}{}",
413 Self::SERVER_NAME,
414 std::env::consts::EXE_SUFFIX
415 )),
416 &destination_path,
417 )
418 .await?;
419 remove_matching(&container_dir, |entry| entry != destination_path).await;
420 }
421 Ok(LanguageServerBinary {
422 path: destination_path,
423 env: None,
424 arguments: Default::default(),
425 })
426 }
427
428 async fn cached_server_binary(
429 &self,
430 container_dir: PathBuf,
431 _delegate: &dyn LspAdapterDelegate,
432 ) -> Option<LanguageServerBinary> {
433 get_cached_version_server_binary(container_dir).await
434 }
435}
436
437#[async_trait(?Send)]
438impl LspAdapter for NodeVersionAdapter {
439 fn name(&self) -> LanguageServerName {
440 Self::SERVER_NAME
441 }
442}
443
444async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
445 maybe!(async {
446 let mut last = None;
447 let mut entries = fs::read_dir(&container_dir).await?;
448 while let Some(entry) = entries.next().await {
449 last = Some(entry?.path());
450 }
451
452 anyhow::Ok(LanguageServerBinary {
453 path: last.context("no cached binary")?,
454 env: None,
455 arguments: Default::default(),
456 })
457 })
458 .await
459 .log_err()
460}