diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a688a..6683456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG main ---- +- Filter properties with dot notation in context pane (`f`) - Stack traversal - select stack and inspect stack frames in current mode. - Fixed light theme. diff --git a/README.md b/README.md index 4edd4eb..23cf980 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Prefix with number to repeat: - `enter` toggle pane focus (full screen) - `t` rotate the theme - `?` Show help +- `f` Filter (context pane) - use dot notation to filter on multiple levels. ## Setting Breakpoints diff --git a/src/app.rs b/src/app.rs index c54c91c..6572986 100644 --- a/src/app.rs +++ b/src/app.rs @@ -169,20 +169,21 @@ impl SourceContext { } #[derive(Debug, Clone)] -pub enum CurrentView { +pub enum SelectedView { Listen, Session, Help, } pub struct App { - pub is_connected: bool, - pub notification: Notification, - pub config: Config, receiver: Receiver, quit: bool, sender: Sender, + pub is_connected: bool, + pub notification: Notification, + pub config: Config, + pub server_status: Option, pub command_input: Input, pub command_response: Option, @@ -191,7 +192,8 @@ pub struct App { pub history: History, - pub view_current: CurrentView, + pub view_current: SelectedView, + pub focus_view: bool, pub session_view: SessionViewState, pub input_plurality: Vec, @@ -229,7 +231,8 @@ impl App { server_status: None, command_input: Input::default(), command_response: None, - view_current: CurrentView::Listen, + view_current: SelectedView::Listen, + focus_view: false, session_view: SessionViewState::new(), snapshot_notify: Arc::new(Notify::new()), @@ -401,6 +404,14 @@ impl App { .feature_set("max_depth", self.context_depth.to_string().as_str()) .await?; } + AppEvent::ContextFilterOpen => { + self.session_view.context_filter.show = true; + self.focus_view = true; + }, + AppEvent::ContextSearchClose => { + self.session_view.context_filter.show = false; + self.focus_view = false; + }, AppEvent::ScrollSource(amount) => { self.session_view.source_scroll = apply_scroll( self.session_view.source_scroll, @@ -440,19 +451,27 @@ impl App { .await?; } AppEvent::PushInputPlurality(char) => self.input_plurality.push(char), - AppEvent::Input(key_event) => match key_event.code { - KeyCode::Char('t') => { - self.theme = self.theme.next(); - self.notification = - Notification::info(format!("Switched to theme: {:?}", self.theme)); - } - KeyCode::Char('?') => { - self.sender - .send(AppEvent::ChangeView(CurrentView::Help)) - .await - .unwrap(); + AppEvent::Input(key_event) => { + if self.focus_view { + // event shandled exclusively by view (e.g. input needs focus) + self.send_event_to_current_view(event).await; + } else { + // global events + match key_event.code { + KeyCode::Char('t') => { + self.theme = self.theme.next(); + self.notification = + Notification::info(format!("Switched to theme: {:?}", self.theme)); + } + KeyCode::Char('?') => { + self.sender + .send(AppEvent::ChangeView(SelectedView::Help)) + .await + .unwrap(); + } + _ => self.send_event_to_current_view(event).await, + } } - _ => self.send_event_to_current_view(event).await, }, _ => self.send_event_to_current_view(event).await, }; @@ -520,9 +539,9 @@ impl App { // route the event to the currently selected view async fn send_event_to_current_view(&mut self, event: AppEvent) { let subsequent_event = match self.view_current { - CurrentView::Help => HelpView::handle(self, event), - CurrentView::Listen => ListenView::handle(self, event), - CurrentView::Session => SessionView::handle(self, event), + SelectedView::Help => HelpView::handle(self, event), + SelectedView::Listen => ListenView::handle(self, event), + SelectedView::Session => SessionView::handle(self, event), }; if let Some(event) = subsequent_event { self.sender.send(event).await.unwrap() @@ -611,7 +630,7 @@ impl App { fn reset(&mut self) { self.server_status = None; - self.view_current = CurrentView::Session; + self.view_current = SelectedView::Session; self.session_view.mode = SessionViewMode::Current; self.analyzed_files = HashMap::new(); self.workspace.reset(); diff --git a/src/dbgp/client.rs b/src/dbgp/client.rs index 0367690..748bb75 100644 --- a/src/dbgp/client.rs +++ b/src/dbgp/client.rs @@ -38,11 +38,12 @@ pub struct ContextGetResponse { pub properties: Vec, } -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Default)] pub enum PropertyType { Bool, Int, Float, + #[default] String, Null, Array, @@ -90,7 +91,7 @@ impl PropertyType { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct Property { pub name: String, pub fullname: String, diff --git a/src/event/input.rs b/src/event/input.rs index 28b9d58..92cd52e 100644 --- a/src/event/input.rs +++ b/src/event/input.rs @@ -11,7 +11,7 @@ use std::time::Duration; use tokio::net::TcpStream; use tokio::sync::mpsc::Sender; -use crate::app::CurrentView; +use crate::app::SelectedView; use crate::dbgp::client::ContinuationStatus; use crate::view::session::SessionViewMode; use crate::view::Scroll; @@ -19,7 +19,7 @@ use crate::view::Scroll; #[derive(Debug)] pub enum AppEvent { ChangeSessionViewMode(SessionViewMode), - ChangeView(CurrentView), + ChangeView(SelectedView), ClientConnected(TcpStream), Disconnect, HistoryNext, @@ -47,6 +47,8 @@ pub enum AppEvent { PushInputPlurality(char), ContextDepth(i8), NextTheme, + ContextFilterOpen, + ContextSearchClose, } pub type EventSender = Sender; diff --git a/src/theme.rs b/src/theme.rs index 2f3b356..f7471c2 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -61,6 +61,7 @@ impl Theme { widget_mode_history: Style::default().fg(Solarized::Red.to_color()).bg(Solarized::Base03.to_color()), background: Style::default().bg(Color::Black), + cursor: Style::default().bg(Color::White), }, Theme::Dark => Scheme { syntax_variable: Style::default().fg(Color::LightBlue), @@ -89,6 +90,7 @@ impl Theme { widget_mode_debug: Style::default().bg(Color::Blue), widget_mode_history: Style::default().bg(Color::Red), background: Style::default().bg(Color::Black), + cursor: Style::default().bg(Color::White) }, } } @@ -121,6 +123,7 @@ pub struct Scheme { pub widget_inactive: Style, pub widget_mode_debug: Style, pub widget_mode_history: Style, + pub cursor: Style, } pub enum Role {} diff --git a/src/view/context.rs b/src/view/context.rs index aea4dea..f703568 100644 --- a/src/view/context.rs +++ b/src/view/context.rs @@ -4,18 +4,45 @@ use crate::dbgp::client::Property; use crate::dbgp::client::PropertyType; use crate::event::input::AppEvent; use crate::theme::Scheme; +use crossterm::event::KeyCode; +use ratatui::layout::Constraint; +use ratatui::layout::Layout; use ratatui::layout::Rect; use ratatui::text::Line; use ratatui::text::Span; +use ratatui::widgets::Block; +use ratatui::widgets::Borders; use ratatui::widgets::Paragraph; use ratatui::Frame; +use tui_input::backend::crossterm::EventHandler; pub struct ContextComponent {} impl View for ContextComponent { - fn handle(_app: &App, event: AppEvent) -> Option { + fn handle(app: &mut App, event: AppEvent) -> Option { + if app.session_view.context_filter.show { + return match event { + AppEvent::Input(e) => { + if e.code == KeyCode::Esc { + return Some(AppEvent::ContextSearchClose); + } + if e.code == KeyCode::Enter { + return Some(AppEvent::ContextSearchClose); + } + app.session_view.context_filter.input.handle_event(&crossterm::event::Event::Key(e)); + return None; + }, + _ => None, + } + } match event { AppEvent::Scroll(scroll) => Some(AppEvent::ScrollContext(scroll)), + AppEvent::Input(e) => { + match e.code { + KeyCode::Char('f') => Some(AppEvent::ContextFilterOpen), + _ => None, + } + }, _ => None, } } @@ -33,12 +60,31 @@ impl View for ContextComponent { None => return, }; let mut lines: Vec = vec![]; - draw_properties(&app.theme(), &context.properties, &mut lines, 0); + let layout = Layout::default() + .constraints([Constraint::Length( + if app.session_view.context_filter.show { 3 } else { 0 } + ), Constraint::Min(1)]); + let areas = layout.split(area); + + frame.render_widget(Paragraph::new(Line::from(vec![ + Span::raw(app.session_view.context_filter.input.value()), + Span::raw(" ").style(app.theme().cursor), + ]) + ).block(Block::default().borders(Borders::all())), areas[0]); + + let mut filter_path = app.session_view.context_filter.segments().clone(); + draw_properties( + &app.theme(), + &context.properties, + &mut lines, + 0, + &mut filter_path, + ); + frame.render_widget( Paragraph::new(lines).scroll(app.session_view.context_scroll), - - area, + areas[1], ); } } @@ -48,8 +94,18 @@ pub fn draw_properties( properties: &Vec, lines: &mut Vec, level: usize, + filter_path: &mut Vec<&str>, ) { + let filter = filter_path.pop(); + println!("{:?}", filter_path); + for property in properties { + if let Some(filter) = filter { + println!("{} = {}", property.name, filter); + if !property.name.contains(filter) { + continue; + } + } let mut spans = vec![ Span::raw(" ".repeat(level)), Span::styled(property.name.to_string(), theme.syntax_label), @@ -77,7 +133,7 @@ pub fn draw_properties( lines.push(Line::from(spans)); if !property.children.is_empty() { - draw_properties(theme, &property.children, lines, level + 1); + draw_properties(theme, &property.children, lines, level + 1, filter_path); lines.push(Line::from(vec![Span::raw(delimiters.1)]).style(theme.syntax_brace)); } } @@ -98,3 +154,97 @@ pub fn render_value<'a>(theme: &Scheme, property: &Property) -> Span<'a> { _ => Span::styled(value, theme.syntax_literal), } } + +#[cfg(test)] +mod test { + use crate::theme::Theme; + use anyhow::Result; + + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_draw_properties_empty() -> Result<()> { + let mut lines = vec![]; + draw_properties( + &Theme::SolarizedDark.scheme(), + &Vec::new(), + &mut lines, + 0, + &mut Vec::new(), + ); + assert_eq!(0, lines.len()); + Ok(()) + } + + #[test] + fn test_draw_properties_two_levels() -> Result<()> { + let mut lines = vec![]; + let mut prop1 = Property::default(); + let mut prop2 = Property::default(); + prop2.name = "bar".to_string(); + prop1.children = vec![ + prop2 + ]; + prop1.name = "foo".to_string(); + + draw_properties( + &Theme::SolarizedDark.scheme(), + &vec![ + prop1 + ], + &mut lines, + 0, + &mut Vec::new(), + ); + assert_eq!(vec![ + "foo string = \"\"{", + " bar string = \"\"", + "}", + ], lines.iter().map( + |l| { l.to_string()} + ).collect::>()); + Ok(()) + } + + #[test] + fn test_filter_property_multiple_level() -> Result<()> { + let mut lines = vec![]; + let mut prop1 = Property::default(); + let mut prop2 = Property::default(); + let prop3 = Property::default(); + + prop2.name = "bar".to_string(); + prop1.children = vec![ + prop2 + ]; + prop1.name = "foo".to_string(); + + // segments are reversed + let mut filter = &mut vec![ + "bar", + "foo", + ]; + + draw_properties( + &Theme::SolarizedDark.scheme(), + &vec![ + prop1, + prop3 + ], + &mut lines, + 0, + &mut filter, + ); + + assert_eq!(vec![ + "foo string = \"\"{", + " bar string = \"\"", + "}", + ], lines.iter().map( + |l| { l.to_string()} + ).collect::>()); + + Ok(()) + } +} diff --git a/src/view/help.rs b/src/view/help.rs index 6908d4d..6c736ff 100644 --- a/src/view/help.rs +++ b/src/view/help.rs @@ -1,5 +1,5 @@ use super::View; -use crate::app::{App, CurrentView}; +use crate::app::{App, SelectedView}; use crate::event::input::AppEvent; use ratatui::layout::Rect; use ratatui::widgets::Paragraph; @@ -8,13 +8,13 @@ use ratatui::Frame; pub struct HelpView {} impl View for HelpView { - fn handle(app: &App, event: AppEvent) -> Option { + fn handle(app: &mut App, event: AppEvent) -> Option { match event { AppEvent::Input(_) => { if app.is_connected { - Some(AppEvent::ChangeView(CurrentView::Session)) + Some(AppEvent::ChangeView(SelectedView::Session)) } else { - Some(AppEvent::ChangeView(CurrentView::Listen)) + Some(AppEvent::ChangeView(SelectedView::Listen)) } }, _ => None @@ -50,6 +50,7 @@ Key mappings (prefix with number to repeat): [+] increase context depth [-] decrease context depth [t] rotate the theme +[f] Filter (context pane) - use dot notation to filter on multiple levels. [enter] toggle pane focus (full screen) Legend: diff --git a/src/view/layout.rs b/src/view/layout.rs index 7fffbd2..b2acbfe 100644 --- a/src/view/layout.rs +++ b/src/view/layout.rs @@ -4,7 +4,7 @@ use super::session::SessionView; use super::session::SessionViewMode; use super::View; use crate::app::App; -use crate::app::CurrentView; +use crate::app::SelectedView; use crate::event::input::AppEvent; use crate::notification::NotificationLevel; use ratatui::layout::Constraint; @@ -20,7 +20,7 @@ use ratatui::Frame; pub struct LayoutView {} impl View for LayoutView { - fn handle(_app: &App, _key: AppEvent) -> Option { + fn handle(_app: &mut App, _key: AppEvent) -> Option { None } @@ -36,9 +36,9 @@ impl View for LayoutView { f.render_widget(status_widget(app), rows[0]); match app.view_current { - CurrentView::Listen => ListenView::draw(app, f, rows[1]), - CurrentView::Session => SessionView::draw(app, f, rows[1]), - CurrentView::Help => HelpView::draw(app, f, rows[1]), + SelectedView::Listen => ListenView::draw(app, f, rows[1]), + SelectedView::Session => SessionView::draw(app, f, rows[1]), + SelectedView::Help => HelpView::draw(app, f, rows[1]), } } } diff --git a/src/view/listen.rs b/src/view/listen.rs index d73c1dc..dc50d95 100644 --- a/src/view/listen.rs +++ b/src/view/listen.rs @@ -6,7 +6,7 @@ use ratatui::Frame; pub struct ListenView {} impl View for ListenView { - fn handle(_app: &App, _key: AppEvent) -> Option { + fn handle(_app: &mut App, _key: AppEvent) -> Option { None } diff --git a/src/view/mod.rs b/src/view/mod.rs index 621969b..62c1c19 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -13,7 +13,7 @@ use ratatui::layout::{Constraint, Rect}; use ratatui::Frame; pub trait View { - fn handle(app: &App, event: AppEvent) -> Option; + fn handle(app: &mut App, event: AppEvent) -> Option; fn draw(app: &App, frame: &mut Frame, area: Rect); } diff --git a/src/view/session.rs b/src/view/session.rs index 59dd6ca..2376425 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -5,7 +5,7 @@ use super::ComponentType; use super::Pane; use super::View; use crate::app::App; -use crate::app::CurrentView; +use crate::app::SelectedView; use crate::event::input::AppEvent; use crossterm::event::KeyCode; use ratatui::layout::Constraint; @@ -19,12 +19,16 @@ use ratatui::Frame; pub struct SessionView {} impl View for SessionView { - fn handle(app: &App, event: AppEvent) -> Option { + fn handle(app: &mut App, event: AppEvent) -> Option { let input_event = match event { AppEvent::Input(key_event) => key_event, _ => return delegate_event_to_pane(app, event), }; - + + if app.focus_view { + return delegate_event_to_pane(app, event); + } + // handle global session events match input_event.code { KeyCode::Tab => return Some(AppEvent::NextPane), @@ -60,7 +64,7 @@ impl View for SessionView { _ => None, }, SessionViewMode::History => match input_event.code { - KeyCode::Esc => Some(AppEvent::ChangeView(CurrentView::Session)), + KeyCode::Esc => Some(AppEvent::ChangeView(SelectedView::Session)), KeyCode::Char(c) => match c { 'n' => Some(AppEvent::HistoryNext), 'p' => Some(AppEvent::HistoryPrevious), @@ -113,7 +117,7 @@ impl View for SessionView { } } -fn delegate_event_to_pane(app: &App, event: AppEvent) -> Option { +fn delegate_event_to_pane(app: &mut App, event: AppEvent) -> Option { let focused_pane = app.session_view.current_pane(); match focused_pane.component_type { @@ -128,10 +132,20 @@ fn build_pane_widget(frame: &mut Frame, app: &App, pane: &Pane, area: Rect, inde .borders(Borders::all()) .title(match pane.component_type { ComponentType::Source => match app.history.current() { - Some(c) => c.source(app.session_view.stack_depth()).filename.to_string(), + Some(c) => c + .source(app.session_view.stack_depth()) + .filename + .to_string(), None => "".to_string(), }, - ComponentType::Context => format!("Context(fetch-depth: {})", app.context_depth), + ComponentType::Context => format!( + "Context(fetch-depth: {}, filter: {})", + app.context_depth, + match app.session_view.context_filter.input.value() { + "" => "n/a", + _ => app.session_view.context_filter.input.value(), + } + ), ComponentType::Stack => format!( "Stack({}/{}, fetch-depth: {})", app.session_view.stack_depth(), @@ -163,10 +177,23 @@ fn build_pane_widget(frame: &mut Frame, app: &App, pane: &Pane, area: Rect, inde }; } +pub struct SearchState { + pub show: bool, + pub search: String, + pub input: tui_input::Input, +} + +impl SearchState { + pub(crate) fn segments(&self) -> Vec<&str> { + self.input.value().rsplit(".").collect() + } +} + pub struct SessionViewState { pub full_screen: bool, pub source_scroll: (u16, u16), pub context_scroll: (u16, u16), + pub context_filter: SearchState, pub stack_scroll: (u16, u16), pub mode: SessionViewMode, pub panes: Vec, @@ -185,6 +212,11 @@ impl SessionViewState { full_screen: false, source_scroll: (0, 0), context_scroll: (0, 0), + context_filter: SearchState { + show: false, + search: String::new(), + input: tui_input::Input::default(), + }, stack_scroll: (0, 0), current_pane: 0, mode: SessionViewMode::Current, diff --git a/src/view/source.rs b/src/view/source.rs index 1ac1af1..e504adb 100644 --- a/src/view/source.rs +++ b/src/view/source.rs @@ -15,7 +15,7 @@ use ratatui::Frame; pub struct SourceComponent {} impl View for SourceComponent { - fn handle(_: &App, event: AppEvent) -> Option { + fn handle(_: &mut App, event: AppEvent) -> Option { match event { AppEvent::Scroll(amount) => Some(AppEvent::ScrollSource(amount)), _ => None, diff --git a/src/view/stack.rs b/src/view/stack.rs index c88c175..f8d3ab3 100644 --- a/src/view/stack.rs +++ b/src/view/stack.rs @@ -10,7 +10,7 @@ use ratatui::Frame; pub struct StackComponent {} impl View for StackComponent { - fn handle(_: &App, event: AppEvent) -> Option { + fn handle(_: &mut App, event: AppEvent) -> Option { match event { AppEvent::Scroll(amount) => Some(AppEvent::ScrollStack(amount)), _ => None,