Skip to main content

rich_rs/
scope.rs

1//! Scope: render local variables as a formatted table.
2//!
3//! This module provides functionality to render variable scopes (like local
4//! variables in a stack frame) as a formatted, styled table inside a panel.
5//!
6//! # Example
7//!
8//! ```
9//! use std::collections::BTreeMap;
10//! use rich_rs::scope::render_scope;
11//!
12//! let mut scope: BTreeMap<String, String> = BTreeMap::new();
13//! scope.insert("foo".to_string(), "42".to_string());
14//! scope.insert("bar".to_string(), r#""hello""#.to_string());
15//!
16//! let renderable = render_scope(&scope, Some("Locals"), true, false, None, None);
17//! ```
18
19use std::collections::BTreeMap;
20use std::io::Stdout;
21
22use crate::Renderable;
23use crate::console::{Console, ConsoleOptions, JustifyMethod, OverflowMethod};
24use crate::highlighter::{Highlighter, repr_highlighter};
25use crate::measure::Measurement;
26use crate::panel::Panel;
27use crate::segment::Segments;
28use crate::style::Style;
29use crate::table::{Column, Row, Table};
30use crate::text::Text;
31
32// ============================================================================
33// Styles
34// ============================================================================
35
36/// Style for regular variable names.
37fn scope_key_style() -> Style {
38    Style::new().with_color(crate::color::SimpleColor::Standard(6)) // cyan
39}
40
41/// Style for special variable names (dunder variables).
42fn scope_key_special_style() -> Style {
43    Style::new()
44        .with_color(crate::color::SimpleColor::Standard(6))
45        .with_dim(true) // cyan + dim
46}
47
48/// Style for the equals sign.
49fn scope_equals_style() -> Style {
50    Style::new().with_bold(true)
51}
52
53/// Style for the panel border.
54fn scope_border_style() -> Style {
55    Style::new().with_color(crate::color::SimpleColor::Standard(6)) // cyan
56}
57
58// ============================================================================
59// ScopeRenderable
60// ============================================================================
61
62/// A renderable scope display.
63///
64/// Renders variable names and their values in a styled table within a panel.
65pub struct ScopeRenderable {
66    /// The scope data (variable name -> debug representation).
67    scope: BTreeMap<String, String>,
68    /// Optional title for the panel.
69    title: Option<String>,
70    /// Whether to sort keys alphabetically.
71    sort_keys: bool,
72    /// Whether to show indent guides in values.
73    indent_guides: bool,
74    /// Maximum length for container values before abbreviating.
75    max_length: Option<usize>,
76    /// Maximum string length before truncating.
77    max_string: Option<usize>,
78    /// Overflow method for long values.
79    overflow: Option<OverflowMethod>,
80    /// Maximum depth for nested value display.
81    max_depth: Option<usize>,
82}
83
84impl std::fmt::Debug for ScopeRenderable {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        f.debug_struct("ScopeRenderable")
87            .field("scope_len", &self.scope.len())
88            .field("title", &self.title)
89            .field("sort_keys", &self.sort_keys)
90            .field("indent_guides", &self.indent_guides)
91            .field("max_length", &self.max_length)
92            .field("max_string", &self.max_string)
93            .field("overflow", &self.overflow)
94            .field("max_depth", &self.max_depth)
95            .finish()
96    }
97}
98
99impl ScopeRenderable {
100    /// Create a new scope renderable.
101    pub fn new(scope: BTreeMap<String, String>) -> Self {
102        Self {
103            scope,
104            title: None,
105            sort_keys: true,
106            indent_guides: false,
107            max_length: None,
108            max_string: None,
109            overflow: None,
110            max_depth: None,
111        }
112    }
113
114    /// Set the title.
115    pub fn with_title(mut self, title: impl Into<String>) -> Self {
116        self.title = Some(title.into());
117        self
118    }
119
120    /// Set whether to sort keys.
121    pub fn with_sort_keys(mut self, sort: bool) -> Self {
122        self.sort_keys = sort;
123        self
124    }
125
126    /// Set whether to show indent guides.
127    pub fn with_indent_guides(mut self, guides: bool) -> Self {
128        self.indent_guides = guides;
129        self
130    }
131
132    /// Set maximum container length.
133    pub fn with_max_length(mut self, max: Option<usize>) -> Self {
134        self.max_length = max;
135        self
136    }
137
138    /// Set maximum string length.
139    pub fn with_max_string(mut self, max: Option<usize>) -> Self {
140        self.max_string = max;
141        self
142    }
143
144    /// Set overflow method for long values.
145    pub fn with_overflow(mut self, overflow: OverflowMethod) -> Self {
146        self.overflow = Some(overflow);
147        self
148    }
149
150    /// Set maximum depth for nested value display.
151    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
152        self.max_depth = Some(max_depth);
153        self
154    }
155
156    /// Get sorted items (dunder vars first, then alphabetical).
157    fn sorted_items(&self) -> Vec<(&String, &String)> {
158        let mut items: Vec<_> = self.scope.iter().collect();
159
160        if self.sort_keys {
161            items.sort_by(|(a, _), (b, _)| {
162                // Dunder variables sort first
163                let a_dunder = a.starts_with("__");
164                let b_dunder = b.starts_with("__");
165
166                match (a_dunder, b_dunder) {
167                    (true, false) => std::cmp::Ordering::Less,
168                    (false, true) => std::cmp::Ordering::Greater,
169                    _ => a.to_lowercase().cmp(&b.to_lowercase()),
170                }
171            });
172        }
173
174        items
175    }
176}
177
178impl Renderable for ScopeRenderable {
179    fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
180        // Create a grid table (no borders, minimal padding)
181        let mut table = Table::grid().with_padding(0, 1).with_expand(false);
182
183        // Add columns: key (right-justified) and value
184        let key_column = Column::new().justify(JustifyMethod::Right);
185        table.add_column(key_column);
186        table.add_column(Column::new());
187
188        // Get highlighter for values
189        let highlighter = repr_highlighter();
190
191        // Add rows
192        let items = self.sorted_items();
193        for (key, value) in items {
194            // Style the key
195            let key_style = if key.starts_with("__") {
196                scope_key_special_style()
197            } else {
198                scope_key_style()
199            };
200
201            // Build key text: "name ="
202            let mut key_text = Text::styled(key, key_style);
203            key_text.append(" =", Some(scope_equals_style()));
204
205            // Highlight the value
206            let mut value_text = Text::plain(value);
207            highlighter.highlight(&mut value_text);
208
209            // Truncate if needed
210            if let Some(max_str) = self.max_string {
211                let overflow = self.overflow.unwrap_or(OverflowMethod::Ellipsis);
212                if value_text.plain_text().len() > max_str {
213                    value_text = value_text.truncate(max_str, overflow, false);
214                }
215            }
216
217            // Create row
218            let cells: Vec<Box<dyn Renderable + Send + Sync>> =
219                vec![Box::new(key_text), Box::new(value_text)];
220            table.add_row(Row::new(cells));
221        }
222
223        // Wrap in a panel
224        let mut panel = Panel::fit(Box::new(table))
225            .with_border_style(scope_border_style())
226            .with_padding((0, 1, 0, 1));
227
228        if let Some(ref title) = self.title {
229            panel = panel.with_title(title);
230        }
231
232        panel.render(console, options)
233    }
234
235    fn measure(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
236        // Delegate to render-then-measure (default behavior)
237        Measurement::from_segments(&self.render(console, options))
238    }
239}
240
241// ============================================================================
242// Public API
243// ============================================================================
244
245/// Render a scope (variable mapping) as a formatted panel.
246///
247/// # Arguments
248///
249/// * `scope` - A mapping of variable names to their debug representations.
250/// * `title` - Optional title for the panel.
251/// * `sort_keys` - Whether to sort keys (dunder variables first, then alphabetical).
252/// * `indent_guides` - Whether to show indent guides in values (not yet implemented).
253/// * `max_length` - Maximum container length before abbreviating (not yet implemented).
254/// * `max_string` - Maximum string length before truncating.
255///
256/// # Returns
257///
258/// A `ScopeRenderable` that implements `Renderable`.
259///
260/// # Example
261///
262/// ```
263/// use std::collections::BTreeMap;
264/// use rich_rs::scope::render_scope;
265///
266/// let mut locals: BTreeMap<String, String> = BTreeMap::new();
267/// locals.insert("x".to_string(), "42".to_string());
268/// locals.insert("name".to_string(), r#""Alice""#.to_string());
269///
270/// let scope_panel = render_scope(&locals, Some("Locals"), true, false, None, None);
271/// ```
272pub fn render_scope(
273    scope: &BTreeMap<String, String>,
274    title: Option<&str>,
275    sort_keys: bool,
276    indent_guides: bool,
277    max_length: Option<usize>,
278    max_string: Option<usize>,
279) -> ScopeRenderable {
280    let mut renderable = ScopeRenderable::new(scope.clone())
281        .with_sort_keys(sort_keys)
282        .with_indent_guides(indent_guides)
283        .with_max_length(max_length)
284        .with_max_string(max_string);
285
286    if let Some(t) = title {
287        renderable = renderable.with_title(t);
288    }
289
290    renderable
291}
292
293// ============================================================================
294// Tests
295// ============================================================================
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_render_scope_empty() {
303        let scope: BTreeMap<String, String> = BTreeMap::new();
304        let renderable = render_scope(&scope, None, true, false, None, None);
305
306        let console = Console::new();
307        let options = ConsoleOptions::default();
308        let segments = renderable.render(&console, &options);
309
310        // Should render even if empty
311        assert!(!segments.is_empty());
312    }
313
314    #[test]
315    fn test_render_scope_with_data() {
316        let mut scope: BTreeMap<String, String> = BTreeMap::new();
317        scope.insert("foo".to_string(), "42".to_string());
318        scope.insert("bar".to_string(), r#""hello""#.to_string());
319
320        let renderable = render_scope(&scope, Some("Locals"), true, false, None, None);
321
322        let console = Console::new();
323        let options = ConsoleOptions::default();
324        let segments = renderable.render(&console, &options);
325        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
326
327        assert!(output.contains("foo"));
328        assert!(output.contains("bar"));
329        assert!(output.contains("42"));
330        assert!(output.contains("hello"));
331        assert!(output.contains("Locals"));
332    }
333
334    #[test]
335    fn test_render_scope_dunder_first() {
336        let mut scope: BTreeMap<String, String> = BTreeMap::new();
337        scope.insert("zebra".to_string(), "1".to_string());
338        scope.insert("__name__".to_string(), r#""test""#.to_string());
339        scope.insert("alpha".to_string(), "2".to_string());
340
341        let renderable = ScopeRenderable::new(scope);
342        let items = renderable.sorted_items();
343        let keys: Vec<&str> = items.iter().map(|(k, _)| k.as_str()).collect();
344
345        // Dunder should be first
346        assert_eq!(keys[0], "__name__");
347    }
348
349    #[test]
350    fn test_scope_renderable_builder() {
351        let scope: BTreeMap<String, String> = BTreeMap::new();
352        let renderable = ScopeRenderable::new(scope)
353            .with_title("Test")
354            .with_sort_keys(false)
355            .with_indent_guides(true)
356            .with_max_length(Some(10))
357            .with_max_string(Some(80))
358            .with_overflow(OverflowMethod::Crop)
359            .with_max_depth(5);
360
361        assert_eq!(renderable.title, Some("Test".to_string()));
362        assert!(!renderable.sort_keys);
363        assert!(renderable.indent_guides);
364        assert_eq!(renderable.max_length, Some(10));
365        assert_eq!(renderable.max_string, Some(80));
366        assert_eq!(renderable.overflow, Some(OverflowMethod::Crop));
367        assert_eq!(renderable.max_depth, Some(5));
368    }
369
370    #[test]
371    fn test_scope_is_send_sync() {
372        fn assert_send<T: Send>() {}
373        fn assert_sync<T: Sync>() {}
374        assert_send::<ScopeRenderable>();
375        assert_sync::<ScopeRenderable>();
376    }
377
378    #[test]
379    fn test_scope_debug() {
380        let mut scope: BTreeMap<String, String> = BTreeMap::new();
381        scope.insert("x".to_string(), "1".to_string());
382        let renderable = ScopeRenderable::new(scope);
383        let debug_str = format!("{:?}", renderable);
384        assert!(debug_str.contains("ScopeRenderable"));
385        assert!(debug_str.contains("scope_len"));
386    }
387}