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": [
 89                            schema_file_match(&paths::SETTINGS),
 90                            &*paths::LOCAL_SETTINGS_RELATIVE_PATH,
 91                        ],
 92                        "schema": settings_schema,
 93                    },
 94                    {
 95                        "fileMatch": [schema_file_match(&paths::KEYMAP)],
 96                        "schema": KeymapFile::generate_json_schema(&action_names),
 97                    },
 98                    {
 99                        "fileMatch": [
100                            schema_file_match(&paths::TASKS),
101                            &*paths::LOCAL_TASKS_RELATIVE_PATH,
102                        ],
103                        "schema": tasks_schema,
104                    },
105                    {
106                        "fileMatch": "*/tsconfig.json",
107                        "schema":tsconfig_schema
108                    }
109                ]
110            }
111        })
112    }
113}
114
115#[async_trait(?Send)]
116impl LspAdapter for JsonLspAdapter {
117    fn name(&self) -> LanguageServerName {
118        LanguageServerName("json-language-server".into())
119    }
120
121    async fn fetch_latest_server_version(
122        &self,
123        _: &dyn LspAdapterDelegate,
124    ) -> Result<Box<dyn 'static + Send + Any>> {
125        Ok(Box::new(
126            self.node
127                .npm_package_latest_version("vscode-json-languageserver")
128                .await?,
129        ) as Box<_>)
130    }
131
132    async fn fetch_server_binary(
133        &self,
134        latest_version: Box<dyn 'static + Send + Any>,
135        container_dir: PathBuf,
136        _: &dyn LspAdapterDelegate,
137    ) -> Result<LanguageServerBinary> {
138        let latest_version = latest_version.downcast::<String>().unwrap();
139        let server_path = container_dir.join(SERVER_PATH);
140        let package_name = "vscode-json-languageserver";
141
142        let should_install_language_server = self
143            .node
144            .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
145            .await;
146
147        if should_install_language_server {
148            self.node
149                .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
150                .await?;
151        }
152
153        Ok(LanguageServerBinary {
154            path: self.node.binary_path().await?,
155            env: None,
156            arguments: server_binary_arguments(&server_path),
157        })
158    }
159
160    async fn cached_server_binary(
161        &self,
162        container_dir: PathBuf,
163        _: &dyn LspAdapterDelegate,
164    ) -> Option<LanguageServerBinary> {
165        get_cached_server_binary(container_dir, &*self.node).await
166    }
167
168    async fn installation_test_binary(
169        &self,
170        container_dir: PathBuf,
171    ) -> Option<LanguageServerBinary> {
172        get_cached_server_binary(container_dir, &*self.node).await
173    }
174
175    async fn initialization_options(
176        self: Arc<Self>,
177        _: &Arc<dyn LspAdapterDelegate>,
178    ) -> Result<Option<serde_json::Value>> {
179        Ok(Some(json!({
180            "provideFormatter": true
181        })))
182    }
183
184    async fn workspace_configuration(
185        self: Arc<Self>,
186        _: &Arc<dyn LspAdapterDelegate>,
187        cx: &mut AsyncAppContext,
188    ) -> Result<Value> {
189        cx.update(|cx| {
190            self.workspace_config
191                .get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
192                .clone()
193        })
194    }
195
196    fn language_ids(&self) -> HashMap<String, String> {
197        [("JSON".into(), "jsonc".into())].into_iter().collect()
198    }
199}
200
201async fn get_cached_server_binary(
202    container_dir: PathBuf,
203    node: &dyn NodeRuntime,
204) -> Option<LanguageServerBinary> {
205    maybe!(async {
206        let mut last_version_dir = None;
207        let mut entries = fs::read_dir(&container_dir).await?;
208        while let Some(entry) = entries.next().await {
209            let entry = entry?;
210            if entry.file_type().await?.is_dir() {
211                last_version_dir = Some(entry.path());
212            }
213        }
214
215        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
216        let server_path = last_version_dir.join(SERVER_PATH);
217        if server_path.exists() {
218            Ok(LanguageServerBinary {
219                path: node.binary_path().await?,
220                env: None,
221                arguments: server_binary_arguments(&server_path),
222            })
223        } else {
224            Err(anyhow!(
225                "missing executable in directory {:?}",
226                last_version_dir
227            ))
228        }
229    })
230    .await
231    .log_err()
232}
233
234fn schema_file_match(path: &Path) -> &Path {
235    path.strip_prefix(path.parent().unwrap().parent().unwrap())
236        .unwrap()
237}