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