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}