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