1use 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
32fn scope_key_style() -> Style {
38 Style::new().with_color(crate::color::SimpleColor::Standard(6)) }
40
41fn scope_key_special_style() -> Style {
43 Style::new()
44 .with_color(crate::color::SimpleColor::Standard(6))
45 .with_dim(true) }
47
48fn scope_equals_style() -> Style {
50 Style::new().with_bold(true)
51}
52
53fn scope_border_style() -> Style {
55 Style::new().with_color(crate::color::SimpleColor::Standard(6)) }
57
58pub struct ScopeRenderable {
66 scope: BTreeMap<String, String>,
68 title: Option<String>,
70 sort_keys: bool,
72 indent_guides: bool,
74 max_length: Option<usize>,
76 max_string: Option<usize>,
78 overflow: Option<OverflowMethod>,
80 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 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 pub fn with_title(mut self, title: impl Into<String>) -> Self {
116 self.title = Some(title.into());
117 self
118 }
119
120 pub fn with_sort_keys(mut self, sort: bool) -> Self {
122 self.sort_keys = sort;
123 self
124 }
125
126 pub fn with_indent_guides(mut self, guides: bool) -> Self {
128 self.indent_guides = guides;
129 self
130 }
131
132 pub fn with_max_length(mut self, max: Option<usize>) -> Self {
134 self.max_length = max;
135 self
136 }
137
138 pub fn with_max_string(mut self, max: Option<usize>) -> Self {
140 self.max_string = max;
141 self
142 }
143
144 pub fn with_overflow(mut self, overflow: OverflowMethod) -> Self {
146 self.overflow = Some(overflow);
147 self
148 }
149
150 pub fn with_max_depth(mut self, max_depth: usize) -> Self {
152 self.max_depth = Some(max_depth);
153 self
154 }
155
156 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 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 let mut table = Table::grid().with_padding(0, 1).with_expand(false);
182
183 let key_column = Column::new().justify(JustifyMethod::Right);
185 table.add_column(key_column);
186 table.add_column(Column::new());
187
188 let highlighter = repr_highlighter();
190
191 let items = self.sorted_items();
193 for (key, value) in items {
194 let key_style = if key.starts_with("__") {
196 scope_key_special_style()
197 } else {
198 scope_key_style()
199 };
200
201 let mut key_text = Text::styled(key, key_style);
203 key_text.append(" =", Some(scope_equals_style()));
204
205 let mut value_text = Text::plain(value);
207 highlighter.highlight(&mut value_text);
208
209 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 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 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 Measurement::from_segments(&self.render(console, options))
238 }
239}
240
241pub 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#[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 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 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}