Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:37:31.817Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

c.rs

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