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