use std::collections::BTreeMap;
use std::io::Stdout;
use crate::Renderable;
use crate::console::{Console, ConsoleOptions, JustifyMethod, OverflowMethod};
use crate::highlighter::{Highlighter, repr_highlighter};
use crate::measure::Measurement;
use crate::panel::Panel;
use crate::segment::Segments;
use crate::style::Style;
use crate::table::{Column, Row, Table};
use crate::text::Text;
fn scope_key_style() -> Style {
Style::new().with_color(crate::color::SimpleColor::Standard(6)) }
fn scope_key_special_style() -> Style {
Style::new()
.with_color(crate::color::SimpleColor::Standard(6))
.with_dim(true) }
fn scope_equals_style() -> Style {
Style::new().with_bold(true)
}
fn scope_border_style() -> Style {
Style::new().with_color(crate::color::SimpleColor::Standard(6)) }
pub struct ScopeRenderable {
scope: BTreeMap<String, String>,
title: Option<String>,
sort_keys: bool,
indent_guides: bool,
max_length: Option<usize>,
max_string: Option<usize>,
overflow: Option<OverflowMethod>,
max_depth: Option<usize>,
}
impl std::fmt::Debug for ScopeRenderable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScopeRenderable")
.field("scope_len", &self.scope.len())
.field("title", &self.title)
.field("sort_keys", &self.sort_keys)
.field("indent_guides", &self.indent_guides)
.field("max_length", &self.max_length)
.field("max_string", &self.max_string)
.field("overflow", &self.overflow)
.field("max_depth", &self.max_depth)
.finish()
}
}
impl ScopeRenderable {
pub fn new(scope: BTreeMap<String, String>) -> Self {
Self {
scope,
title: None,
sort_keys: true,
indent_guides: false,
max_length: None,
max_string: None,
overflow: None,
max_depth: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_sort_keys(mut self, sort: bool) -> Self {
self.sort_keys = sort;
self
}
pub fn with_indent_guides(mut self, guides: bool) -> Self {
self.indent_guides = guides;
self
}
pub fn with_max_length(mut self, max: Option<usize>) -> Self {
self.max_length = max;
self
}
pub fn with_max_string(mut self, max: Option<usize>) -> Self {
self.max_string = max;
self
}
pub fn with_overflow(mut self, overflow: OverflowMethod) -> Self {
self.overflow = Some(overflow);
self
}
pub fn with_max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = Some(max_depth);
self
}
fn sorted_items(&self) -> Vec<(&String, &String)> {
let mut items: Vec<_> = self.scope.iter().collect();
if self.sort_keys {
items.sort_by(|(a, _), (b, _)| {
let a_dunder = a.starts_with("__");
let b_dunder = b.starts_with("__");
match (a_dunder, b_dunder) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.to_lowercase().cmp(&b.to_lowercase()),
}
});
}
items
}
}
impl Renderable for ScopeRenderable {
fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
let mut table = Table::grid().with_padding(0, 1).with_expand(false);
let key_column = Column::new().justify(JustifyMethod::Right);
table.add_column(key_column);
table.add_column(Column::new());
let highlighter = repr_highlighter();
let items = self.sorted_items();
for (key, value) in items {
let key_style = if key.starts_with("__") {
scope_key_special_style()
} else {
scope_key_style()
};
let mut key_text = Text::styled(key, key_style);
key_text.append(" =", Some(scope_equals_style()));
let mut value_text = Text::plain(value);
highlighter.highlight(&mut value_text);
if let Some(max_str) = self.max_string {
let overflow = self.overflow.unwrap_or(OverflowMethod::Ellipsis);
if value_text.plain_text().len() > max_str {
value_text = value_text.truncate(max_str, overflow, false);
}
}
let cells: Vec<Box<dyn Renderable + Send + Sync>> =
vec![Box::new(key_text), Box::new(value_text)];
table.add_row(Row::new(cells));
}
let mut panel = Panel::fit(Box::new(table))
.with_border_style(scope_border_style())
.with_padding((0, 1, 0, 1));
if let Some(ref title) = self.title {
panel = panel.with_title(title);
}
panel.render(console, options)
}
fn measure(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
Measurement::from_segments(&self.render(console, options))
}
}
pub fn render_scope(
scope: &BTreeMap<String, String>,
title: Option<&str>,
sort_keys: bool,
indent_guides: bool,
max_length: Option<usize>,
max_string: Option<usize>,
) -> ScopeRenderable {
let mut renderable = ScopeRenderable::new(scope.clone())
.with_sort_keys(sort_keys)
.with_indent_guides(indent_guides)
.with_max_length(max_length)
.with_max_string(max_string);
if let Some(t) = title {
renderable = renderable.with_title(t);
}
renderable
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_scope_empty() {
let scope: BTreeMap<String, String> = BTreeMap::new();
let renderable = render_scope(&scope, None, true, false, None, None);
let console = Console::new();
let options = ConsoleOptions::default();
let segments = renderable.render(&console, &options);
assert!(!segments.is_empty());
}
#[test]
fn test_render_scope_with_data() {
let mut scope: BTreeMap<String, String> = BTreeMap::new();
scope.insert("foo".to_string(), "42".to_string());
scope.insert("bar".to_string(), r#""hello""#.to_string());
let renderable = render_scope(&scope, Some("Locals"), true, false, None, None);
let console = Console::new();
let options = ConsoleOptions::default();
let segments = renderable.render(&console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains("foo"));
assert!(output.contains("bar"));
assert!(output.contains("42"));
assert!(output.contains("hello"));
assert!(output.contains("Locals"));
}
#[test]
fn test_render_scope_dunder_first() {
let mut scope: BTreeMap<String, String> = BTreeMap::new();
scope.insert("zebra".to_string(), "1".to_string());
scope.insert("__name__".to_string(), r#""test""#.to_string());
scope.insert("alpha".to_string(), "2".to_string());
let renderable = ScopeRenderable::new(scope);
let items = renderable.sorted_items();
let keys: Vec<&str> = items.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(keys[0], "__name__");
}
#[test]
fn test_scope_renderable_builder() {
let scope: BTreeMap<String, String> = BTreeMap::new();
let renderable = ScopeRenderable::new(scope)
.with_title("Test")
.with_sort_keys(false)
.with_indent_guides(true)
.with_max_length(Some(10))
.with_max_string(Some(80))
.with_overflow(OverflowMethod::Crop)
.with_max_depth(5);
assert_eq!(renderable.title, Some("Test".to_string()));
assert!(!renderable.sort_keys);
assert!(renderable.indent_guides);
assert_eq!(renderable.max_length, Some(10));
assert_eq!(renderable.max_string, Some(80));
assert_eq!(renderable.overflow, Some(OverflowMethod::Crop));
assert_eq!(renderable.max_depth, Some(5));
}
#[test]
fn test_scope_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<ScopeRenderable>();
assert_sync::<ScopeRenderable>();
}
#[test]
fn test_scope_debug() {
let mut scope: BTreeMap<String, String> = BTreeMap::new();
scope.insert("x".to_string(), "1".to_string());
let renderable = ScopeRenderable::new(scope);
let debug_str = format!("{:?}", renderable);
assert!(debug_str.contains("ScopeRenderable"));
assert!(debug_str.contains("scope_len"));
}
}