bugstalker 0.4.5

BugStalker is a modern and lightweight debugger for rust applications.
Documentation
use crate::debugger::register::debug::BreakCondition;
use crate::ui::proto::ClientExchanger;
use crate::ui::short::Abbreviator;
use crate::ui::syntax;
use crate::ui::syntax::StylizedLine;
use crate::ui::tui::app::port::UserEvent;
use crate::ui::tui::config::CommonAction;
use crate::ui::tui::utils::mstextarea::MultiSpanTextarea;
use crate::ui::tui::utils::syntect::into_text_span;
use crate::ui::tui::{Id, Msg};
use crate::{ui, weak_error};
use log::warn;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use syntect::util::LinesWithEndings;
use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::props::{Borders, Style, TextSpan};
use tuirealm::ratatui::layout::Alignment;
use tuirealm::ratatui::prelude::Color;
use tuirealm::ratatui::widgets::BorderType;
use tuirealm::{
    AttrValue, Attribute, Component, Event, MockComponent, Sub, SubClause, SubEventClause,
};

#[derive(Default)]
struct FileLinesCache {
    files: HashMap<PathBuf, Vec<Vec<TextSpan>>>,
    empty_file: Vec<Vec<TextSpan>>,
}

impl FileLinesCache {
    fn lines(&mut self, file: &Path) -> anyhow::Result<&Vec<Vec<TextSpan>>> {
        let lines = match self.files.entry(file.to_path_buf()) {
            Entry::Occupied(o) => o.into_mut(),
            Entry::Vacant(v) => {
                let mut file = match fs::File::open(file) {
                    Ok(f) => f,
                    Err(e) => {
                        warn!("error while open {file:?}: {e}");
                        return Ok(&self.empty_file);
                    }
                };

                let mut source_code = String::new();
                file.read_to_string(&mut source_code)?;

                let syntax_renderer = syntax::rust_syntax_renderer();
                let mut line_renderer = syntax_renderer.line_renderer();

                let mut lines = vec![];
                for (i, line) in LinesWithEndings::from(&source_code).enumerate() {
                    let mut line_spans = vec![TextSpan::new(format!("{:>4} ", i + 1))];

                    match line_renderer.render_line(line)? {
                        StylizedLine::NoneStyle(l) => {
                            line_spans.push(TextSpan::new(l));
                        }
                        StylizedLine::Stylized(segment) => segment.into_iter().for_each(|s| {
                            if let Ok(span) = into_text_span(s) {
                                line_spans.push(span)
                            }
                        }),
                    }

                    lines.push(line_spans);
                }

                v.insert(lines)
            }
        };
        Ok(lines)
    }
}

#[derive(MockComponent)]
pub struct Source {
    component: MultiSpanTextarea,
    file_cache: FileLinesCache,
}

impl Source {
    fn get_title(mb_file: Option<&Path>) -> String {
        if let Some(file) = mb_file {
            let abbreviator = Abbreviator::new("/", "/..", 70);

            format!(
                "Program source code ({:?})",
                abbreviator.apply(file.to_string_lossy().as_ref())
            )
        } else {
            "Program source code".into()
        }
    }

    pub fn new(exchanger: Arc<ClientExchanger>) -> anyhow::Result<Self> {
        let mb_threads = exchanger
            .request_sync(|dbg| dbg.thread_state())
            .expect("messaging enabled")
            .ok();
        let mb_place_in_focus = mb_threads.and_then(|threads| {
            threads
                .into_iter()
                .find_map(|snap| if snap.in_focus { snap.place } else { None })
        });

        let cache = FileLinesCache::default();
        let component = MultiSpanTextarea::default()
            .borders(
                Borders::default()
                    .modifiers(BorderType::Rounded)
                    .color(Color::LightYellow),
            )
            .title("Program source code", Alignment::Center)
            .step(4)
            .inactive(Style::default().fg(Color::Gray))
            .highlighted_str("â–¶");

        let mut this = Self {
            file_cache: cache,
            component,
        };

        if let Some(place) = mb_place_in_focus {
            weak_error!(this.update_source_view(place.file.as_path(), Some(place.line_number)));
        }

        Ok(this)
    }

    fn update_source_view(&mut self, file: &Path, mb_line_num: Option<u64>) -> anyhow::Result<()> {
        self.component.attr(
            Attribute::Title,
            AttrValue::Title((Self::get_title(Some(file)), Alignment::Center)),
        );

        let lines = self
            .file_cache
            .lines(file)?
            .iter()
            .cloned()
            .enumerate()
            .map(|(i, mut line)| {
                if Some((i + 1) as u64) == mb_line_num {
                    line.iter_mut().for_each(|text| text.fg = Color::LightRed)
                }
                line
            })
            .collect();
        self.component.text_rows(lines);

        if let Some(line) = mb_line_num {
            self.component.states.list_index = (line as usize).saturating_sub(1);
        }
        Ok(())
    }

    pub fn subscriptions() -> Vec<Sub<Id, UserEvent>> {
        vec![
            Sub::new(
                SubEventClause::User(UserEvent::Breakpoint {
                    pc: Default::default(),
                    num: 0,
                    file: None,
                    line: None,
                    function: None,
                }),
                SubClause::Always,
            ),
            Sub::new(
                SubEventClause::User(UserEvent::Watchpoint {
                    pc: Default::default(),
                    num: 0,
                    file: None,
                    line: None,
                    cond: BreakCondition::DataReadsWrites,
                    old_value: None,
                    new_value: None,
                    end_of_scope: false,
                }),
                SubClause::Always,
            ),
            Sub::new(
                SubEventClause::User(UserEvent::Step {
                    pc: Default::default(),
                    file: None,
                    line: None,
                    function: None,
                }),
                SubClause::Always,
            ),
            Sub::new(SubEventClause::User(UserEvent::Exit(0)), SubClause::Always),
        ]
    }
}

impl Component<Msg, UserEvent> for Source {
    fn on(&mut self, ev: Event<UserEvent>) -> Option<Msg> {
        match ev {
            Event::Keyboard(key_event) => {
                let keymap = &ui::config::current().tui_keymap;
                if let Some(action) = keymap.get_common(&key_event) {
                    match action {
                        CommonAction::Up => {
                            self.perform(Cmd::Move(Direction::Up));
                        }
                        CommonAction::Down => {
                            self.perform(Cmd::Move(Direction::Down));
                        }
                        CommonAction::ScrollUp => {
                            self.perform(Cmd::Scroll(Direction::Up));
                        }
                        CommonAction::ScrollDown => {
                            self.perform(Cmd::Scroll(Direction::Down));
                        }
                        CommonAction::GotoBegin => {
                            self.perform(Cmd::GoTo(Position::Begin));
                        }
                        CommonAction::GotoEnd => {
                            self.perform(Cmd::GoTo(Position::End));
                        }
                        _ => {}
                    }
                }
            }
            Event::User(UserEvent::Breakpoint { file, line, .. })
            | Event::User(UserEvent::Step { file, line, .. })
            | Event::User(UserEvent::Watchpoint { file, line, .. }) => {
                if let Some(file) = file {
                    weak_error!(self.update_source_view(PathBuf::from(file).as_path(), line));
                }
            }
            Event::User(UserEvent::Exit { .. }) => {
                self.component.text_rows(vec![]);
            }
            _ => {}
        };
        Some(Msg::None)
    }
}