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