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