diff --git a/CHANGELOG.md b/CHANGELOG.md index 20477e7..65a688a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +main +---- + +- Stack traversal - select stack and inspect stack frames in current mode. +- Fixed light theme. + 0.0.4 ----- diff --git a/README.md b/README.md index 58e4672..4edd4eb 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Interactive [Xdebug](https://xdebug.org) step-debugging client your terminal. - **Travel forwards**: step over, into and out. - **Travel backwards**: it's not quite time travel, but you can revisit previous steps in _history mode_. +- **Jump the stack**: jump up and down the stack. - **Vim-like motions**: Typing `100n` will repeat "step into" 100 times. - **Inline values**: Show variable values inline with the source code. @@ -30,14 +31,14 @@ Prefix with number to repeat: - `N` step over - `p` previous (switches to history mode if in current mode) - `o` step out -- `j` scroll down -- `J` scroll down 10 -- `k` scroll up -- `K` scroll up 10 -- `h` scroll left -- `H` scroll left 10 -- `l` scroll right -- `L` scroll right 10 +- `j` down +- `J` down 10 +- `k` up +- `K` up 10 +- `h` left +- `H` left 10 +- `l` right +- `L` right 10 - `+` increase context depth - `-` decrease context depth - `tab` switch pane diff --git a/src/app.rs b/src/app.rs index c7393ca..ac40943 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,6 @@ use crate::dbgp::client::ContinuationResponse; use crate::dbgp::client::ContinuationStatus; use crate::dbgp::client::DbgpClient; use crate::dbgp::client::Property; -use crate::dbgp::client::StackGetResponse; use crate::event::input::AppEvent; use crate::notification::Notification; use crate::theme::Scheme; @@ -30,7 +29,6 @@ use ratatui::widgets::Block; use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::Terminal; -use tokio::sync::Notify; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io; @@ -40,21 +38,65 @@ use tokio::net::TcpListener; use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Sender; use tokio::sync::Mutex; +use tokio::sync::Notify; use tokio::task; use tui_input::Input; -type AnalyzedFiles = HashMap; +type AnalyzedFiles = HashMap; #[derive(Clone, Debug)] -pub struct HistoryEntry { +pub struct StackFrame { + pub level: u16, pub source: SourceContext, - pub stack: StackGetResponse, - pub context: ContextGetResponse, + pub context: Option, } -impl HistoryEntry { - // todo: this is inefficient! +impl StackFrame { pub(crate) fn get_property(&self, name: &str) -> Option<&Property> { - self.context.properties.iter().find(|&property| property.name == name) + match &self.context { + Some(c) => c.properties.iter().find(|&property| property.name == name), + None => None, + } + } +} + +#[derive(Clone, Debug)] +pub struct HistoryEntry { + pub stacks: Vec, +} + +impl HistoryEntry { + fn push(&mut self, frame: StackFrame) { + self.stacks.push(frame); + } + fn new() -> Self { + let stacks = Vec::new(); + HistoryEntry { stacks } + } + + fn initial(filename: String, source: String) -> HistoryEntry { + HistoryEntry { + stacks: vec![StackFrame { + level: 0, + source: SourceContext { + source, + filename, + line_no: 0, + }, + context: None, + }], + } + } + + pub fn source(&self, level: u16) -> SourceContext { + let entry = self.stacks.get(level as usize); + match entry { + Some(e) => e.source.clone(), + None => SourceContext::default(), + } + } + + pub(crate) fn stack(&self, stack_depth: u16) -> Option<&StackFrame> { + self.stacks.get(stack_depth as usize) } } @@ -99,22 +141,14 @@ impl History { self.entries.get(self.offset) } + pub(crate) fn current_mut(&mut self) -> Option<&mut HistoryEntry> { + self.entries.get_mut(self.offset) + } + fn push(&mut self, entry: HistoryEntry) { self.entries.push(entry); self.offset = self.entries.len() - 1; } - - fn push_source(&mut self, filename: String, source: String) { - self.push(HistoryEntry { - source: SourceContext { - source, - filename, - line_no: 1, - }, - context: ContextGetResponse { properties: vec![] }, - stack: StackGetResponse { entries: vec![] }, - }); - } } #[derive(Clone, Debug)] @@ -123,6 +157,15 @@ pub struct SourceContext { pub filename: String, pub line_no: u32, } +impl SourceContext { + fn default() -> SourceContext { + SourceContext { + source: "".to_string(), + filename: "".to_string(), + line_no: 0, + } + } +} #[derive(Debug, Clone)] pub enum CurrentView { @@ -157,6 +200,8 @@ pub struct App { pub theme: Theme, pub analyzed_files: AnalyzedFiles, + + pub stack_max_context_fetch: u16, } impl App { @@ -174,6 +219,7 @@ impl App { client: Arc::new(Mutex::new(client)), counter: 0, context_depth: 4, + stack_max_context_fetch: 1, theme: Theme::SolarizedDark, server_status: None, @@ -304,8 +350,8 @@ impl App { let mut client = self.client.lock().await; let response = client.deref_mut().connect(s).await?; for (feature, value) in [ - ("max_depth",self.context_depth.to_string().as_str()), - ("extended_properties","1"), + ("max_depth", self.context_depth.to_string().as_str()), + ("extended_properties", "1"), ] { info!("setting feature {} to {:?}", feature, value); client.feature_set(feature, value).await?; @@ -317,7 +363,8 @@ impl App { let source = client.source(response.fileuri.clone()).await.unwrap(); self.history = History::default(); - self.history.push_source(response.fileuri.clone(), source); + self.history + .push(HistoryEntry::initial(response.fileuri.clone(), source)); } AppEvent::Snapshot() => { self.snapshot().await?; @@ -338,19 +385,33 @@ impl App { AppEvent::ContextDepth(inc) => { let depth = self.context_depth as i8; self.context_depth = depth.wrapping_add(inc).max(0) as u8; - self.client.lock().await.feature_set( - "max_depth", - self.context_depth.to_string().as_str() - ).await?; - }, + self.client + .lock() + .await + .feature_set("max_depth", self.context_depth.to_string().as_str()) + .await?; + } AppEvent::ScrollSource(amount) => { - self.session_view.source_scroll = apply_scroll(self.session_view.source_scroll, amount, self.take_motion() as i16); + self.session_view.source_scroll = apply_scroll( + self.session_view.source_scroll, + amount, + self.take_motion() as i16, + ); } AppEvent::ScrollContext(amount) => { - self.session_view.context_scroll = apply_scroll(self.session_view.context_scroll, amount, self.take_motion() as i16); + self.session_view.context_scroll = apply_scroll( + self.session_view.context_scroll, + amount, + self.take_motion() as i16, + ); } AppEvent::ScrollStack(amount) => { - self.session_view.stack_scroll = apply_scroll(self.session_view.stack_scroll, amount, self.take_motion() as i16); + self.session_view.stack_scroll = apply_scroll( + self.session_view.stack_scroll, + amount, + self.take_motion() as i16, + ); + self.populate_stack_context().await?; } AppEvent::ToggleFullscreen => { self.session_view.full_screen = !self.session_view.full_screen; @@ -369,17 +430,19 @@ 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(); - }, - _ => self.send_event_to_current_view(event).await + 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(); } + _ => self.send_event_to_current_view(event).await, }, _ => self.send_event_to_current_view(event).await, }; @@ -400,7 +463,6 @@ impl App { tokio::spawn(async move { let mut last_response: Option = None; for i in 0..count { - // we need to wait for the snapshot to complete before running a further // continuation. snapshot_notify.notified().await; @@ -477,41 +539,66 @@ impl App { pub async fn snapshot(&mut self) -> Result<()> { let mut client = self.client.lock().await; let stack = client.deref_mut().get_stack().await?; - if let Some(top) = stack.top_or_none() { - let filename = &top.filename; - let line_no = top.line; + let mut entry = HistoryEntry::new(); + for (level, frame) in stack.entries.iter().enumerate() { + let filename = &frame.filename; + let line_no = frame.line; + let context = match (level as u16) < self.stack_max_context_fetch { + true => Some(client.deref_mut().context_get(level as u16).await.unwrap()), + false => None, + }; let source_code = client .deref_mut() .source(filename.to_string()) .await .unwrap(); + let source = SourceContext { source: source_code, filename: filename.to_string(), line_no, }; - let context = client.deref_mut().context_get().await.unwrap(); - match self.analyzed_files.entry(source.filename.clone()) { + + match self.analyzed_files.entry(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 { + + entry.push(StackFrame { + level: (level as u16), source, - stack, context, - }; - self.history.push(entry); - self.session_view.reset(); + }); } + self.history.push(entry); + self.session_view.reset(); Ok(()) } pub(crate) fn theme(&self) -> Scheme { self.theme.scheme() } + + async fn populate_stack_context(&mut self) -> Result<()> { + if !self.history.is_current() { + return Ok(()); + } + let level = self.session_view.stack_scroll.0 as usize; + if let Some(c) = self.history.current_mut() { + let stack = c.stacks.get_mut(level); + if let Some(s) = stack { + if s.context.is_none() { + let mut client = self.client.lock().await; + let context = client.deref_mut().context_get(level as u16).await?; + s.context = Some(context); + } + }; + }; + Ok(()) + } } fn apply_scroll(scroll: (u16, u16), amount: (i16, i16), motion: i16) -> (u16, u16) { diff --git a/src/dbgp/client.rs b/src/dbgp/client.rs index f7a2df7..ea47996 100644 --- a/src/dbgp/client.rs +++ b/src/dbgp/client.rs @@ -151,16 +151,13 @@ impl StackGetResponse { .first() .expect("Expected at least one stack entry") } - - pub(crate) fn top_or_none(&self) -> Option<&StackEntry> { - self.entries.first() - } } #[derive(Debug, Clone)] pub struct StackEntry { pub filename: String, pub line: u32, + pub level: u32, } #[derive(Debug, Clone)] @@ -235,8 +232,8 @@ impl DbgpClient { } } - pub(crate) async fn context_get(&mut self) -> Result { - match self.command("context_get", &mut vec![]).await? { + pub(crate) async fn context_get(&mut self, depth: u16) -> Result { + match self.command("context_get", &mut vec!["-d", format!("{}", depth).as_str()]).await? { Message::Response(r) => match r.command { CommandResponse::ContextGet(s) => Ok(s), _ => anyhow::bail!("Unexpected response"), @@ -481,6 +478,12 @@ fn parse_stack_get(element: &Element) -> StackGetResponse { .expect("Expected lineno to be set") .parse() .unwrap(), + level: stack_el + .attributes + .get("level") + .expect("Expected level to be set") + .parse() + .unwrap(), }; entries.push(entry); } diff --git a/src/view/context.rs b/src/view/context.rs index 5b693fd..aea4dea 100644 --- a/src/view/context.rs +++ b/src/view/context.rs @@ -21,8 +21,15 @@ impl View for ContextComponent { } fn draw(app: &App, frame: &mut Frame, area: Rect) { - let context = match app.history.current() { - Some(e) => &e.context, + let entry = match app.history.current() { + Some(e) => e, + None => return, + }; + let context = match entry.stack(app.session_view.stack_depth()) { + Some(stack) => match &stack.context { + Some(context) => context, + None => return, + }, None => return, }; let mut lines: Vec = vec![]; diff --git a/src/view/layout.rs b/src/view/layout.rs index a5cb9d4..7fffbd2 100644 --- a/src/view/layout.rs +++ b/src/view/layout.rs @@ -63,7 +63,7 @@ fn status_widget(app: &App) -> Paragraph { format!( "  {:<3} ", app.history.current().map_or("n/a".to_string(), |entry| { - entry.stack.depth().to_string() + entry.stacks.len().to_string() }) ), app.theme().widget_inactive, diff --git a/src/view/session.rs b/src/view/session.rs index 61cb66f..5fdc07a 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -127,11 +127,19 @@ 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.filename.to_string(), + Some(c) => c.source(app.session_view.stack_depth()).filename.to_string(), None => "".to_string(), }, - ComponentType::Context => format!("Context({})", app.context_depth), - ComponentType::Stack => "Stack".to_string(), + ComponentType::Context => format!("Context(fetch-depth: {})", app.context_depth), + ComponentType::Stack => format!( + "Stack({}/{}, fetch-depth: {})", + app.session_view.stack_depth(), + match app.history.current() { + Some(e) => e.stacks.len() - 1, + None => 0, + }, + app.stack_max_context_fetch, + ), }) .style(match index == app.session_view.current_pane { true => app.theme().pane_border_active, @@ -210,6 +218,10 @@ impl SessionViewState { self.stack_scroll = (0, 0); self.source_scroll = (0, 0); } + + pub(crate) fn stack_depth(&self) -> u16 { + self.stack_scroll.0 + } } #[derive(Debug, Clone)] diff --git a/src/view/source.rs b/src/view/source.rs index e26434d..1ac1af1 100644 --- a/src/view/source.rs +++ b/src/view/source.rs @@ -37,12 +37,17 @@ impl View for SourceComponent { let mut annotations = vec![]; let mut lines: Vec = Vec::new(); + let stack = match history_entry.stack(app.session_view.stack_depth()) { + None => return, + Some(stack) => stack + }; + let analysis = app .analyzed_files - .get(&history_entry.source.filename.to_string()); + .get(&stack.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; + for (line_no, line) in stack.source.source.lines().enumerate() { + let is_current_line = stack.source.line_no == line_no as u32 + 1; lines.push(Line::from(vec![ Span::styled(format!("{:<6}", line_no), app.theme().source_line_no), @@ -59,7 +64,7 @@ impl View for SourceComponent { 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()); + let property = stack.get_property(var.name.as_str()); if property.is_none() { continue; } @@ -81,8 +86,8 @@ impl View for SourceComponent { } } - let scroll: u16 = if history_entry.source.line_no as u16 > area.height { - let center = (history_entry.source.line_no as u16) + let scroll: u16 = if stack.source.line_no as u16 > area.height { + let center = (stack.source.line_no as u16) .saturating_sub(area.height.div_ceil(2)) as i16; center .saturating_add(app.session_view.source_scroll.0 as i16) @@ -112,11 +117,7 @@ impl View for SourceComponent { frame.render_widget( Paragraph::new(line.clone()).scroll(( 0, - if app.session_view.source_scroll.1 > line_length as u16 { - app.session_view.source_scroll.1 - line_length as u16 - } else { - 0 - }) + app.session_view.source_scroll.1.saturating_sub(line_length as u16)) ), area ); diff --git a/src/view/stack.rs b/src/view/stack.rs index 0150f25..c88c175 100644 --- a/src/view/stack.rs +++ b/src/view/stack.rs @@ -18,22 +18,29 @@ impl View for StackComponent { } fn draw(app: &App, frame: &mut Frame, area: Rect) { - let stack = match app.history.current() { - Some(s) => &s.stack, + let entry = match app.history.current() { + Some(s) => s, None => return, }; let mut lines: Vec = Vec::new(); - for entry in &stack.entries { - let entry_string = format!("{}:{}", entry.filename, entry.line); + for stack in &entry.stacks { + let entry_string = format!("{}:{}", stack.source.filename, stack.source.line_no); lines.push(Line::from( entry_string [entry_string.len().saturating_sub(area.width as usize)..entry_string.len()] .to_string(), - )); + ).style(match stack.level == app.session_view.stack_depth() { + true => app.theme().source_line_highlight, + false => app.theme().source_line, + })); } + let y_scroll = match (app.session_view.stack_depth() + 1) > area.height { + true => (app.session_view.stack_depth() + 1) - area.height, + false => 0, + }; frame.render_widget( Paragraph::new(lines) .alignment(if app.session_view.full_screen { @@ -42,7 +49,7 @@ impl View for StackComponent { Alignment::Right }) .style(app.theme.scheme().stack_line) - .scroll(app.session_view.stack_scroll), + .scroll((y_scroll, app.session_view.stack_scroll.1)), area, ); }