1use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Modifier, Style},
7 text::{Line, Span},
8 widgets::{
9 block::BorderType, Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation,
10 ScrollbarState, StatefulWidget, Widget, Wrap,
11 },
12};
13
14use crate::analyzer::{
15 AnalyzedItem, ConstInfo, EnumInfo, FunctionInfo, ImplInfo, ModuleInfo, StaticInfo, StructInfo,
16 StructKind, TraitInfo, TypeAliasInfo, VariantFields, Visibility,
17};
18use crate::ui::theme::Theme;
19
20pub struct InspectorPanel<'a> {
22 item: Option<&'a AnalyzedItem>,
23 all_items: Option<&'a [AnalyzedItem]>,
25 theme: &'a Theme,
26 focused: bool,
27 scroll_offset: usize,
28}
29
30impl<'a> InspectorPanel<'a> {
31 pub fn new(theme: &'a Theme) -> Self {
32 Self {
33 item: None,
34 all_items: None,
35 theme,
36 focused: false,
37 scroll_offset: 0,
38 }
39 }
40
41 pub fn item(mut self, item: Option<&'a AnalyzedItem>) -> Self {
42 self.item = item;
43 self
44 }
45
46 pub fn all_items(mut self, items: Option<&'a [AnalyzedItem]>) -> Self {
47 self.all_items = items;
48 self
49 }
50
51 pub fn focused(mut self, focused: bool) -> Self {
52 self.focused = focused;
53 self
54 }
55
56 pub fn scroll(mut self, offset: usize) -> Self {
57 self.scroll_offset = offset;
58 self
59 }
60
61 fn section_header(&self, title: &str) -> Line<'static> {
62 Line::from(vec![
63 Span::styled("▸ ", self.theme.style_accent()),
64 Span::styled(
65 title.to_string(),
66 self.theme.style_accent().add_modifier(Modifier::BOLD),
67 ),
68 Span::styled(" ─────────────────", self.theme.style_muted()),
69 ])
70 }
71
72 fn key_value(&self, key: &str, value: String) -> Line<'static> {
73 Line::from(vec![
74 Span::styled(format!(" {} ", key), self.theme.style_dim()),
75 Span::styled(value, self.theme.style_normal()),
76 ])
77 }
78
79 fn badge(&self, text: &str, is_warning: bool) -> Span<'static> {
80 let style = if is_warning {
81 self.theme.style_error().add_modifier(Modifier::BOLD)
82 } else {
83 self.theme.style_keyword()
84 };
85 Span::styled(format!(" [{}] ", text), style)
86 }
87
88 fn render_empty(&self, area: Rect, buf: &mut Buffer) {
89 let block = Block::default()
90 .borders(Borders::ALL)
91 .border_type(BorderType::Rounded)
92 .border_style(self.theme.style_border())
93 .style(Style::default().bg(self.theme.bg_panel))
94 .title(" ◇ Inspector ");
95
96 let inner = block.inner(area);
97 block.render(area, buf);
98
99 let help_text = vec![
100 Line::from(""),
101 Line::from(Span::styled(" No item selected", self.theme.style_muted())),
102 Line::from(""),
103 Line::from(" Select an item from the list to view"),
104 Line::from(" detailed API information."),
105 Line::from(""),
106 self.section_header("Navigation"),
107 Line::from(""),
108 Line::from(vec![
109 Span::raw(" "),
110 Span::styled("↑/↓ j/k", self.theme.style_accent()),
111 Span::raw(" Navigate list"),
112 ]),
113 Line::from(vec![
114 Span::raw(" "),
115 Span::styled("Enter/→", self.theme.style_accent()),
116 Span::raw(" View details"),
117 ]),
118 Line::from(vec![
119 Span::raw(" "),
120 Span::styled("/", self.theme.style_accent()),
121 Span::raw(" Search"),
122 ]),
123 Line::from(vec![
124 Span::raw(" "),
125 Span::styled("1-4", self.theme.style_accent()),
126 Span::raw(" Switch tabs"),
127 ]),
128 ];
129
130 Paragraph::new(help_text)
131 .wrap(Wrap { trim: false })
132 .render(inner, buf);
133 }
134
135 fn render_function(&self, func: &FunctionInfo, area: Rect, buf: &mut Buffer) {
136 let mut lines = Vec::new();
137
138 let mut header = vec![
140 Span::styled("fn ", self.theme.style_keyword()),
141 Span::styled(
142 func.name.clone(),
143 self.theme
144 .style_accent_bold()
145 .add_modifier(Modifier::UNDERLINED),
146 ),
147 ];
148
149 if func.is_async {
150 header.push(self.badge("async", false));
151 }
152 if func.is_const {
153 header.push(self.badge("const", false));
154 }
155 if func.is_unsafe {
156 header.push(self.badge("unsafe", true));
157 }
158
159 lines.push(Line::from(header));
160
161 if !func.module_path.is_empty() {
163 lines.push(Line::from(vec![
164 Span::styled(" use ", self.theme.style_keyword()),
165 Span::styled(
166 format!("{}::{}", func.module_path.join("::"), func.name),
167 self.theme.style_type(),
168 ),
169 Span::styled(";", self.theme.style_normal()),
170 ]));
171 }
172 lines.push(Line::from(""));
173
174 lines.push(self.section_header("Signature"));
176 lines.push(Line::from(""));
177 for sig_line in func.signature.lines() {
178 lines.push(Line::from(vec![
179 Span::raw(" "),
180 Span::styled(sig_line.to_string(), self.theme.style_function()),
181 ]));
182 }
183
184 if func.source_location.file.is_some() {
186 lines.push(Line::from(""));
187 lines.push(self.section_header("Source"));
188 lines.push(Line::from(""));
189 lines.push(Line::from(vec![
190 Span::raw(" 📍 "),
191 Span::styled(func.source_location.to_string(), self.theme.style_muted()),
192 ]));
193 }
194
195 lines.push(Line::from(""));
197 lines.push(self.section_header("Overview"));
198 lines.push(Line::from(""));
199 lines.push(self.key_value("Visibility:", func.visibility.to_string()));
200
201 let mut props = Vec::new();
203 if func.is_async {
204 props.push("async");
205 }
206 if func.is_const {
207 props.push("const");
208 }
209 if func.is_unsafe {
210 props.push("unsafe");
211 }
212 if !props.is_empty() {
213 lines.push(self.key_value("Modifiers:", props.join(", ")));
214 }
215
216 if !func.generics.is_empty() {
217 lines.push(self.key_value("Generics:", format!("<{}>", func.generics.join(", "))));
218 }
219
220 if !func.parameters.is_empty() {
222 lines.push(Line::from(""));
223 lines.push(self.section_header(&format!("Parameters ({})", func.parameters.len())));
224 lines.push(Line::from(""));
225
226 for (i, param) in func.parameters.iter().enumerate() {
227 if param.is_self {
228 let self_str = if param.is_ref && param.is_mut {
229 "&mut self"
230 } else if param.is_ref {
231 "&self"
232 } else if param.is_mut {
233 "mut self"
234 } else {
235 "self"
236 };
237
238 let ownership_hint = if param.is_ref && param.is_mut {
239 "mutable borrow"
240 } else if param.is_ref {
241 "immutable borrow"
242 } else {
243 "takes ownership"
244 };
245
246 lines.push(Line::from(vec![
247 Span::styled(format!(" {}. ", i + 1), self.theme.style_number()),
248 Span::styled(self_str, self.theme.style_keyword()),
249 ]));
250 lines.push(Line::from(vec![
251 Span::raw(" "),
252 Span::styled(format!("↳ {}", ownership_hint), self.theme.style_muted()),
253 ]));
254 } else {
255 lines.push(Line::from(vec![
256 Span::styled(format!(" {}. ", i + 1), self.theme.style_number()),
257 Span::styled(param.name.clone(), self.theme.style_accent()),
258 Span::styled(": ", self.theme.style_muted()),
259 Span::styled(param.ty.clone(), self.theme.style_type()),
260 ]));
261
262 let ty_str = ¶m.ty;
264 let hint = if ty_str.starts_with('&') && ty_str.contains("mut") {
265 Some("↳ mutable borrow")
266 } else if ty_str.starts_with('&') {
267 Some("↳ immutable borrow")
268 } else if ty_str.contains("impl ") || ty_str.contains("dyn ") {
269 Some("↳ trait object/impl")
270 } else if param.is_mut {
271 Some("↳ mutable binding")
272 } else {
273 None
274 };
275
276 if let Some(h) = hint {
277 lines.push(Line::from(vec![
278 Span::raw(" "),
279 Span::styled(h.to_string(), self.theme.style_muted()),
280 ]));
281 }
282 }
283 }
284 } else {
285 lines.push(Line::from(""));
286 lines.push(self.section_header("Parameters"));
287 lines.push(Line::from(""));
288 lines.push(Line::from(vec![
289 Span::raw(" "),
290 Span::styled("None (zero-argument function)", self.theme.style_muted()),
291 ]));
292 }
293
294 lines.push(Line::from(""));
296 lines.push(self.section_header("Returns"));
297 lines.push(Line::from(""));
298
299 if let Some(ref ret) = func.return_type {
300 lines.push(Line::from(vec![
301 Span::raw(" "),
302 Span::styled("→ ", self.theme.style_accent()),
303 Span::styled(ret.clone(), self.theme.style_type()),
304 ]));
305
306 let ret_lower = ret.to_lowercase();
308 if ret_lower.contains("result") {
309 lines.push(Line::from(vec![
310 Span::raw(" "),
311 Span::styled(
312 "⚠ Can fail - handle errors appropriately",
313 self.theme.style_warning(),
314 ),
315 ]));
316 } else if ret_lower.contains("option") {
317 lines.push(Line::from(vec![
318 Span::raw(" "),
319 Span::styled("⚠ May return None", self.theme.style_warning()),
320 ]));
321 }
322 } else {
323 lines.push(Line::from(vec![
324 Span::raw(" "),
325 Span::styled("→ ", self.theme.style_accent()),
326 Span::styled("() ", self.theme.style_type()),
327 Span::styled("(unit type)", self.theme.style_muted()),
328 ]));
329 }
330
331 if let Some(ref where_clause) = func.where_clause {
333 lines.push(Line::from(""));
334 lines.push(self.section_header("Constraints"));
335 lines.push(Line::from(""));
336 lines.push(Line::from(vec![
337 Span::raw(" "),
338 Span::styled("where ", self.theme.style_keyword()),
339 Span::styled(where_clause.clone(), self.theme.style_type()),
340 ]));
341 }
342
343 if let Some(ref docs) = func.documentation {
345 lines.push(Line::from(""));
346 lines.push(self.section_header("Documentation"));
347 lines.push(Line::from(""));
348 for doc_line in docs.lines() {
349 let trimmed = doc_line.trim_start_matches('/').trim_start();
350 lines.push(Line::from(Span::styled(
351 format!(" {}", trimmed),
352 self.theme.style_comment(),
353 )));
354 }
355 }
356
357 if !func.attributes.is_empty() {
359 lines.push(Line::from(""));
360 lines.push(self.section_header("Attributes"));
361 lines.push(Line::from(""));
362 for attr in &func.attributes {
363 lines.push(Line::from(Span::styled(
364 format!(" #[{}]", attr),
365 self.theme.style_muted(),
366 )));
367 }
368 }
369
370 self.render_panel(" 🔧 Function ", lines, area, buf);
371 }
372
373 fn render_struct(&self, st: &StructInfo, area: Rect, buf: &mut Buffer) {
374 let mut lines = Vec::new();
375
376 let kind_str = match st.kind {
378 StructKind::Named => "struct",
379 StructKind::Tuple => "tuple struct",
380 StructKind::Unit => "unit struct",
381 };
382
383 lines.push(Line::from(vec![
384 Span::styled("struct ", self.theme.style_keyword()),
385 Span::styled(
386 st.name.clone(),
387 self.theme
388 .style_accent_bold()
389 .add_modifier(Modifier::UNDERLINED),
390 ),
391 Span::raw(" "),
392 Span::styled(format!("({})", kind_str), self.theme.style_muted()),
393 ]));
394
395 if !st.module_path.is_empty() {
397 lines.push(Line::from(vec![
398 Span::styled(" use ", self.theme.style_keyword()),
399 Span::styled(
400 format!("{}::{}", st.module_path.join("::"), st.name),
401 self.theme.style_type(),
402 ),
403 Span::styled(";", self.theme.style_normal()),
404 ]));
405 }
406 lines.push(Line::from(""));
407
408 lines.push(self.section_header("Definition"));
410 lines.push(Line::from(""));
411 for line in st.full_definition().lines() {
412 lines.push(Line::from(vec![
413 Span::raw(" "),
414 Span::styled(line.to_string(), self.theme.style_function()),
415 ]));
416 }
417
418 if st.source_location.file.is_some() {
420 lines.push(Line::from(""));
421 lines.push(self.section_header("Source"));
422 lines.push(Line::from(""));
423 lines.push(Line::from(vec![
424 Span::raw(" 📍 "),
425 Span::styled(st.source_location.to_string(), self.theme.style_muted()),
426 ]));
427 }
428
429 lines.push(Line::from(""));
431 lines.push(self.section_header("Overview"));
432 lines.push(Line::from(""));
433 lines.push(self.key_value("Visibility:", st.visibility.to_string()));
434 lines.push(self.key_value("Kind:", kind_str.to_string()));
435 lines.push(self.key_value("Field Count:", st.fields.len().to_string()));
436
437 if !st.generics.is_empty() {
438 lines.push(self.key_value("Generics:", format!("<{}>", st.generics.join(", "))));
439 }
440
441 if let Some(ref wc) = st.where_clause {
442 lines.push(self.key_value("Where:", wc.clone()));
443 }
444
445 if !st.derives.is_empty() {
447 lines.push(Line::from(""));
448 lines.push(self.section_header("Derived Traits"));
449 lines.push(Line::from(""));
450
451 let (standard, serde_derives, other): (Vec<_>, Vec<_>, Vec<_>) = st
453 .derives
454 .iter()
455 .fold((vec![], vec![], vec![]), |mut acc, d| {
456 let d_lower = d.to_lowercase();
457 if [
458 "debug",
459 "clone",
460 "copy",
461 "default",
462 "partialeq",
463 "eq",
464 "partialord",
465 "ord",
466 "hash",
467 ]
468 .contains(&d_lower.as_str())
469 {
470 acc.0.push(d);
471 } else if d_lower.contains("serde")
472 || d_lower == "serialize"
473 || d_lower == "deserialize"
474 {
475 acc.1.push(d);
476 } else {
477 acc.2.push(d);
478 }
479 acc
480 });
481
482 if !standard.is_empty() {
483 lines.push(Line::from(vec![
484 Span::raw(" "),
485 Span::styled("Standard: ", self.theme.style_dim()),
486 Span::styled(
487 standard
488 .iter()
489 .map(|s| s.as_str())
490 .collect::<Vec<_>>()
491 .join(", "),
492 self.theme.style_type(),
493 ),
494 ]));
495 }
496 if !serde_derives.is_empty() {
497 lines.push(Line::from(vec![
498 Span::raw(" "),
499 Span::styled("Serde: ", self.theme.style_dim()),
500 Span::styled(
501 serde_derives
502 .iter()
503 .map(|s| s.as_str())
504 .collect::<Vec<_>>()
505 .join(", "),
506 self.theme.style_type(),
507 ),
508 ]));
509 }
510 if !other.is_empty() {
511 lines.push(Line::from(vec![
512 Span::raw(" "),
513 Span::styled("Other: ", self.theme.style_dim()),
514 Span::styled(
515 other
516 .iter()
517 .map(|s| s.as_str())
518 .collect::<Vec<_>>()
519 .join(", "),
520 self.theme.style_type(),
521 ),
522 ]));
523 }
524 }
525
526 if !st.fields.is_empty() {
528 lines.push(Line::from(""));
529 lines.push(self.section_header(&format!("Fields ({})", st.fields.len())));
530 lines.push(Line::from(""));
531
532 for (i, field) in st.fields.iter().enumerate() {
533 let vis_str = match field.visibility {
534 Visibility::Public => "pub ",
535 Visibility::Crate => "pub(crate) ",
536 Visibility::Super => "pub(super) ",
537 _ => "",
538 };
539
540 lines.push(Line::from(vec![
541 Span::styled(format!(" {}. ", i + 1), self.theme.style_number()),
542 Span::styled(vis_str.to_string(), self.theme.style_keyword()),
543 Span::styled(field.name.clone(), self.theme.style_accent()),
544 Span::styled(": ", self.theme.style_muted()),
545 Span::styled(field.ty.clone(), self.theme.style_type()),
546 ]));
547
548 let ty_lower = field.ty.to_lowercase();
550 if ty_lower.contains("option") {
551 lines.push(Line::from(vec![
552 Span::raw(" "),
553 Span::styled("⚪ Optional field", self.theme.style_muted()),
554 ]));
555 } else if ty_lower.contains("vec")
556 || ty_lower.contains("hashmap")
557 || ty_lower.contains("btreemap")
558 {
559 lines.push(Line::from(vec![
560 Span::raw(" "),
561 Span::styled("📦 Collection type", self.theme.style_muted()),
562 ]));
563 } else if ty_lower.contains("box")
564 || ty_lower.contains("rc")
565 || ty_lower.contains("arc")
566 {
567 lines.push(Line::from(vec![
568 Span::raw(" "),
569 Span::styled("🔗 Heap-allocated/Shared", self.theme.style_muted()),
570 ]));
571 }
572
573 if let Some(ref doc) = field.documentation {
574 for doc_line in doc.lines().take(2) {
575 let trimmed = doc_line.trim_start_matches('/').trim_start();
576 if !trimmed.is_empty() {
577 lines.push(Line::from(vec![
578 Span::raw(" "),
579 Span::styled(trimmed.to_string(), self.theme.style_comment()),
580 ]));
581 }
582 }
583 }
584 }
585 }
586
587 lines.push(Line::from(""));
589 lines.push(self.section_header("Usage"));
590 lines.push(Line::from(""));
591
592 match st.kind {
594 StructKind::Named => {
595 lines.push(Line::from(vec![
596 Span::raw(" "),
597 Span::styled("let instance = ", self.theme.style_dim()),
598 Span::styled(
599 format!("{} {{ ... }};", st.name),
600 self.theme.style_function(),
601 ),
602 ]));
603 }
604 StructKind::Tuple => {
605 let placeholders = (0..st.fields.len())
606 .map(|_| "_")
607 .collect::<Vec<_>>()
608 .join(", ");
609 lines.push(Line::from(vec![
610 Span::raw(" "),
611 Span::styled("let instance = ", self.theme.style_dim()),
612 Span::styled(
613 format!("{}({});", st.name, placeholders),
614 self.theme.style_function(),
615 ),
616 ]));
617 }
618 StructKind::Unit => {
619 lines.push(Line::from(vec![
620 Span::raw(" "),
621 Span::styled("let instance = ", self.theme.style_dim()),
622 Span::styled(format!("{};", st.name), self.theme.style_function()),
623 ]));
624 }
625 }
626
627 if st.derives.iter().any(|d| d.to_lowercase() == "default") {
629 lines.push(Line::from(vec![
630 Span::raw(" "),
631 Span::styled("let instance = ", self.theme.style_dim()),
632 Span::styled(
633 format!("{}::default();", st.name),
634 self.theme.style_function(),
635 ),
636 ]));
637 }
638
639 if let Some(ref docs) = st.documentation {
641 lines.push(Line::from(""));
642 lines.push(self.section_header("Documentation"));
643 lines.push(Line::from(""));
644 for doc_line in docs.lines() {
645 let trimmed = doc_line.trim_start_matches('/').trim_start();
646 lines.push(Line::from(Span::styled(
647 format!(" {}", trimmed),
648 self.theme.style_comment(),
649 )));
650 }
651 }
652
653 if !st.attributes.is_empty() {
655 lines.push(Line::from(""));
656 lines.push(self.section_header("Attributes"));
657 lines.push(Line::from(""));
658 for attr in &st.attributes {
659 lines.push(Line::from(vec![
660 Span::raw(" "),
661 Span::styled(format!("#[{}]", attr), self.theme.style_muted()),
662 ]));
663 }
664 }
665
666 self.render_panel(" 📦 Struct ", lines, area, buf);
667 }
668
669 fn render_enum(&self, en: &EnumInfo, area: Rect, buf: &mut Buffer) {
670 let mut lines = Vec::new();
671
672 lines.push(Line::from(vec![
673 Span::styled("enum ", self.theme.style_keyword()),
674 Span::styled(
675 en.name.clone(),
676 self.theme
677 .style_accent_bold()
678 .add_modifier(Modifier::UNDERLINED),
679 ),
680 ]));
681
682 if !en.module_path.is_empty() {
684 lines.push(Line::from(vec![
685 Span::styled(" use ", self.theme.style_keyword()),
686 Span::styled(
687 format!("{}::{}", en.module_path.join("::"), en.name),
688 self.theme.style_type(),
689 ),
690 Span::styled(";", self.theme.style_normal()),
691 ]));
692 }
693 lines.push(Line::from(""));
694
695 lines.push(self.section_header("Overview"));
697 lines.push(Line::from(""));
698 lines.push(self.key_value("Visibility:", en.visibility.to_string()));
699 lines.push(self.key_value("Variants:", en.variants.len().to_string()));
700
701 if !en.generics.is_empty() {
702 lines.push(self.key_value("Generics:", format!("<{}>", en.generics.join(", "))));
703 }
704
705 if !en.derives.is_empty() {
707 lines.push(Line::from(""));
708 lines.push(self.section_header("Derived Traits"));
709 lines.push(Line::from(""));
710 for derive in &en.derives {
711 lines.push(Line::from(vec![
712 Span::raw(" • "),
713 Span::styled(derive.clone(), self.theme.style_type()),
714 ]));
715 }
716 }
717
718 lines.push(Line::from(""));
720 lines.push(self.section_header(&format!("Variants ({})", en.variants.len())));
721 lines.push(Line::from(""));
722
723 for (i, variant) in en.variants.iter().enumerate() {
724 let fields_str = match &variant.fields {
725 VariantFields::Named(fields) => {
726 let f: Vec<_> = fields
727 .iter()
728 .map(|f| format!("{}: {}", f.name, f.ty))
729 .collect();
730 format!(" {{ {} }}", f.join(", "))
731 }
732 VariantFields::Unnamed(types) => format!("({})", types.join(", ")),
733 VariantFields::Unit => String::new(),
734 };
735
736 let discriminant = variant
737 .discriminant
738 .as_ref()
739 .map(|d| format!(" = {}", d))
740 .unwrap_or_default();
741
742 lines.push(Line::from(vec![
743 Span::styled(format!(" {}. ", i + 1), self.theme.style_dim()),
744 Span::styled(variant.name.clone(), self.theme.style_type()),
745 Span::styled(fields_str, self.theme.style_muted()),
746 Span::styled(discriminant, self.theme.style_number()),
747 ]));
748
749 if let Some(ref doc) = variant.documentation {
750 let first_line = doc.lines().next().unwrap_or("");
751 lines.push(Line::from(vec![
752 Span::raw(" "),
753 Span::styled(first_line.to_string(), self.theme.style_comment()),
754 ]));
755 }
756 }
757
758 if let Some(ref docs) = en.documentation {
760 lines.push(Line::from(""));
761 lines.push(self.section_header("Documentation"));
762 lines.push(Line::from(""));
763 for doc_line in docs.lines() {
764 let trimmed = doc_line.trim_start_matches('/').trim_start();
765 lines.push(Line::from(Span::styled(
766 format!(" {}", trimmed),
767 self.theme.style_comment(),
768 )));
769 }
770 }
771
772 self.render_panel(" 🏷️ Enum ", lines, area, buf);
773 }
774
775 fn render_trait(&self, tr: &TraitInfo, area: Rect, buf: &mut Buffer) {
776 let mut lines = Vec::new();
777
778 let mut header = vec![
779 Span::styled("trait ", self.theme.style_keyword()),
780 Span::styled(
781 tr.name.clone(),
782 self.theme
783 .style_accent_bold()
784 .add_modifier(Modifier::UNDERLINED),
785 ),
786 ];
787
788 if tr.is_unsafe {
789 header.push(self.badge("unsafe", true));
790 }
791 if tr.is_auto {
792 header.push(self.badge("auto", false));
793 }
794
795 lines.push(Line::from(header));
796
797 if !tr.module_path.is_empty() {
799 lines.push(Line::from(vec![
800 Span::styled(" use ", self.theme.style_keyword()),
801 Span::styled(
802 format!("{}::{}", tr.module_path.join("::"), tr.name),
803 self.theme.style_type(),
804 ),
805 Span::styled(";", self.theme.style_normal()),
806 ]));
807 }
808 lines.push(Line::from(""));
809
810 lines.push(self.section_header("Overview"));
812 lines.push(Line::from(""));
813 lines.push(self.key_value("Visibility:", tr.visibility.to_string()));
814 lines.push(self.key_value("Methods:", tr.methods.len().to_string()));
815
816 if !tr.associated_types.is_empty() {
817 lines.push(self.key_value("Associated Types:", tr.associated_types.len().to_string()));
818 }
819
820 if !tr.supertraits.is_empty() {
822 lines.push(Line::from(""));
823 lines.push(self.section_header("Supertraits"));
824 lines.push(Line::from(""));
825 lines.push(Line::from(vec![
826 Span::raw(" "),
827 Span::styled(tr.supertraits.join(" + "), self.theme.style_type()),
828 ]));
829 }
830
831 if !tr.associated_types.is_empty() {
833 lines.push(Line::from(""));
834 lines.push(self.section_header("Associated Types"));
835 lines.push(Line::from(""));
836 for at in &tr.associated_types {
837 let bounds = if at.bounds.is_empty() {
838 String::new()
839 } else {
840 format!(": {}", at.bounds.join(" + "))
841 };
842 let default = at
843 .default
844 .as_ref()
845 .map(|d| format!(" = {}", d))
846 .unwrap_or_default();
847
848 lines.push(Line::from(vec![
849 Span::raw(" "),
850 Span::styled("type ", self.theme.style_keyword()),
851 Span::styled(at.name.clone(), self.theme.style_type()),
852 Span::styled(bounds, self.theme.style_muted()),
853 Span::styled(default, self.theme.style_type()),
854 ]));
855 }
856 }
857
858 if let Some(all) = self.all_items {
860 let impls: Vec<&ImplInfo> = all
861 .iter()
862 .filter_map(|i| {
863 if let AnalyzedItem::Impl(im) = i {
864 let matches = im.trait_name.as_deref().is_some_and(|tn| {
865 tn == tr.name || tn.ends_with(&format!("::{}", tr.name))
866 });
867 if matches {
868 Some(im)
869 } else {
870 None
871 }
872 } else {
873 None
874 }
875 })
876 .collect();
877 if !impls.is_empty() {
878 lines.push(Line::from(""));
879 lines.push(self.section_header(&format!("Implementations ({})", impls.len())));
880 lines.push(Line::from(""));
881 for (i, im) in impls.iter().enumerate() {
882 lines.push(Line::from(vec![
883 Span::styled(format!(" {}. ", i + 1), self.theme.style_dim()),
884 Span::styled(im.full_definition(), self.theme.style_type()),
885 ]));
886 }
887 }
888 }
889
890 if !tr.methods.is_empty() {
892 lines.push(Line::from(""));
893 lines.push(self.section_header(&format!("Methods ({})", tr.methods.len())));
894 lines.push(Line::from(""));
895
896 for (i, method) in tr.methods.iter().enumerate() {
897 let mut method_line = vec![
898 Span::styled(format!(" {}. ", i + 1), self.theme.style_dim()),
899 Span::styled("fn ", self.theme.style_keyword()),
900 Span::styled(method.name.clone(), self.theme.style_function()),
901 ];
902
903 if method.has_default {
904 method_line.push(Span::styled(" [default]", self.theme.style_success()));
905 }
906 if method.is_async {
907 method_line.push(Span::styled(" async", self.theme.style_keyword()));
908 }
909
910 lines.push(Line::from(method_line));
911
912 if let Some(ref doc) = method.documentation {
913 let first_line = doc.lines().next().unwrap_or("");
914 lines.push(Line::from(vec![
915 Span::raw(" "),
916 Span::styled(first_line.to_string(), self.theme.style_comment()),
917 ]));
918 }
919 }
920 }
921
922 if let Some(ref docs) = tr.documentation {
924 lines.push(Line::from(""));
925 lines.push(self.section_header("Documentation"));
926 lines.push(Line::from(""));
927 for doc_line in docs.lines() {
928 let trimmed = doc_line.trim_start_matches('/').trim_start();
929 lines.push(Line::from(Span::styled(
930 format!(" {}", trimmed),
931 self.theme.style_comment(),
932 )));
933 }
934 }
935
936 self.render_panel(" 📜 Trait ", lines, area, buf);
937 }
938
939 fn render_impl(&self, im: &ImplInfo, area: Rect, buf: &mut Buffer) {
940 let mut lines = Vec::new();
941
942 let title = if let Some(ref trait_name) = im.trait_name {
943 format!("impl {} for {}", trait_name, im.self_ty)
944 } else {
945 format!("impl {}", im.self_ty)
946 };
947
948 let mut header = vec![
949 Span::styled("impl ", self.theme.style_keyword()),
950 Span::styled(title, self.theme.style_accent_bold()),
951 ];
952
953 if im.is_unsafe {
954 header.push(self.badge("unsafe", true));
955 }
956 if im.is_negative {
957 header.push(self.badge("negative", true));
958 }
959
960 lines.push(Line::from(header));
961 lines.push(Line::from(""));
962
963 lines.push(self.section_header("Overview"));
965 lines.push(Line::from(""));
966 lines.push(self.key_value("Type:", im.self_ty.clone()));
967
968 if let Some(ref trait_name) = im.trait_name {
969 lines.push(self.key_value("Trait:", trait_name.clone()));
970 }
971
972 lines.push(self.key_value("Methods:", im.methods.len().to_string()));
973
974 if !im.generics.is_empty() {
975 lines.push(self.key_value("Generics:", format!("<{}>", im.generics.join(", "))));
976 }
977
978 if !im.methods.is_empty() {
980 lines.push(Line::from(""));
981 lines.push(self.section_header(&format!("Methods ({})", im.methods.len())));
982 lines.push(Line::from(""));
983
984 for (i, method) in im.methods.iter().enumerate() {
985 let vis = if method.visibility == Visibility::Public {
986 "pub "
987 } else {
988 ""
989 };
990
991 let mut method_line = vec![
992 Span::styled(format!(" {}. ", i + 1), self.theme.style_dim()),
993 Span::styled(vis.to_string(), self.theme.style_keyword()),
994 Span::styled("fn ", self.theme.style_keyword()),
995 Span::styled(method.name.clone(), self.theme.style_function()),
996 ];
997
998 let param_count = method.parameters.len();
1000 method_line.push(Span::styled(
1001 format!("({} params)", param_count),
1002 self.theme.style_muted(),
1003 ));
1004
1005 if let Some(ref ret) = method.return_type {
1007 method_line.push(Span::styled(" → ", self.theme.style_accent()));
1008 method_line.push(Span::styled(ret.clone(), self.theme.style_type()));
1009 }
1010
1011 lines.push(Line::from(method_line));
1012 }
1013 }
1014
1015 if let Some(ref where_clause) = im.where_clause {
1017 lines.push(Line::from(""));
1018 lines.push(self.section_header("Constraints"));
1019 lines.push(Line::from(""));
1020 lines.push(Line::from(vec![
1021 Span::raw(" "),
1022 Span::styled("where ", self.theme.style_keyword()),
1023 Span::styled(where_clause.clone(), self.theme.style_type()),
1024 ]));
1025 }
1026
1027 self.render_panel(" ⚙️ Implementation ", lines, area, buf);
1028 }
1029
1030 fn render_module(&self, module: &ModuleInfo, area: Rect, buf: &mut Buffer) {
1031 let mut lines = vec![
1032 Line::from(vec![
1033 Span::styled("mod ", self.theme.style_keyword()),
1034 Span::styled(
1035 module.name.clone(),
1036 self.theme
1037 .style_accent_bold()
1038 .add_modifier(Modifier::UNDERLINED),
1039 ),
1040 ]),
1041 Line::from(""),
1042 self.section_header("Overview"),
1043 Line::from(""),
1044 self.key_value("Visibility:", module.visibility.to_string()),
1045 self.key_value("Path:", module.path.clone()),
1046 self.key_value(
1047 "Inline:",
1048 if module.is_inline { "yes" } else { "no" }.to_string(),
1049 ),
1050 ];
1051
1052 if !module.submodules.is_empty() {
1054 lines.push(Line::from(""));
1055 lines.push(
1056 self.section_header(&format!("Submodules / flow ({})", module.submodules.len())),
1057 );
1058 lines.push(Line::from(""));
1059 let n = module.submodules.len();
1060 for (i, submod) in module.submodules.iter().enumerate() {
1061 let connector = if i == n - 1 {
1062 "└── "
1063 } else {
1064 "├── "
1065 };
1066 lines.push(Line::from(vec![
1067 Span::raw(" "),
1068 Span::styled(connector, self.theme.style_muted()),
1069 Span::styled(submod.clone(), self.theme.style_accent()),
1070 ]));
1071 }
1072 }
1073
1074 if !module.items.is_empty() {
1076 lines.push(Line::from(""));
1077 lines.push(self.section_header(&format!("Items ({})", module.items.len())));
1078 lines.push(Line::from(""));
1079 for item in module.items.iter().take(20) {
1080 lines.push(Line::from(vec![
1081 Span::raw(" • "),
1082 Span::styled(item.clone(), self.theme.style_normal()),
1083 ]));
1084 }
1085 if module.items.len() > 20 {
1086 lines.push(Line::from(vec![
1087 Span::raw(" "),
1088 Span::styled(
1089 format!("... and {} more", module.items.len() - 20),
1090 self.theme.style_muted(),
1091 ),
1092 ]));
1093 }
1094 }
1095
1096 if let Some(ref docs) = module.documentation {
1098 lines.push(Line::from(""));
1099 lines.push(self.section_header("Documentation"));
1100 lines.push(Line::from(""));
1101 for doc_line in docs.lines() {
1102 let trimmed = doc_line.trim_start_matches('/').trim_start();
1103 lines.push(Line::from(Span::styled(
1104 format!(" {}", trimmed),
1105 self.theme.style_comment(),
1106 )));
1107 }
1108 }
1109
1110 self.render_panel(" 📁 Module ", lines, area, buf);
1111 }
1112
1113 fn render_type_alias(&self, alias: &TypeAliasInfo, area: Rect, buf: &mut Buffer) {
1114 let mut lines = vec![
1115 Line::from(vec![
1116 Span::styled("type ", self.theme.style_keyword()),
1117 Span::styled(
1118 alias.name.clone(),
1119 self.theme
1120 .style_accent_bold()
1121 .add_modifier(Modifier::UNDERLINED),
1122 ),
1123 Span::styled(" = ", self.theme.style_muted()),
1124 Span::styled(alias.ty.clone(), self.theme.style_type()),
1125 ]),
1126 Line::from(""),
1127 self.section_header("Overview"),
1128 Line::from(""),
1129 self.key_value("Visibility:", alias.visibility.to_string()),
1130 self.key_value("Aliased Type:", alias.ty.clone()),
1131 ];
1132
1133 if !alias.generics.is_empty() {
1134 lines.push(self.key_value("Generics:", format!("<{}>", alias.generics.join(", "))));
1135 }
1136
1137 if let Some(ref docs) = alias.documentation {
1138 lines.push(Line::from(""));
1139 lines.push(self.section_header("Documentation"));
1140 lines.push(Line::from(""));
1141 for doc_line in docs.lines() {
1142 let trimmed = doc_line.trim_start_matches('/').trim_start();
1143 lines.push(Line::from(Span::styled(
1144 format!(" {}", trimmed),
1145 self.theme.style_comment(),
1146 )));
1147 }
1148 }
1149
1150 self.render_panel(" 🔗 Type Alias ", lines, area, buf);
1151 }
1152
1153 fn render_const(&self, c: &ConstInfo, area: Rect, buf: &mut Buffer) {
1154 let mut lines = vec![
1155 Line::from(vec![
1156 Span::styled("const ", self.theme.style_keyword()),
1157 Span::styled(
1158 c.name.clone(),
1159 self.theme
1160 .style_accent_bold()
1161 .add_modifier(Modifier::UNDERLINED),
1162 ),
1163 Span::styled(": ", self.theme.style_muted()),
1164 Span::styled(c.ty.clone(), self.theme.style_type()),
1165 ]),
1166 Line::from(""),
1167 self.section_header("Overview"),
1168 Line::from(""),
1169 self.key_value("Visibility:", c.visibility.to_string()),
1170 self.key_value("Type:", c.ty.clone()),
1171 ];
1172
1173 if let Some(ref value) = c.value {
1174 lines.push(self.key_value("Value:", value.clone()));
1175 }
1176
1177 if let Some(ref docs) = c.documentation {
1178 lines.push(Line::from(""));
1179 lines.push(self.section_header("Documentation"));
1180 lines.push(Line::from(""));
1181 for doc_line in docs.lines() {
1182 let trimmed = doc_line.trim_start_matches('/').trim_start();
1183 lines.push(Line::from(Span::styled(
1184 format!(" {}", trimmed),
1185 self.theme.style_comment(),
1186 )));
1187 }
1188 }
1189
1190 self.render_panel(" 📌 Constant ", lines, area, buf);
1191 }
1192
1193 fn render_static(&self, s: &StaticInfo, area: Rect, buf: &mut Buffer) {
1194 let mut lines = Vec::new();
1195
1196 let mut header = vec![Span::styled("static ", self.theme.style_keyword())];
1197
1198 if s.is_mut {
1199 header.push(Span::styled("mut ", self.theme.style_keyword()));
1200 }
1201
1202 header.push(Span::styled(
1203 s.name.clone(),
1204 self.theme
1205 .style_accent_bold()
1206 .add_modifier(Modifier::UNDERLINED),
1207 ));
1208 header.push(Span::styled(": ", self.theme.style_muted()));
1209 header.push(Span::styled(s.ty.clone(), self.theme.style_type()));
1210
1211 if s.is_mut {
1212 header.push(self.badge("mutable", true));
1213 }
1214
1215 lines.push(Line::from(header));
1216 lines.push(Line::from(""));
1217
1218 lines.push(self.section_header("Overview"));
1219 lines.push(Line::from(""));
1220 lines.push(self.key_value("Visibility:", s.visibility.to_string()));
1221 lines.push(self.key_value("Type:", s.ty.clone()));
1222 lines.push(self.key_value(
1223 "Mutable:",
1224 if s.is_mut { "yes ⚠️" } else { "no" }.to_string(),
1225 ));
1226
1227 if s.is_mut {
1228 lines.push(Line::from(""));
1229 lines.push(Line::from(vec![
1230 Span::raw(" "),
1231 Span::styled("⚠ Warning: ", self.theme.style_error()),
1232 Span::styled("Mutable statics are unsafe!", self.theme.style_warning()),
1233 ]));
1234 }
1235
1236 if let Some(ref docs) = s.documentation {
1237 lines.push(Line::from(""));
1238 lines.push(self.section_header("Documentation"));
1239 lines.push(Line::from(""));
1240 for doc_line in docs.lines() {
1241 let trimmed = doc_line.trim_start_matches('/').trim_start();
1242 lines.push(Line::from(Span::styled(
1243 format!(" {}", trimmed),
1244 self.theme.style_comment(),
1245 )));
1246 }
1247 }
1248
1249 self.render_panel(" 🌐 Static ", lines, area, buf);
1250 }
1251
1252 fn render_panel(&self, title: &str, lines: Vec<Line<'static>>, area: Rect, buf: &mut Buffer) {
1253 let total_lines = lines.len();
1254
1255 let block = Block::default()
1256 .borders(Borders::ALL)
1257 .border_type(BorderType::Rounded)
1258 .border_style(if self.focused {
1259 self.theme.style_border_focused()
1260 } else {
1261 self.theme.style_border()
1262 })
1263 .style(Style::default().bg(self.theme.bg_panel))
1264 .title(title);
1265
1266 let inner = block.inner(area);
1267 block.render(area, buf);
1268
1269 let visible_lines: Vec<Line> = lines.into_iter().skip(self.scroll_offset).collect();
1271
1272 Paragraph::new(visible_lines)
1273 .wrap(Wrap { trim: false })
1274 .render(inner, buf);
1275
1276 if total_lines > inner.height as usize {
1278 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
1279 .begin_symbol(Some("↑"))
1280 .end_symbol(Some("↓"));
1281
1282 let mut scrollbar_state = ScrollbarState::new(total_lines).position(self.scroll_offset);
1283
1284 scrollbar.render(inner, buf, &mut scrollbar_state);
1285 }
1286 }
1287}
1288
1289impl Widget for InspectorPanel<'_> {
1290 fn render(self, area: Rect, buf: &mut Buffer) {
1291 match self.item {
1292 None => self.render_empty(area, buf),
1293 Some(AnalyzedItem::Function(f)) => self.render_function(f, area, buf),
1294 Some(AnalyzedItem::Struct(s)) => self.render_struct(s, area, buf),
1295 Some(AnalyzedItem::Enum(e)) => self.render_enum(e, area, buf),
1296 Some(AnalyzedItem::Trait(t)) => self.render_trait(t, area, buf),
1297 Some(AnalyzedItem::Impl(i)) => self.render_impl(i, area, buf),
1298 Some(AnalyzedItem::Module(m)) => self.render_module(m, area, buf),
1299 Some(AnalyzedItem::TypeAlias(t)) => self.render_type_alias(t, area, buf),
1300 Some(AnalyzedItem::Const(c)) => self.render_const(c, area, buf),
1301 Some(AnalyzedItem::Static(s)) => self.render_static(s, area, buf),
1302 }
1303 }
1304}