1use anyhow::{Context as _, Result, bail};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui::{App, AsyncApp};
5use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
6use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
7pub use language::*;
8use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName};
9use project::lsp_store::clangd_ext;
10use serde_json::json;
11use smol::fs;
12use std::{env::consts, path::PathBuf, sync::Arc};
13use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
14
15pub struct CLspAdapter;
16
17impl CLspAdapter {
18 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd");
19}
20
21impl LspInstaller for CLspAdapter {
22 type BinaryVersion = GitHubLspBinaryVersion;
23
24 async fn fetch_latest_server_version(
25 &self,
26 delegate: &dyn LspAdapterDelegate,
27 pre_release: bool,
28 _: &mut AsyncApp,
29 ) -> Result<GitHubLspBinaryVersion> {
30 let release =
31 latest_github_release("clangd/clangd", true, pre_release, delegate.http_client())
32 .await?;
33 let os_suffix = match consts::OS {
34 "macos" => "mac",
35 "linux" => "linux",
36 "windows" => "windows",
37 other => bail!("Running on unsupported os: {other}"),
38 };
39 let asset_name = format!("clangd-{}-{}.zip", os_suffix, release.tag_name);
40 let asset = release
41 .assets
42 .iter()
43 .find(|asset| asset.name == asset_name)
44 .with_context(|| format!("no asset found matching {asset_name:?}"))?;
45 let version = GitHubLspBinaryVersion {
46 name: release.tag_name,
47 url: asset.browser_download_url.clone(),
48 digest: asset.digest.clone(),
49 };
50 Ok(version)
51 }
52
53 async fn check_if_user_installed(
54 &self,
55 delegate: &dyn LspAdapterDelegate,
56 _: Option<Toolchain>,
57 _: &AsyncApp,
58 ) -> Option<LanguageServerBinary> {
59 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
60 Some(LanguageServerBinary {
61 path,
62 arguments: Vec::new(),
63 env: None,
64 })
65 }
66
67 async fn fetch_server_binary(
68 &self,
69 version: GitHubLspBinaryVersion,
70 container_dir: PathBuf,
71 delegate: &dyn LspAdapterDelegate,
72 ) -> Result<LanguageServerBinary> {
73 let GitHubLspBinaryVersion {
74 name,
75 url,
76 digest: expected_digest,
77 } = version;
78 let version_dir = container_dir.join(format!("clangd_{name}"));
79 let binary_path = version_dir.join("bin/clangd");
80
81 let binary = LanguageServerBinary {
82 path: binary_path.clone(),
83 env: None,
84 arguments: Default::default(),
85 };
86
87 let metadata_path = version_dir.join("metadata");
88 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
89 .await
90 .ok();
91 if let Some(metadata) = metadata {
92 let validity_check = async || {
93 delegate
94 .try_exec(LanguageServerBinary {
95 path: binary_path.clone(),
96 arguments: vec!["--version".into()],
97 env: None,
98 })
99 .await
100 .inspect_err(|err| {
101 log::warn!("Unable to run {binary_path:?} asset, redownloading: {err:#}",)
102 })
103 };
104 if let (Some(actual_digest), Some(expected_digest)) =
105 (&metadata.digest, &expected_digest)
106 {
107 if actual_digest == expected_digest {
108 if validity_check().await.is_ok() {
109 return Ok(binary);
110 }
111 } else {
112 log::info!(
113 "SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
114 );
115 }
116 } else if validity_check().await.is_ok() {
117 return Ok(binary);
118 }
119 }
120 download_server_binary(
121 &*delegate.http_client(),
122 &url,
123 expected_digest.as_deref(),
124 &container_dir,
125 AssetKind::Zip,
126 )
127 .await?;
128 remove_matching(&container_dir, |entry| entry != version_dir).await;
129 GithubBinaryMetadata::write_to_file(
130 &GithubBinaryMetadata {
131 metadata_version: 1,
132 digest: expected_digest,
133 },
134 &metadata_path,
135 )
136 .await?;
137
138 Ok(binary)
139 }
140
141 async fn cached_server_binary(
142 &self,
143 container_dir: PathBuf,
144 _: &dyn LspAdapterDelegate,
145 ) -> Option<LanguageServerBinary> {
146 get_cached_server_binary(container_dir).await
147 }
148}
149
150#[async_trait(?Send)]
151impl super::LspAdapter for CLspAdapter {
152 fn name(&self) -> LanguageServerName {
153 Self::SERVER_NAME
154 }
155
156 async fn label_for_completion(
157 &self,
158 completion: &lsp::CompletionItem,
159 language: &Arc<Language>,
160 ) -> Option<CodeLabel> {
161 let label_detail = match &completion.label_details {
162 Some(label_detail) => match &label_detail.detail {
163 Some(detail) => detail.trim(),
164 None => "",
165 },
166 None => "",
167 };
168
169 let mut label = completion
170 .label
171 .strip_prefix('•')
172 .unwrap_or(&completion.label)
173 .trim()
174 .to_owned();
175
176 if !label_detail.is_empty() {
177 let should_add_space = match completion.kind {
178 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) => false,
179 _ => true,
180 };
181
182 if should_add_space && !label.ends_with(' ') && !label_detail.starts_with(' ') {
183 label.push(' ');
184 }
185 label.push_str(label_detail);
186 }
187
188 match completion.kind {
189 Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
190 let detail = completion.detail.as_ref().unwrap();
191 let text = format!("{} {}", detail, label);
192 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
193 let runs = language.highlight_text(&source, 11..11 + text.len());
194 let filter_range = completion
195 .filter_text
196 .as_deref()
197 .and_then(|filter_text| {
198 text.find(filter_text)
199 .map(|start| start..start + filter_text.len())
200 })
201 .unwrap_or(detail.len() + 1..text.len());
202 return Some(CodeLabel::new(text, filter_range, runs));
203 }
204 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
205 if completion.detail.is_some() =>
206 {
207 let detail = completion.detail.as_ref().unwrap();
208 let text = format!("{} {}", detail, label);
209 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
210 let filter_range = completion
211 .filter_text
212 .as_deref()
213 .and_then(|filter_text| {
214 text.find(filter_text)
215 .map(|start| start..start + filter_text.len())
216 })
217 .unwrap_or(detail.len() + 1..text.len());
218 return Some(CodeLabel::new(text, filter_range, runs));
219 }
220 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
221 if completion.detail.is_some() =>
222 {
223 let detail = completion.detail.as_ref().unwrap();
224 let text = format!("{} {}", detail, label);
225 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
226 let filter_range = completion
227 .filter_text
228 .as_deref()
229 .and_then(|filter_text| {
230 text.find(filter_text)
231 .map(|start| start..start + filter_text.len())
232 })
233 .unwrap_or_else(|| {
234 let filter_start = detail.len() + 1;
235 let filter_end = text
236 .rfind('(')
237 .filter(|end| *end > filter_start)
238 .unwrap_or(text.len());
239 filter_start..filter_end
240 });
241
242 return Some(CodeLabel::new(text, filter_range, runs));
243 }
244 Some(kind) => {
245 let highlight_name = match kind {
246 lsp::CompletionItemKind::STRUCT
247 | lsp::CompletionItemKind::INTERFACE
248 | lsp::CompletionItemKind::CLASS
249 | lsp::CompletionItemKind::ENUM => Some("type"),
250 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
251 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
252 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
253 Some("constant")
254 }
255 _ => None,
256 };
257 if let Some(highlight_id) = language
258 .grammar()
259 .and_then(|g| g.highlight_id_for_name(highlight_name?))
260 {
261 let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
262 label.runs.push((
263 0..label.text.rfind('(').unwrap_or(label.text.len()),
264 highlight_id,
265 ));
266 return Some(label);
267 }
268 }
269 _ => {}
270 }
271 Some(CodeLabel::plain(label, completion.filter_text.as_deref()))
272 }
273
274 async fn label_for_symbol(
275 &self,
276 name: &str,
277 kind: lsp::SymbolKind,
278 language: &Arc<Language>,
279 ) -> Option<CodeLabel> {
280 let (text, filter_range, display_range) = match kind {
281 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
282 let text = format!("void {} () {{}}", name);
283 let filter_range = 0..name.len();
284 let display_range = 5..5 + name.len();
285 (text, filter_range, display_range)
286 }
287 lsp::SymbolKind::STRUCT => {
288 let text = format!("struct {} {{}}", name);
289 let filter_range = 7..7 + name.len();
290 let display_range = 0..filter_range.end;
291 (text, filter_range, display_range)
292 }
293 lsp::SymbolKind::ENUM => {
294 let text = format!("enum {} {{}}", name);
295 let filter_range = 5..5 + name.len();
296 let display_range = 0..filter_range.end;
297 (text, filter_range, display_range)
298 }
299 lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
300 let text = format!("class {} {{}}", name);
301 let filter_range = 6..6 + name.len();
302 let display_range = 0..filter_range.end;
303 (text, filter_range, display_range)
304 }
305 lsp::SymbolKind::CONSTANT => {
306 let text = format!("const int {} = 0;", name);
307 let filter_range = 10..10 + name.len();
308 let display_range = 0..filter_range.end;
309 (text, filter_range, display_range)
310 }
311 lsp::SymbolKind::MODULE => {
312 let text = format!("namespace {} {{}}", name);
313 let filter_range = 10..10 + name.len();
314 let display_range = 0..filter_range.end;
315 (text, filter_range, display_range)
316 }
317 lsp::SymbolKind::TYPE_PARAMETER => {
318 let text = format!("typename {} {{}};", name);
319 let filter_range = 9..9 + name.len();
320 let display_range = 0..filter_range.end;
321 (text, filter_range, display_range)
322 }
323 _ => return None,
324 };
325
326 Some(CodeLabel::new(
327 text[display_range.clone()].to_string(),
328 filter_range,
329 language.highlight_text(&text.as_str().into(), display_range),
330 ))
331 }
332
333 fn prepare_initialize_params(
334 &self,
335 mut original: InitializeParams,
336 _: &App,
337 ) -> Result<InitializeParams> {
338 let experimental = json!({
339 "textDocument": {
340 "completion" : {
341 // enable clangd's dot-to-arrow feature.
342 "editsNearCursor": true
343 },
344 "inactiveRegionsCapabilities": {
345 "inactiveRegions": true,
346 }
347 }
348 });
349 if let Some(ref mut original_experimental) = original.capabilities.experimental {
350 merge_json_value_into(experimental, original_experimental);
351 } else {
352 original.capabilities.experimental = Some(experimental);
353 }
354 Ok(original)
355 }
356
357 fn retain_old_diagnostic(&self, previous_diagnostic: &Diagnostic, _: &App) -> bool {
358 clangd_ext::is_inactive_region(previous_diagnostic)
359 }
360
361 fn underline_diagnostic(&self, diagnostic: &lsp::Diagnostic) -> bool {
362 !clangd_ext::is_lsp_inactive_region(diagnostic)
363 }
364}
365
366async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
367 maybe!(async {
368 let mut last_clangd_dir = None;
369 let mut entries = fs::read_dir(&container_dir).await?;
370 while let Some(entry) = entries.next().await {
371 let entry = entry?;
372 if entry.file_type().await?.is_dir() {
373 last_clangd_dir = Some(entry.path());
374 }
375 }
376 let clangd_dir = last_clangd_dir.context("no cached binary")?;
377 let clangd_bin = clangd_dir.join("bin/clangd");
378 anyhow::ensure!(
379 clangd_bin.exists(),
380 "missing clangd binary in directory {clangd_dir:?}"
381 );
382 Ok(LanguageServerBinary {
383 path: clangd_bin,
384 env: None,
385 arguments: Vec::new(),
386 })
387 })
388 .await
389 .log_err()
390}
391
392#[cfg(test)]
393mod tests {
394 use gpui::{AppContext as _, BorrowAppContext, TestAppContext};
395 use language::{AutoindentMode, Buffer};
396 use settings::SettingsStore;
397 use std::num::NonZeroU32;
398 use unindent::Unindent;
399
400 #[gpui::test]
401 async fn test_c_autoindent_basic(cx: &mut TestAppContext) {
402 cx.update(|cx| {
403 let test_settings = SettingsStore::test(cx);
404 cx.set_global(test_settings);
405 cx.update_global::<SettingsStore, _>(|store, cx| {
406 store.update_user_settings(cx, |s| {
407 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
408 });
409 });
410 });
411 let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
412
413 cx.new(|cx| {
414 let mut buffer = Buffer::local("", cx).with_language(language, cx);
415
416 buffer.edit([(0..0, "int main() {}")], None, cx);
417
418 let ix = buffer.len() - 1;
419 buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
420 assert_eq!(
421 buffer.text(),
422 "int main() {\n \n}",
423 "content inside braces should be indented"
424 );
425
426 buffer
427 });
428 }
429
430 #[gpui::test]
431 async fn test_c_autoindent_if_else(cx: &mut TestAppContext) {
432 cx.update(|cx| {
433 let test_settings = SettingsStore::test(cx);
434 cx.set_global(test_settings);
435 language::init(cx);
436 cx.update_global::<SettingsStore, _>(|store, cx| {
437 store.update_user_settings(cx, |s| {
438 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
439 });
440 });
441 });
442 let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
443
444 cx.new(|cx| {
445 let mut buffer = Buffer::local("", cx).with_language(language, cx);
446
447 buffer.edit(
448 [(
449 0..0,
450 r#"
451 int main() {
452 if (a)
453 b;
454 }
455 "#
456 .unindent(),
457 )],
458 Some(AutoindentMode::EachLine),
459 cx,
460 );
461 assert_eq!(
462 buffer.text(),
463 r#"
464 int main() {
465 if (a)
466 b;
467 }
468 "#
469 .unindent(),
470 "body of if-statement without braces should be indented"
471 );
472
473 let ix = buffer.len() - 4;
474 buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
475 assert_eq!(
476 buffer.text(),
477 r#"
478 int main() {
479 if (a)
480 b
481 .c;
482 }
483 "#
484 .unindent(),
485 "field expression (.c) should be indented further than the statement body"
486 );
487
488 buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
489 buffer.edit(
490 [(
491 0..0,
492 r#"
493 int main() {
494 if (a) a++;
495 else b++;
496 }
497 "#
498 .unindent(),
499 )],
500 Some(AutoindentMode::EachLine),
501 cx,
502 );
503 assert_eq!(
504 buffer.text(),
505 r#"
506 int main() {
507 if (a) a++;
508 else b++;
509 }
510 "#
511 .unindent(),
512 "single-line if/else without braces should align at the same level"
513 );
514
515 buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
516 buffer.edit(
517 [(
518 0..0,
519 r#"
520 int main() {
521 if (a)
522 b++;
523 else
524 c++;
525 }
526 "#
527 .unindent(),
528 )],
529 Some(AutoindentMode::EachLine),
530 cx,
531 );
532 assert_eq!(
533 buffer.text(),
534 r#"
535 int main() {
536 if (a)
537 b++;
538 else
539 c++;
540 }
541 "#
542 .unindent(),
543 "multi-line if/else without braces should indent statement bodies"
544 );
545
546 buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
547 buffer.edit(
548 [(
549 0..0,
550 r#"
551 int main() {
552 if (a)
553 if (b)
554 c++;
555 }
556 "#
557 .unindent(),
558 )],
559 Some(AutoindentMode::EachLine),
560 cx,
561 );
562 assert_eq!(
563 buffer.text(),
564 r#"
565 int main() {
566 if (a)
567 if (b)
568 c++;
569 }
570 "#
571 .unindent(),
572 "nested if statements without braces should indent properly"
573 );
574
575 buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
576 buffer.edit(
577 [(
578 0..0,
579 r#"
580 int main() {
581 if (a)
582 b++;
583 else if (c)
584 d++;
585 else
586 f++;
587 }
588 "#
589 .unindent(),
590 )],
591 Some(AutoindentMode::EachLine),
592 cx,
593 );
594 assert_eq!(
595 buffer.text(),
596 r#"
597 int main() {
598 if (a)
599 b++;
600 else if (c)
601 d++;
602 else
603 f++;
604 }
605 "#
606 .unindent(),
607 "else-if chains should align all conditions at same level with indented bodies"
608 );
609
610 buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
611 buffer.edit(
612 [(
613 0..0,
614 r#"
615 int main() {
616 if (a) {
617 b++;
618 } else
619 c++;
620 }
621 "#
622 .unindent(),
623 )],
624 Some(AutoindentMode::EachLine),
625 cx,
626 );
627 assert_eq!(
628 buffer.text(),
629 r#"
630 int main() {
631 if (a) {
632 b++;
633 } else
634 c++;
635 }
636 "#
637 .unindent(),
638 "mixed braces should indent properly"
639 );
640
641 buffer
642 });
643 }
644}