1use anyhow::{anyhow, bail, Context, Result};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use collections::HashMap;
6use futures::StreamExt;
7use gpui::{App, AsyncApp};
8use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
9use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
10use lsp::{LanguageServerBinary, LanguageServerName};
11use node_runtime::NodeRuntime;
12use project::{lsp_store::language_server_settings, ContextProviderWithTasks, Fs};
13use serde_json::{json, Value};
14use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
15use smol::{
16 fs::{self},
17 io::BufReader,
18};
19use std::{
20 any::Any,
21 env::consts,
22 ffi::OsString,
23 path::{Path, PathBuf},
24 str::FromStr,
25 sync::{Arc, OnceLock},
26};
27use task::{TaskTemplate, TaskTemplates, VariableName};
28use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt};
29
30const SERVER_PATH: &str =
31 "node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
32
33// Origin: https://github.com/SchemaStore/schemastore
34const TSCONFIG_SCHEMA: &str = include_str!("json/schemas/tsconfig.json");
35const PACKAGE_JSON_SCHEMA: &str = include_str!("json/schemas/package.json");
36
37pub(super) fn json_task_context() -> ContextProviderWithTasks {
38 ContextProviderWithTasks::new(TaskTemplates(vec![
39 TaskTemplate {
40 label: "package script $ZED_CUSTOM_script".to_owned(),
41 command: "npm --prefix $ZED_DIRNAME run".to_owned(),
42 args: vec![VariableName::Custom("script".into()).template_value()],
43 tags: vec!["package-script".into()],
44 ..TaskTemplate::default()
45 },
46 TaskTemplate {
47 label: "composer script $ZED_CUSTOM_script".to_owned(),
48 command: "composer -d $ZED_DIRNAME".to_owned(),
49 args: vec![VariableName::Custom("script".into()).template_value()],
50 tags: vec!["composer-script".into()],
51 ..TaskTemplate::default()
52 },
53 ]))
54}
55
56fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
57 vec![server_path.into(), "--stdio".into()]
58}
59
60pub struct JsonLspAdapter {
61 node: NodeRuntime,
62 languages: Arc<LanguageRegistry>,
63 workspace_config: OnceLock<Value>,
64}
65
66impl JsonLspAdapter {
67 const PACKAGE_NAME: &str = "vscode-langservers-extracted";
68
69 pub fn new(node: NodeRuntime, languages: Arc<LanguageRegistry>) -> Self {
70 Self {
71 node,
72 languages,
73 workspace_config: Default::default(),
74 }
75 }
76
77 fn get_workspace_config(language_names: Vec<String>, cx: &mut App) -> Value {
78 let keymap_schema = KeymapFile::generate_json_schema_for_registered_actions(cx);
79 let font_names = &cx.text_system().all_font_names();
80 let settings_schema = cx.global::<SettingsStore>().json_schema(
81 &SettingsJsonSchemaParams {
82 language_names: &language_names,
83 font_names,
84 },
85 cx,
86 );
87 let tasks_schema = task::TaskTemplates::generate_json_schema();
88 let snippets_schema = snippet_provider::format::VSSnippetsFile::generate_json_schema();
89 let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
90 let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
91
92 // This can be viewed via `debug: open language server logs` -> `json-language-server` ->
93 // `Server Info`
94 serde_json::json!({
95 "json": {
96 "format": {
97 "enable": true,
98 },
99 "validate":
100 {
101 "enable": true,
102 },
103 "schemas": [
104 {
105 "fileMatch": ["tsconfig.json"],
106 "schema":tsconfig_schema
107 },
108 {
109 "fileMatch": ["package.json"],
110 "schema":package_json_schema
111 },
112 {
113 "fileMatch": [
114 schema_file_match(paths::settings_file()),
115 paths::local_settings_file_relative_path()
116 ],
117 "schema": settings_schema,
118 },
119 {
120 "fileMatch": [schema_file_match(paths::keymap_file())],
121 "schema": keymap_schema,
122 },
123 {
124 "fileMatch": [
125 schema_file_match(paths::tasks_file()),
126 paths::local_tasks_file_relative_path()
127 ],
128 "schema": tasks_schema,
129 },
130 {
131 "fileMatch": [
132 schema_file_match(
133 paths::snippets_dir()
134 .join("*.json")
135 .as_path()
136 )
137 ],
138 "schema": snippets_schema,
139 }
140 ]
141 }
142 })
143 }
144}
145
146#[async_trait(?Send)]
147impl LspAdapter for JsonLspAdapter {
148 fn name(&self) -> LanguageServerName {
149 LanguageServerName("json-language-server".into())
150 }
151
152 async fn fetch_latest_server_version(
153 &self,
154 _: &dyn LspAdapterDelegate,
155 ) -> Result<Box<dyn 'static + Send + Any>> {
156 Ok(Box::new(
157 self.node
158 .npm_package_latest_version(Self::PACKAGE_NAME)
159 .await?,
160 ) as Box<_>)
161 }
162
163 async fn check_if_version_installed(
164 &self,
165 version: &(dyn 'static + Send + Any),
166 container_dir: &PathBuf,
167 _: &dyn LspAdapterDelegate,
168 ) -> Option<LanguageServerBinary> {
169 let version = version.downcast_ref::<String>().unwrap();
170 let server_path = container_dir.join(SERVER_PATH);
171
172 let should_install_language_server = self
173 .node
174 .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
175 .await;
176
177 if should_install_language_server {
178 None
179 } else {
180 Some(LanguageServerBinary {
181 path: self.node.binary_path().await.ok()?,
182 env: None,
183 arguments: server_binary_arguments(&server_path),
184 })
185 }
186 }
187
188 async fn fetch_server_binary(
189 &self,
190 latest_version: Box<dyn 'static + Send + Any>,
191 container_dir: PathBuf,
192 _: &dyn LspAdapterDelegate,
193 ) -> Result<LanguageServerBinary> {
194 let latest_version = latest_version.downcast::<String>().unwrap();
195 let server_path = container_dir.join(SERVER_PATH);
196
197 self.node
198 .npm_install_packages(
199 &container_dir,
200 &[(Self::PACKAGE_NAME, latest_version.as_str())],
201 )
202 .await?;
203
204 Ok(LanguageServerBinary {
205 path: self.node.binary_path().await?,
206 env: None,
207 arguments: server_binary_arguments(&server_path),
208 })
209 }
210
211 async fn cached_server_binary(
212 &self,
213 container_dir: PathBuf,
214 _: &dyn LspAdapterDelegate,
215 ) -> Option<LanguageServerBinary> {
216 get_cached_server_binary(container_dir, &self.node).await
217 }
218
219 async fn initialization_options(
220 self: Arc<Self>,
221 _: &dyn Fs,
222 _: &Arc<dyn LspAdapterDelegate>,
223 ) -> Result<Option<serde_json::Value>> {
224 Ok(Some(json!({
225 "provideFormatter": true
226 })))
227 }
228
229 async fn workspace_configuration(
230 self: Arc<Self>,
231 _: &dyn Fs,
232 delegate: &Arc<dyn LspAdapterDelegate>,
233 _: Arc<dyn LanguageToolchainStore>,
234 cx: &mut AsyncApp,
235 ) -> Result<Value> {
236 let mut config = cx.update(|cx| {
237 self.workspace_config
238 .get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
239 .clone()
240 })?;
241
242 let project_options = cx.update(|cx| {
243 language_server_settings(delegate.as_ref(), &self.name(), cx)
244 .and_then(|s| s.settings.clone())
245 })?;
246
247 if let Some(override_options) = project_options {
248 merge_json_value_into(override_options, &mut config);
249 }
250
251 Ok(config)
252 }
253
254 fn language_ids(&self) -> HashMap<String, String> {
255 [
256 ("JSON".into(), "json".into()),
257 ("JSONC".into(), "jsonc".into()),
258 ]
259 .into_iter()
260 .collect()
261 }
262}
263
264async fn get_cached_server_binary(
265 container_dir: PathBuf,
266 node: &NodeRuntime,
267) -> Option<LanguageServerBinary> {
268 maybe!(async {
269 let mut last_version_dir = None;
270 let mut entries = fs::read_dir(&container_dir).await?;
271 while let Some(entry) = entries.next().await {
272 let entry = entry?;
273 if entry.file_type().await?.is_dir() {
274 last_version_dir = Some(entry.path());
275 }
276 }
277
278 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
279 let server_path = last_version_dir.join(SERVER_PATH);
280 if server_path.exists() {
281 Ok(LanguageServerBinary {
282 path: node.binary_path().await?,
283 env: None,
284 arguments: server_binary_arguments(&server_path),
285 })
286 } else {
287 Err(anyhow!(
288 "missing executable in directory {:?}",
289 last_version_dir
290 ))
291 }
292 })
293 .await
294 .log_err()
295}
296
297#[inline]
298fn schema_file_match(path: &Path) -> String {
299 path.strip_prefix(path.parent().unwrap().parent().unwrap())
300 .unwrap()
301 .display()
302 .to_string()
303 .replace('\\', "/")
304}
305
306pub(super) struct NodeVersionAdapter;
307
308#[async_trait(?Send)]
309impl LspAdapter for NodeVersionAdapter {
310 fn name(&self) -> LanguageServerName {
311 LanguageServerName("package-version-server".into())
312 }
313
314 async fn fetch_latest_server_version(
315 &self,
316 delegate: &dyn LspAdapterDelegate,
317 ) -> Result<Box<dyn 'static + Send + Any>> {
318 let release = latest_github_release(
319 "zed-industries/package-version-server",
320 true,
321 false,
322 delegate.http_client(),
323 )
324 .await?;
325 let os = match consts::OS {
326 "macos" => "apple-darwin",
327 "linux" => "unknown-linux-gnu",
328 "windows" => "pc-windows-msvc",
329 other => bail!("Running on unsupported os: {other}"),
330 };
331 let suffix = if consts::OS == "windows" {
332 ".zip"
333 } else {
334 ".tar.gz"
335 };
336 let asset_name = format!("package-version-server-{}-{os}{suffix}", consts::ARCH);
337 let asset = release
338 .assets
339 .iter()
340 .find(|asset| asset.name == asset_name)
341 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
342 Ok(Box::new(GitHubLspBinaryVersion {
343 name: release.tag_name,
344 url: asset.browser_download_url.clone(),
345 }))
346 }
347
348 async fn fetch_server_binary(
349 &self,
350 latest_version: Box<dyn 'static + Send + Any>,
351 container_dir: PathBuf,
352 delegate: &dyn LspAdapterDelegate,
353 ) -> Result<LanguageServerBinary> {
354 let version = latest_version.downcast::<GitHubLspBinaryVersion>().unwrap();
355 let destination_path = container_dir.join(format!(
356 "package-version-server-{}{}",
357 version.name,
358 std::env::consts::EXE_SUFFIX
359 ));
360 let destination_container_path =
361 container_dir.join(format!("package-version-server-{}-tmp", version.name));
362 if fs::metadata(&destination_path).await.is_err() {
363 let mut response = delegate
364 .http_client()
365 .get(&version.url, Default::default(), true)
366 .await
367 .map_err(|err| anyhow!("error downloading release: {}", err))?;
368 if version.url.ends_with(".zip") {
369 node_runtime::extract_zip(
370 &destination_container_path,
371 BufReader::new(response.body_mut()),
372 )
373 .await?;
374 } else if version.url.ends_with(".tar.gz") {
375 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
376 let archive = Archive::new(decompressed_bytes);
377 archive.unpack(&destination_container_path).await?;
378 }
379
380 fs::copy(
381 destination_container_path.join(format!(
382 "package-version-server{}",
383 std::env::consts::EXE_SUFFIX
384 )),
385 &destination_path,
386 )
387 .await?;
388 // todo("windows")
389 #[cfg(not(windows))]
390 {
391 fs::set_permissions(
392 &destination_path,
393 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
394 )
395 .await?;
396 }
397 remove_matching(&container_dir, |entry| entry != destination_path).await;
398 }
399 Ok(LanguageServerBinary {
400 path: destination_path,
401 env: None,
402 arguments: Default::default(),
403 })
404 }
405
406 async fn cached_server_binary(
407 &self,
408 container_dir: PathBuf,
409 _delegate: &dyn LspAdapterDelegate,
410 ) -> Option<LanguageServerBinary> {
411 get_cached_version_server_binary(container_dir).await
412 }
413}
414
415async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
416 maybe!(async {
417 let mut last = None;
418 let mut entries = fs::read_dir(&container_dir).await?;
419 while let Some(entry) = entries.next().await {
420 last = Some(entry?.path());
421 }
422
423 anyhow::Ok(LanguageServerBinary {
424 path: last.ok_or_else(|| anyhow!("no cached binary"))?,
425 env: None,
426 arguments: Default::default(),
427 })
428 })
429 .await
430 .log_err()
431}