diff --git a/.gitignore b/.gitignore index 9181300..8999b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target +/debug.log /test.log diff --git a/Cargo.lock b/Cargo.lock index af62be3..2e97599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -149,6 +158,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cc" +version = "1.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -317,6 +335,7 @@ dependencies = [ "anyhow", "base64", "better-panic", + "cc", "clap", "crossterm", "log", @@ -325,6 +344,8 @@ dependencies = [ "serde", "simple-logging", "tokio", + "tree-sitter", + "tree-sitter-php", "tui-input", "xmlem", "xmltree", @@ -801,6 +822,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -877,6 +927,19 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "servo_arc" version = "0.4.0" @@ -886,6 +949,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -970,6 +1039,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "strsim" version = "0.11.1" @@ -1091,6 +1166,36 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tree-sitter" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ac5ea5e7f2f1700842ec071401010b9c59bf735295f6e9fa079c3dc035b167" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" + +[[package]] +name = "tree-sitter-php" +version = "0.23.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f066e94e9272cfe4f1dcb07a1c50c66097eca648f2d7233d299c8ae9ed8c130c" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tui-input" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index c3227f5..1cc77f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ name = "debug-tui" version = "0.1.0" edition = "2021" +[build-dependencies] +cc="*" [dependencies] anyhow = "1.0.97" base64 = "0.22.1" @@ -15,6 +17,8 @@ ratatui = "0.29.0" serde = { version = "1.0.219", features = ["derive"] } simple-logging = "2.0.2" tokio = { version = "1.44.1", features = ["full"] } +tree-sitter = "0.25.3" +tree-sitter-php = "0.23.11" tui-input = "0.11.1" xmlem = "0.3.3" xmltree = "0.11.0" diff --git a/README.md b/README.md index f927610..e3e9c2e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Interactive XDebug step debugger for your terminal with vim-like key bindings. - **Travel backwards**: it's not quite time travel, but you can revisit previous steps in _history mode_. - **Vim-like motions**: Typing `100n` will repeat "step into" 100 times. +- **Inline values**: Show variable values inline with the source code. ## Installation @@ -33,6 +34,8 @@ Prefix with number to repeat: - `J` scroll down 10 - `k` scroll up - `K` scroll up 10 +- `+` increase context depth +- `-` decrease context depth - `tab` switch pane - `enter` toggle pane focus (full screen) - `?` Show help diff --git a/src/analyzer.rs b/src/analyzer.rs new file mode 100644 index 0000000..ff6bd70 --- /dev/null +++ b/src/analyzer.rs @@ -0,0 +1,170 @@ +use std::collections::HashMap; + +use anyhow::Result; +use tree_sitter::{Node, Parser, Tree}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Value { + pub value: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Position { + pub row: usize, + pub char: usize, +} + +impl Position { + pub fn new(row: usize, column: usize) -> Self { + Self { row, char: column } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Range { + pub start: Position, + pub end: Position, +} + +impl Range { + + pub fn new(start: Position, end: Position) -> Self { + Range { start, end } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct VariableRef { + pub range: Range, + pub name: String, + pub value: Option, +} + +// variable's start char is the key +type Row = HashMap; + +#[derive(Clone, Debug)] +pub struct Analysis { + rows: HashMap, +} + +impl Analysis { + fn register(&mut self, variable: VariableRef) { + let line = self.rows.entry(variable.range.end.row).or_default(); + line.insert(variable.range.start.char, variable); + } + + pub fn row(&self, number: usize) -> Row { + let value = self.rows.get(&number); + if value.is_none() { + return Row::new(); + } + value.unwrap().clone() + } + + fn new() -> Self { + Self{ + rows: HashMap::new(), + } + } +} + +pub struct Analyser { + analysis: Analysis, +} + +impl Default for Analyser { + fn default() -> Self { + Self::new() + } +} + +impl Analyser { + pub fn analyze(&mut self, source: &str) -> Result { + self.analysis = Analysis::new(); + let tree = self.parse(source); + self.walk(&tree.root_node(), source); + + Ok(self.analysis.clone()) + } + + fn parse(&mut self, source: &str) -> Tree{ + let mut parser = Parser::new(); + let language = tree_sitter_php::LANGUAGE_PHP; + parser.set_language(&language.into()).unwrap(); + + parser.parse(source, None).unwrap() + + } + + fn walk(&mut self, node: &Node, source: &str) { + let count = node.child_count(); + if node.kind() == "variable_name" { + let var_ref = VariableRef{ + name: node.utf8_text(source.as_bytes()).unwrap().to_string(), + range: Range{ + start: Position {row: node.start_position().row, char: node.start_position().column}, + end: Position {row: node.end_position().row, char: node.end_position().column}, + }, + value: None, + }; + self.analysis.register(var_ref); + } + + for index in 0..count { + let child = node.child(index).unwrap(); + self.walk(&child, source); + } + } + + pub fn new() -> Self { + Self { analysis: Analysis { rows: HashMap::new() } } + } +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_analyse_vars() -> Result<(), anyhow::Error> { + let source = r#" Result<(), anyhow::Error> { + let source = r#"; + #[derive(Clone, Debug)] pub struct HistoryEntry { pub source: SourceContext, pub stack: StackGetResponse, pub context: ContextGetResponse, } +impl HistoryEntry { + // todo: this is inefficient! + pub(crate) fn get_property(&self, name: &str) -> Option<&Property> { + self.context.properties.iter().find(|&property| property.name == name) + } +} pub struct History { pub entries: Vec, @@ -139,6 +152,8 @@ pub struct App { pub snapshot_notify: Arc, pub context_depth: u8, + + pub analyzed_files: AnalyzedFiles, } impl App { @@ -164,6 +179,8 @@ impl App { session_view: SessionViewState::new(), snapshot_notify: Arc::new(Notify::new()), + + analyzed_files: HashMap::new(), } } @@ -288,6 +305,7 @@ impl App { self.view_current = CurrentView::Session; self.session_view.mode = SessionViewMode::Current; let source = client.source(response.fileuri.clone()).await.unwrap(); + self.history = History::default(); self.history.push_source(response.fileuri.clone(), source); } @@ -316,10 +334,10 @@ impl App { ).await?; }, AppEvent::ScrollSource(amount) => { - self.session_view.source_scroll = self + self.session_view.source_scroll = Some(self .session_view - .source_scroll - .saturating_add_signed(amount * self.take_motion() as i16); + .source_scroll.unwrap_or(0) + .saturating_add(amount * self.take_motion() as i16)); } AppEvent::ScrollContext(amount) => { self.session_view.context_scroll = self @@ -468,6 +486,13 @@ impl App { line_no, }; let context = client.deref_mut().context_get().await.unwrap(); + match self.analyzed_files.entry(source.filename.clone()) { + Entry::Occupied(_) => (), + Entry::Vacant(vacant_entry) => { + let mut analyser = Analyser::new(); + vacant_entry.insert(analyser.analyze(source.source.as_str()).unwrap()); + } + }; let entry = HistoryEntry { source, stack, diff --git a/src/dbgp/client.rs b/src/dbgp/client.rs index 1d500a6..4a1a0d7 100644 --- a/src/dbgp/client.rs +++ b/src/dbgp/client.rs @@ -1,8 +1,9 @@ use anyhow::Result; -use log::{debug}; use base64::engine::general_purpose; use base64::Engine; use core::str; +use std::fmt::Display; +use log::debug; use tokio::io::AsyncBufReadExt; use tokio::io::AsyncWriteExt; use tokio::io::BufReader; @@ -37,6 +38,58 @@ pub struct ContextGetResponse { pub properties: Vec, } +#[derive(PartialEq, Clone, Debug)] +pub enum PropertyType { + Bool, + Int, + Float, + String, + Null, + Array, + Hash, + Object, + Resource, + Undefined, +} + +impl Display for PropertyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl PropertyType { + pub fn as_str(&self) -> &str { + match self { + PropertyType::Bool => "bool", + PropertyType::Int => "int", + PropertyType::Float => "float", + PropertyType::String => "string", + PropertyType::Null => "null", + PropertyType::Array => "array", + PropertyType::Hash => "hash", + PropertyType::Object => "object", + PropertyType::Resource => "resource", + PropertyType::Undefined => "undefined", + } + } + + fn from_str(expect: &str) -> PropertyType { + match expect { + "bool" => Self::Bool, + "int" => Self::Int, + "float" => Self::Float, + "string" => Self::String, + "null" => Self::Null, + "array" => Self::Array, + "hash" => Self::Hash, + "object" => Self::Object, + "resource" => Self::Resource, + _ => Self::Undefined, + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct Property { pub name: String, @@ -44,7 +97,7 @@ pub struct Property { pub classname: Option, pub page: Option, pub pagesize: Option, - pub property_type: String, + pub property_type: PropertyType, pub facet: Option, pub size: Option, pub children: Vec, @@ -53,6 +106,20 @@ pub struct Property { pub encoding: Option, pub value: Option, } +impl Property { + pub(crate) fn type_name(&self) -> String { + match self.property_type { + PropertyType::Object => self.classname.clone().unwrap_or("object".to_string()), + _ => self.property_type.to_string(), + } + } + pub(crate) fn value_is(&self, value: &str) -> bool { + match &self.value { + Some(v) => value == *v, + None => false, + } + } +} #[derive(Clone, Debug)] pub enum ContinuationStatus { @@ -69,19 +136,20 @@ pub struct ContinuationResponse { #[derive(Debug, Clone)] pub struct StackGetResponse { - pub entries: Vec + pub entries: Vec, } impl StackGetResponse { pub fn depth(&self) -> usize { self.entries.len() } - } impl StackGetResponse { pub fn top(&self) -> &StackEntry { - self.entries.first().expect("Expected at least one stack entry") + self.entries + .first() + .expect("Expected at least one stack entry") } pub(crate) fn top_or_none(&self) -> Option<&StackEntry> { @@ -156,7 +224,10 @@ impl DbgpClient { } pub(crate) async fn feature_set(&mut self, feature: &str, value: &str) -> Result<()> { - match self.command("feature_set", &mut vec!["-n",feature,"-v",value]).await? { + match self + .command("feature_set", &mut vec!["-n", feature, "-v", value]) + .await? + { Message::Response(r) => match r.command { CommandResponse::Unknown => Ok(()), _ => anyhow::bail!("Unexpected response"), @@ -246,9 +317,9 @@ impl DbgpClient { .map_err(anyhow::Error::from) } - pub(crate) async fn disonnect(&mut self) ->Result<(), anyhow::Error>{ + pub(crate) async fn disonnect(&mut self) -> Result<(), anyhow::Error> { if let Some(s) = &mut self.stream { - let res = s.shutdown().await.or_else(|e|anyhow::bail!(e.to_string())); + let res = s.shutdown().await.or_else(|e| anyhow::bail!(e.to_string())); self.stream = None; return res; }; @@ -319,41 +390,53 @@ fn parse_context_get(element: &mut Element) -> Result().unwrap()), - pagesize: child.attributes + page: child + .attributes + .get("page") + .map(|s| s.parse::().unwrap()), + pagesize: child + .attributes .get("pagesize") .map(|s| s.parse::().unwrap()), - property_type: child.attributes - .get("type") - .expect("Expected property_type to be set") - .to_string(), + property_type: PropertyType::from_str( + child + .attributes + .get("type") + .expect("Expected property_type to be set"), + ), facet: child.attributes.get("facet").map(|s| s.to_string()), - size: child.attributes.get("size").map(|s| s.parse::().unwrap()), + size: child + .attributes + .get("size") + .map(|s| s.parse::().unwrap()), key: child.attributes.get("key").map(|name| name.to_string()), address: child.attributes.get("address").map(|name| name.to_string()), encoding: encoding.clone(), children: parse_context_get(&mut child).unwrap().properties, value: match child.children.first() { - Some(XMLNode::CData(cdata)) => Some( - match encoding { - Some(encoding) => match encoding.as_str() { - "base64" => String::from_utf8(general_purpose::STANDARD.decode(cdata).unwrap()).unwrap(), - _ => cdata.to_string(), - }, + Some(XMLNode::CData(cdata)) => Some(match encoding { + Some(encoding) => match encoding.as_str() { + "base64" => { + String::from_utf8(general_purpose::STANDARD.decode(cdata).unwrap()) + .unwrap() + } _ => cdata.to_string(), - } - ), + }, + _ => cdata.to_string(), + }), _ => None, - } + }, }; properties.push(p); } @@ -371,30 +454,28 @@ fn parse_stack_get(element: &Element) -> StackGetResponse { continue; } let entry = StackEntry { - filename: stack_el - .attributes - .get("filename") - .expect("Expected status to be set") - .to_string(), - line: stack_el - .attributes - .get("lineno") - .expect("Expected lineno to be set") - .parse() - .unwrap(), + filename: stack_el + .attributes + .get("filename") + .expect("Expected status to be set") + .to_string(), + line: stack_el + .attributes + .get("lineno") + .expect("Expected lineno to be set") + .parse() + .unwrap(), }; entries.push(entry); } - StackGetResponse { entries} + StackGetResponse { entries } } fn parse_continuation_response( attributes: &std::collections::HashMap, ) -> ContinuationResponse { - let status = attributes - .get("status") - .expect("Expected status to be set"); + let status = attributes.get("status").expect("Expected status to be set"); ContinuationResponse { status: match status.as_str() { "break" => ContinuationStatus::Break, @@ -411,7 +492,7 @@ fn parse_continuation_response( #[cfg(test)] mod test { use super::*; - use pretty_assertions::{assert_eq, assert_ne}; + use pretty_assertions::assert_eq; #[test] fn test_parse_xml() -> Result<(), anyhow::Error> { @@ -511,6 +592,7 @@ function call_function(string $hello) { + "#, )?; @@ -527,7 +609,7 @@ function call_function(string $hello) { classname: None, page: None, pagesize: None, - property_type: "string".to_string(), + property_type: PropertyType::String, facet: None, size: Some(3), children: vec![], @@ -542,7 +624,7 @@ function call_function(string $hello) { classname: None, page: None, pagesize: None, - property_type: "float".to_string(), + property_type: PropertyType::Float, facet: None, size: None, children: vec![], @@ -557,7 +639,7 @@ function call_function(string $hello) { classname: None, page: None, pagesize: None, - property_type: "int".to_string(), + property_type: PropertyType::Int, facet: None, size: None, children: vec![], @@ -572,7 +654,7 @@ function call_function(string $hello) { classname: None, page: None, pagesize: None, - property_type: "bool".to_string(), + property_type: PropertyType::Bool, facet: None, size: None, children: vec![], @@ -587,7 +669,7 @@ function call_function(string $hello) { classname: Some("Foo".to_string()), page: Some(0), pagesize: Some(32), - property_type: "object".to_string(), + property_type: PropertyType::Object, facet: None, size: None, children: vec![ @@ -597,7 +679,7 @@ function call_function(string $hello) { classname: None, page: None, pagesize: None, - property_type: "bool".to_string(), + property_type: PropertyType::Bool, facet: Some("public".to_string()), size: None, children: vec![], @@ -612,7 +694,7 @@ function call_function(string $hello) { classname: None, page: None, pagesize: None, - property_type: "string".to_string(), + property_type: PropertyType::String, facet: Some("public".to_string()), size: Some(3), children: vec![], @@ -620,7 +702,22 @@ function call_function(string $hello) { address: None, encoding: Some("base64".to_string()), value: Some("foo".to_string()), - } + }, + Property { + name: "handle".to_string(), + fullname: "handle".to_string(), + classname: None, + page: None, + pagesize: None, + property_type: PropertyType::Resource, + facet: Some("private".to_string()), + size: None, + children: vec![], + key: None, + address: None, + encoding: None, + value: Some("resource id='18' type='stream'".to_string()), + }, ], key: None, address: None, diff --git a/src/main.rs b/src/main.rs index 1888fe4..a0f6ac9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ pub mod dbgp; pub mod event; pub mod notification; pub mod view; +pub mod analyzer; use app::App; use better_panic::Settings; diff --git a/src/view/session.rs b/src/view/session.rs index 48fb095..5f81c7c 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -149,7 +149,7 @@ fn build_pane_widget(frame: &mut Frame, app: &App, pane: &Pane, area: Rect, inde pub struct SessionViewState { pub full_screen: bool, - pub source_scroll: u16, + pub source_scroll: Option, pub context_scroll: u16, pub stack_scroll: u16, pub mode: SessionViewMode, @@ -167,7 +167,7 @@ impl SessionViewState { pub fn new() -> Self { Self { full_screen: false, - source_scroll: 0, + source_scroll: None, context_scroll: 0, stack_scroll: 0, current_pane: 0, @@ -200,7 +200,7 @@ impl SessionViewState { pub(crate) fn reset(&mut self) { self.context_scroll = 0; - self.source_scroll = 0; + self.source_scroll = None; } } diff --git a/src/view/source.rs b/src/view/source.rs index afea586..665c617 100644 --- a/src/view/source.rs +++ b/src/view/source.rs @@ -1,7 +1,11 @@ +use super::View; use crate::app::App; +use crate::dbgp::client::Property; +use crate::dbgp::client::PropertyType; use crate::event::input::AppEvent; use ratatui::layout::Constraint; use ratatui::layout::Layout; +use ratatui::layout::Position; use ratatui::layout::Rect; use ratatui::style::Color; use ratatui::style::Style; @@ -10,10 +14,7 @@ use ratatui::text::Span; use ratatui::widgets::Paragraph; use ratatui::Frame; -use super::View; - -pub struct SourceComponent { -} +pub struct SourceComponent {} impl View for SourceComponent { fn handle(_: &App, event: AppEvent) -> Option { @@ -25,8 +26,8 @@ impl View for SourceComponent { } fn draw(app: &App, frame: &mut Frame, area: Rect) { - let source_context = match app.history.current() { - Some(s) => &s.source, + let history_entry = match app.history.current() { + Some(s) => s, None => return, }; @@ -36,28 +37,234 @@ impl View for SourceComponent { .constraints(constraints) .split(area); + let mut annotations = vec![]; let mut lines: Vec = Vec::new(); - let mut line_no = 1; - for line in source_context.source.lines() { + let analysis = app + .analyzed_files + .get(&history_entry.source.filename.to_string()); + + for (line_no, line) in history_entry.source.source.lines().enumerate() { + let is_current_line = history_entry.source.line_no == line_no as u32+ 1; + lines.push(Line::from(vec![ Span::styled( format!("{:<6}", line_no), Style::default().fg(Color::Yellow), ), - match source_context.line_no == line_no { + match is_current_line { + // highlight the current line true => Span::styled(line.to_string(), Style::default().bg(Color::Blue)), false => Span::styled(line.to_string(), Style::default().fg(Color::White)), }, ])); - line_no += 1; + // record annotations to add at the end of the line + let mut labels = vec![Span::raw("// ").style(Style::default().fg(Color::DarkGray))]; + + if is_current_line { + if let Some(analysis) = analysis { + for (_, var) in analysis.row(line_no) { + let property = history_entry.get_property(var.name.as_str()); + if property.is_none() { + continue; + } + match render_label(property.unwrap()) { + Some(label) => labels.push(Span::raw(label)), + None => continue, + }; + labels.push(Span::raw(",").style(Style::default().fg(Color::DarkGray))); + } + if labels.len() > 1 { + labels.pop(); + annotations.push((line_no + 1, line.len() + 8, Line::from(labels).style(Style::default().fg(Color::DarkGray)))); + } + } + } } - if source_context.line_no as u16 > area.height { - let offset = (source_context.line_no as u16).saturating_sub(area.height.div_ceil(2)); - lines = lines[offset as usize..].to_vec(); + + + let scroll:u16 = if history_entry.source.line_no as u16 > area.height { + let center = (history_entry.source.line_no as u16).saturating_sub(area.height.div_ceil(2)) as i16; + center.saturating_add(app.session_view.source_scroll.unwrap_or(0)).max(0) as u16 + } else { + app.session_view.source_scroll.unwrap_or(0).max(0) as u16 + }; + + frame.render_widget( + Paragraph::new(lines.clone()).scroll((scroll, 0)), + rows[0], + ); + + for (line_no, line_length, line) in annotations { + let position = Position { + x: line_length as u16, + y: (line_no as u32).saturating_sub(scroll as u32) as u16 + 1, + }; + if !rows[0].contains(position) { + continue; + } + + frame + .buffer_mut() + .set_line(position.x, position.y, &line, rows[0].width); } + } +} + +fn render_label(property: &Property) -> Option { + Some(match property.property_type { + PropertyType::Object|PropertyType::Array|PropertyType::Hash => format!("{}{{{}}}", property.type_name(), { + let mut labels: Vec = Vec::new(); + for child in &property.children { + let label = render_label(child); + if label.is_none() { + continue; + } - frame.render_widget(Paragraph::new(lines).scroll((app.session_view.source_scroll, 0)), rows[0]); + labels.push(format!("{}:{}", child.name, label.unwrap())); + } + labels.join(",") + }), + PropertyType::Bool => { + if property.value_is("1") { + String::from("true") + } else { + String::from("false") + } + } + PropertyType::Int => property.value.clone().unwrap_or("".to_string()), + PropertyType::Float => property.value.clone().unwrap_or("".to_string()), + PropertyType::String => format!("\"{}\"", property.value.clone().unwrap_or("".to_string())), + PropertyType::Null => String::from("null"), + PropertyType::Resource => String::from(property.value.clone().unwrap_or("".to_string())), + PropertyType::Undefined => String::from("undefined"), + }) +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_render_label() { + assert_eq!( + Some(String::from("Foo{true:true,bar:\"foo\"}")), + render_label(&create_object()) + ); + assert_eq!( + Some(String::from("true")), + render_label(&create_simple_property(PropertyType::Bool, "1")) + ); + assert_eq!( + Some(String::from("false")), + render_label(&create_simple_property(PropertyType::Bool, "0")) + ); + assert_eq!( + Some(String::from("12")), + render_label(&create_simple_property(PropertyType::Int, "12")) + ); + assert_eq!( + Some(String::from("\"12\"")), + render_label(&create_simple_property(PropertyType::String, "12")) + ); + assert_eq!( + Some(String::from("null")), + render_label(&create_simple_property(PropertyType::Null, "")) + ); + assert_eq!( + Some(String::from("undefined")), + render_label(&create_simple_property(PropertyType::Undefined, "")) + ); + assert_eq!( + Some(String::from("resource id='18' type='stream'")), + render_label(&create_resource()) + ); + } + + fn create_simple_property(property_type: PropertyType, value: &str) -> Property { + Property { + name: "test".to_string(), + fullname: "test".to_string(), + classname: None, + page: None, + pagesize: None, + property_type, + facet: None, + size: None, + children: Vec::new(), + key: None, + address: None, + encoding: None, + value: Some(value.to_string()), + } + } + + fn create_resource() -> Property { + Property { + name: "handle".to_string(), + fullname: "handle".to_string(), + classname: None, + page: None, + pagesize: None, + property_type: PropertyType::Resource, + facet: Some("private".to_string()), + size: None, + children: vec![], + key: None, + address: None, + encoding: None, + value: Some("resource id='18' type='stream'".to_string()), + } + } + + fn create_object() -> Property { + Property { + name: "$this".to_string(), + fullname: "$this".to_string(), + classname: Some("Foo".to_string()), + page: Some(0), + pagesize: Some(32), + property_type: PropertyType::Object, + facet: None, + size: None, + children: vec![ + Property { + name: "true".to_string(), + fullname: "true".to_string(), + classname: None, + page: None, + pagesize: None, + property_type: PropertyType::Bool, + facet: Some("public".to_string()), + size: None, + children: vec![], + key: None, + address: None, + encoding: None, + value: Some("1".to_string()), + }, + Property { + name: "bar".to_string(), + fullname: "bar".to_string(), + classname: None, + page: None, + pagesize: None, + property_type: PropertyType::String, + facet: Some("public".to_string()), + size: Some(3), + children: vec![], + key: None, + address: None, + encoding: Some("base64".to_string()), + value: Some("foo".to_string()), + }, + ], + key: None, + address: None, + encoding: None, + value: None, + } } }