Skip to repository content732 lines · 25.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:38:55.812Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
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, 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