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