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        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            cx.update_global::<SettingsStore, _>(|store, cx| {
436                store.update_user_settings(cx, |s| {
437                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
438                });
439            });
440        });
441        let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
442
443        cx.new(|cx| {
444            let mut buffer = Buffer::local("", cx).with_language(language, cx);
445
446            buffer.edit(
447                [(
448                    0..0,
449                    r#"
450                    int main() {
451                    if (a)
452                    b;
453                    }
454                    "#
455                    .unindent(),
456                )],
457                Some(AutoindentMode::EachLine),
458                cx,
459            );
460            assert_eq!(
461                buffer.text(),
462                r#"
463                int main() {
464                  if (a)
465                    b;
466                }
467                "#
468                .unindent(),
469                "body of if-statement without braces should be indented"
470            );
471
472            let ix = buffer.len() - 4;
473            buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
474            assert_eq!(
475                buffer.text(),
476                r#"
477                int main() {
478                  if (a)
479                    b
480                      .c;
481                }
482                "#
483                .unindent(),
484                "field expression (.c) should be indented further than the statement body"
485            );
486
487            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
488            buffer.edit(
489                [(
490                    0..0,
491                    r#"
492                    int main() {
493                    if (a) a++;
494                    else b++;
495                    }
496                    "#
497                    .unindent(),
498                )],
499                Some(AutoindentMode::EachLine),
500                cx,
501            );
502            assert_eq!(
503                buffer.text(),
504                r#"
505                int main() {
506                  if (a) a++;
507                  else b++;
508                }
509                "#
510                .unindent(),
511                "single-line if/else without braces should align at the same level"
512            );
513
514            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
515            buffer.edit(
516                [(
517                    0..0,
518                    r#"
519                    int main() {
520                    if (a)
521                    b++;
522                    else
523                    c++;
524                    }
525                    "#
526                    .unindent(),
527                )],
528                Some(AutoindentMode::EachLine),
529                cx,
530            );
531            assert_eq!(
532                buffer.text(),
533                r#"
534                int main() {
535                  if (a)
536                    b++;
537                  else
538                    c++;
539                }
540                "#
541                .unindent(),
542                "multi-line if/else without braces should indent statement bodies"
543            );
544
545            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
546            buffer.edit(
547                [(
548                    0..0,
549                    r#"
550                    int main() {
551                    if (a)
552                    if (b)
553                    c++;
554                    }
555                    "#
556                    .unindent(),
557                )],
558                Some(AutoindentMode::EachLine),
559                cx,
560            );
561            assert_eq!(
562                buffer.text(),
563                r#"
564                int main() {
565                  if (a)
566                    if (b)
567                      c++;
568                }
569                "#
570                .unindent(),
571                "nested if statements without braces should indent properly"
572            );
573
574            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
575            buffer.edit(
576                [(
577                    0..0,
578                    r#"
579                    int main() {
580                    if (a)
581                    b++;
582                    else if (c)
583                    d++;
584                    else
585                    f++;
586                    }
587                    "#
588                    .unindent(),
589                )],
590                Some(AutoindentMode::EachLine),
591                cx,
592            );
593            assert_eq!(
594                buffer.text(),
595                r#"
596                int main() {
597                  if (a)
598                    b++;
599                  else if (c)
600                    d++;
601                  else
602                    f++;
603                }
604                "#
605                .unindent(),
606                "else-if chains should align all conditions at same level with indented bodies"
607            );
608
609            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
610            buffer.edit(
611                [(
612                    0..0,
613                    r#"
614                    int main() {
615                    if (a) {
616                    b++;
617                    } else
618                    c++;
619                    }
620                    "#
621                    .unindent(),
622                )],
623                Some(AutoindentMode::EachLine),
624                cx,
625            );
626            assert_eq!(
627                buffer.text(),
628                r#"
629                int main() {
630                  if (a) {
631                    b++;
632                  } else
633                    c++;
634                }
635                "#
636                .unindent(),
637                "mixed braces should indent properly"
638            );
639
640            buffer
641        });
642    }
643}