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}