c.rs

  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}