json.rs

  1use anyhow::{anyhow, Result};
  2use async_trait::async_trait;
  3use collections::HashMap;
  4use feature_flags::FeatureFlagAppExt;
  5use futures::StreamExt;
  6use gpui::{AppContext, AsyncAppContext};
  7use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
  8use lsp::LanguageServerBinary;
  9use node_runtime::NodeRuntime;
 10use project::ContextProviderWithTasks;
 11use serde_json::{json, Value};
 12use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
 13use smol::fs;
 14use std::{
 15    any::Any,
 16    ffi::OsString,
 17    path::{Path, PathBuf},
 18    str::FromStr,
 19    sync::{Arc, OnceLock},
 20};
 21use task::{TaskTemplate, TaskTemplates, VariableName};
 22use util::{maybe, paths, ResultExt};
 23
 24const SERVER_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
 25
 26// Origin: https://github.com/SchemaStore/schemastore
 27const TSCONFIG_SCHEMA: &str = include_str!("json/schemas/tsconfig.json");
 28pub(super) fn json_task_context() -> ContextProviderWithTasks {
 29    ContextProviderWithTasks::new(TaskTemplates(vec![
 30        TaskTemplate {
 31            label: "package script $ZED_CUSTOM_script".to_owned(),
 32            command: "npm run".to_owned(),
 33            args: vec![VariableName::Custom("script".into()).template_value()],
 34            tags: vec!["package-script".into()],
 35            ..TaskTemplate::default()
 36        },
 37        TaskTemplate {
 38            label: "composer script $ZED_CUSTOM_script".to_owned(),
 39            command: "composer".to_owned(),
 40            args: vec![VariableName::Custom("script".into()).template_value()],
 41            tags: vec!["composer-script".into()],
 42            ..TaskTemplate::default()
 43        },
 44    ]))
 45}
 46
 47fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 48    vec![server_path.into(), "--stdio".into()]
 49}
 50
 51pub struct JsonLspAdapter {
 52    node: Arc<dyn NodeRuntime>,
 53    languages: Arc<LanguageRegistry>,
 54    workspace_config: OnceLock<Value>,
 55}
 56
 57impl JsonLspAdapter {
 58    pub fn new(node: Arc<dyn NodeRuntime>, languages: Arc<LanguageRegistry>) -> Self {
 59        Self {
 60            node,
 61            languages,
 62            workspace_config: Default::default(),
 63        }
 64    }
 65
 66    fn get_workspace_config(language_names: Vec<String>, cx: &mut AppContext) -> Value {
 67        let action_names = cx.all_action_names();
 68        let staff_mode = cx.is_staff();
 69
 70        let font_names = &cx.text_system().all_font_names();
 71        let settings_schema = cx.global::<SettingsStore>().json_schema(
 72            &SettingsJsonSchemaParams {
 73                language_names: &language_names,
 74                staff_mode,
 75                font_names,
 76            },
 77            cx,
 78        );
 79        let tasks_schema = task::TaskTemplates::generate_json_schema();
 80        let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
 81        serde_json::json!({
 82            "json": {
 83                "format": {
 84                    "enable": true,
 85                },
 86                "schemas": [
 87                    {
 88                        "fileMatch": ["tsconfig.json"],
 89                        "schema":tsconfig_schema
 90                    },
 91                    {
 92                        "fileMatch": [
 93                            schema_file_match(&paths::SETTINGS),
 94                            &*paths::LOCAL_SETTINGS_RELATIVE_PATH,
 95                        ],
 96                        "schema": settings_schema,
 97                    },
 98                    {
 99                        "fileMatch": [schema_file_match(&paths::KEYMAP)],
100                        "schema": KeymapFile::generate_json_schema(&action_names),
101                    },
102                    {
103                        "fileMatch": [
104                            schema_file_match(&paths::TASKS),
105                            &*paths::LOCAL_TASKS_RELATIVE_PATH,
106                        ],
107                        "schema": tasks_schema,
108                    }
109
110                ]
111            }
112        })
113    }
114}
115
116#[async_trait(?Send)]
117impl LspAdapter for JsonLspAdapter {
118    fn name(&self) -> LanguageServerName {
119        LanguageServerName("json-language-server".into())
120    }
121
122    async fn fetch_latest_server_version(
123        &self,
124        _: &dyn LspAdapterDelegate,
125    ) -> Result<Box<dyn 'static + Send + Any>> {
126        Ok(Box::new(
127            self.node
128                .npm_package_latest_version("vscode-json-languageserver")
129                .await?,
130        ) as Box<_>)
131    }
132
133    async fn fetch_server_binary(
134        &self,
135        latest_version: Box<dyn 'static + Send + Any>,
136        container_dir: PathBuf,
137        _: &dyn LspAdapterDelegate,
138    ) -> Result<LanguageServerBinary> {
139        let latest_version = latest_version.downcast::<String>().unwrap();
140        let server_path = container_dir.join(SERVER_PATH);
141        let package_name = "vscode-json-languageserver";
142
143        let should_install_language_server = self
144            .node
145            .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
146            .await;
147
148        if should_install_language_server {
149            self.node
150                .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
151                .await?;
152        }
153
154        Ok(LanguageServerBinary {
155            path: self.node.binary_path().await?,
156            env: None,
157            arguments: server_binary_arguments(&server_path),
158        })
159    }
160
161    async fn cached_server_binary(
162        &self,
163        container_dir: PathBuf,
164        _: &dyn LspAdapterDelegate,
165    ) -> Option<LanguageServerBinary> {
166        get_cached_server_binary(container_dir, &*self.node).await
167    }
168
169    async fn installation_test_binary(
170        &self,
171        container_dir: PathBuf,
172    ) -> Option<LanguageServerBinary> {
173        get_cached_server_binary(container_dir, &*self.node).await
174    }
175
176    async fn initialization_options(
177        self: Arc<Self>,
178        _: &Arc<dyn LspAdapterDelegate>,
179    ) -> Result<Option<serde_json::Value>> {
180        Ok(Some(json!({
181            "provideFormatter": true
182        })))
183    }
184
185    async fn workspace_configuration(
186        self: Arc<Self>,
187        _: &Arc<dyn LspAdapterDelegate>,
188        cx: &mut AsyncAppContext,
189    ) -> Result<Value> {
190        cx.update(|cx| {
191            self.workspace_config
192                .get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
193                .clone()
194        })
195    }
196
197    fn language_ids(&self) -> HashMap<String, String> {
198        [("JSON".into(), "jsonc".into())].into_iter().collect()
199    }
200}
201
202async fn get_cached_server_binary(
203    container_dir: PathBuf,
204    node: &dyn NodeRuntime,
205) -> Option<LanguageServerBinary> {
206    maybe!(async {
207        let mut last_version_dir = None;
208        let mut entries = fs::read_dir(&container_dir).await?;
209        while let Some(entry) = entries.next().await {
210            let entry = entry?;
211            if entry.file_type().await?.is_dir() {
212                last_version_dir = Some(entry.path());
213            }
214        }
215
216        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
217        let server_path = last_version_dir.join(SERVER_PATH);
218        if server_path.exists() {
219            Ok(LanguageServerBinary {
220                path: node.binary_path().await?,
221                env: None,
222                arguments: server_binary_arguments(&server_path),
223            })
224        } else {
225            Err(anyhow!(
226                "missing executable in directory {:?}",
227                last_version_dir
228            ))
229        }
230    })
231    .await
232    .log_err()
233}
234
235fn schema_file_match(path: &Path) -> &Path {
236    path.strip_prefix(path.parent().unwrap().parent().unwrap())
237        .unwrap()
238}