json.rs

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