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