Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/debug.log
/test.log
105 changes: 105 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
170 changes: 170 additions & 0 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
@@ -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<Value>,
}

// variable's start char is the key
type Row = HashMap<usize, VariableRef>;

#[derive(Clone, Debug)]
pub struct Analysis {
rows: HashMap<usize,Row>,
}

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<Analysis> {
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#"<?php
$var1 = 'hello'; $var2 = 'bar';
"#;
let analysis = Analyser::new().analyze(source)?;
let line = analysis.row(1);
assert_eq!(2, line.values().len());

assert_eq!(&VariableRef{
range: Range::new(Position::new(1,0), Position::new(1,5)),
name: "$var1".to_string(),
value: None,
}, line.get(&0).unwrap());

assert_eq!(&VariableRef{
range: Range::new(Position::new(1,17), Position::new(1,22)),
name: "$var2".to_string(),
value: None,
}, line.get(&17).unwrap());
Ok(())
}

#[test]
fn test_analyse_list() -> Result<(), anyhow::Error> {
let source = r#"<?php
list($var1, $var2) = some_call();
"#;
let analysis = Analyser::new().analyze(source)?;
let line = analysis.row(1);
assert_eq!(2, line.values().len());

assert_eq!(&VariableRef{
range: Range::new(Position::new(1,5), Position::new(1,10)),
name: "$var1".to_string(),
value: None,
}, line.get(&5).unwrap());
Ok(())
}
}

Loading