1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::{future::BoxFuture, FutureExt, StreamExt};
5use gpui::MutableAppContext;
6use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
7use node_runtime::NodeRuntime;
8use serde_json::json;
9use settings::{keymap_file_json_schema, settings_file_json_schema};
10use smol::fs;
11use std::{
12 any::Any,
13 ffi::OsString,
14 future,
15 path::{Path, PathBuf},
16 sync::Arc,
17};
18use theme::ThemeRegistry;
19use util::{fs::remove_matching, http::HttpClient};
20use util::{paths, ResultExt, StaffMode};
21
22const SERVER_PATH: &'static str =
23 "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
24
25fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
26 vec![server_path.into(), "--stdio".into()]
27}
28
29pub struct JsonLspAdapter {
30 node: Arc<NodeRuntime>,
31 languages: Arc<LanguageRegistry>,
32 themes: Arc<ThemeRegistry>,
33}
34
35impl JsonLspAdapter {
36 pub fn new(
37 node: Arc<NodeRuntime>,
38 languages: Arc<LanguageRegistry>,
39 themes: Arc<ThemeRegistry>,
40 ) -> Self {
41 JsonLspAdapter {
42 node,
43 languages,
44 themes,
45 }
46 }
47}
48
49#[async_trait]
50impl LspAdapter for JsonLspAdapter {
51 async fn name(&self) -> LanguageServerName {
52 LanguageServerName("json-language-server".into())
53 }
54
55 async fn fetch_latest_server_version(
56 &self,
57 _: Arc<dyn HttpClient>,
58 ) -> Result<Box<dyn 'static + Send + Any>> {
59 Ok(Box::new(
60 self.node
61 .npm_package_latest_version("vscode-json-languageserver")
62 .await?,
63 ) as Box<_>)
64 }
65
66 async fn fetch_server_binary(
67 &self,
68 version: Box<dyn 'static + Send + Any>,
69 _: Arc<dyn HttpClient>,
70 container_dir: PathBuf,
71 ) -> Result<LanguageServerBinary> {
72 let version = version.downcast::<String>().unwrap();
73 let version_dir = container_dir.join(version.as_str());
74 fs::create_dir_all(&version_dir)
75 .await
76 .context("failed to create version directory")?;
77 let server_path = version_dir.join(SERVER_PATH);
78
79 if fs::metadata(&server_path).await.is_err() {
80 self.node
81 .npm_install_packages(
82 [("vscode-json-languageserver", version.as_str())],
83 &version_dir,
84 )
85 .await?;
86
87 remove_matching(&container_dir, |entry| entry != server_path).await;
88 }
89
90 Ok(LanguageServerBinary {
91 path: self.node.binary_path().await?,
92 arguments: server_binary_arguments(&server_path),
93 })
94 }
95
96 async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
97 (|| async move {
98 let mut last_version_dir = None;
99 let mut entries = fs::read_dir(&container_dir).await?;
100 while let Some(entry) = entries.next().await {
101 let entry = entry?;
102 if entry.file_type().await?.is_dir() {
103 last_version_dir = Some(entry.path());
104 }
105 }
106
107 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
108 let server_path = last_version_dir.join(SERVER_PATH);
109 if server_path.exists() {
110 Ok(LanguageServerBinary {
111 path: self.node.binary_path().await?,
112 arguments: server_binary_arguments(&server_path),
113 })
114 } else {
115 Err(anyhow!(
116 "missing executable in directory {:?}",
117 last_version_dir
118 ))
119 }
120 })()
121 .await
122 .log_err()
123 }
124
125 async fn initialization_options(&self) -> Option<serde_json::Value> {
126 Some(json!({
127 "provideFormatter": true
128 }))
129 }
130
131 fn workspace_configuration(
132 &self,
133 cx: &mut MutableAppContext,
134 ) -> Option<BoxFuture<'static, serde_json::Value>> {
135 let action_names = cx.all_action_names().collect::<Vec<_>>();
136 let theme_names = self
137 .themes
138 .list(**cx.default_global::<StaffMode>())
139 .map(|meta| meta.name)
140 .collect();
141 let language_names = self.languages.language_names();
142 Some(
143 future::ready(serde_json::json!({
144 "json": {
145 "format": {
146 "enable": true,
147 },
148 "schemas": [
149 {
150 "fileMatch": [schema_file_match(&paths::SETTINGS)],
151 "schema": settings_file_json_schema(theme_names, &language_names),
152 },
153 {
154 "fileMatch": [schema_file_match(&paths::KEYMAP)],
155 "schema": keymap_file_json_schema(&action_names),
156 }
157 ]
158 }
159 }))
160 .boxed(),
161 )
162 }
163
164 async fn language_ids(&self) -> HashMap<String, String> {
165 [("JSON".into(), "jsonc".into())].into_iter().collect()
166 }
167}
168
169fn schema_file_match(path: &Path) -> &Path {
170 path.strip_prefix(path.parent().unwrap().parent().unwrap())
171 .unwrap()
172}