Implement and archive Phase 5 debugger
This commit is contained in:
@@ -192,6 +192,8 @@ pub enum AfterSave {
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DialogKind {
|
||||
Watch(Command),
|
||||
DeleteWatch,
|
||||
DesignProperty,
|
||||
DesignPalette,
|
||||
DesignMenu((String, Option<i32>)),
|
||||
@@ -259,6 +261,7 @@ impl Dialog {
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Hit {
|
||||
DebugFrame(usize),
|
||||
DesignTool(usize),
|
||||
DesignPaint(&'static str, u8),
|
||||
DesignObject(u64),
|
||||
@@ -275,6 +278,7 @@ pub enum Hit {
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub debugger: crate::debugger::Debugger,
|
||||
pub designer: crate::designer::Designer,
|
||||
pub session: crate::execution::Session,
|
||||
pub editor: crate::editor::Editor,
|
||||
@@ -314,6 +318,7 @@ impl App {
|
||||
let (options, errors, config_disk) = Options::load(&config_path);
|
||||
project.include_paths = options.include_paths.clone();
|
||||
let mut app = Self {
|
||||
debugger: Default::default(),
|
||||
designer: Default::default(),
|
||||
session: Default::default(),
|
||||
editor: Default::default(),
|
||||
@@ -570,6 +575,9 @@ impl App {
|
||||
}
|
||||
}
|
||||
fn action(&mut self, command: Command) -> Result<()> {
|
||||
if self.debug_command(command)? {
|
||||
return Ok(());
|
||||
}
|
||||
if self.designer_command(command)? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1056,6 +1064,7 @@ impl App {
|
||||
self.project.open_project(&path, Decision::Discard)?;
|
||||
}
|
||||
}
|
||||
self.debugger = Default::default();
|
||||
self.session = Default::default();
|
||||
self.designer = Default::default();
|
||||
self.basic_events.clear();
|
||||
@@ -1133,6 +1142,10 @@ impl App {
|
||||
}
|
||||
fn submit(&mut self, d: &mut Dialog) -> Result<bool> {
|
||||
match d.kind.clone() {
|
||||
DialogKind::Watch(_) | DialogKind::DeleteWatch => {
|
||||
self.debug_submit(d)?;
|
||||
return Ok(false);
|
||||
}
|
||||
DialogKind::DesignProperty
|
||||
| DialogKind::DesignPalette
|
||||
| DialogKind::DesignMenu(_)
|
||||
@@ -1491,6 +1504,15 @@ impl App {
|
||||
} else if self.program_focus() {
|
||||
self.basic_events.push(event);
|
||||
} else if let Event::Paste(text) = event {
|
||||
if self.dialog.is_none()
|
||||
&& self.menu.is_none()
|
||||
&& self
|
||||
.active_window()
|
||||
.is_some_and(|w| w.kind == WindowKind::Immediate)
|
||||
{
|
||||
self.debugger.immediate.push_str(&text);
|
||||
return;
|
||||
}
|
||||
if self.dialog.is_none() && self.menu.is_none() && self.mode == Mode::Environment {
|
||||
if let Err(e) = self.editor_insert(&text) {
|
||||
self.message = e.to_string();
|
||||
@@ -1535,6 +1557,11 @@ impl App {
|
||||
return;
|
||||
}
|
||||
match hit {
|
||||
Hit::DebugFrame(index) => {
|
||||
if let Err(e) = self.select_debug_frame(index) {
|
||||
self.message = e.to_string();
|
||||
}
|
||||
}
|
||||
Hit::DesignTool(_) => {}
|
||||
Hit::DesignObject(id) => {
|
||||
if let Err(e) = self.design_select(id, false) {
|
||||
@@ -1924,6 +1951,9 @@ impl App {
|
||||
))
|
||||
}
|
||||
fn code_key(&mut self, key: KeyEvent) -> Result<()> {
|
||||
if self.debug_key(key)? {
|
||||
return Ok(());
|
||||
}
|
||||
if self.mode == Mode::Designer || self.editor_view().is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -101,9 +101,6 @@ impl Command {
|
||||
pub fn feature_phase(self) -> Option<u8> {
|
||||
use Command::*;
|
||||
match self {
|
||||
NextStatement | AddWatch | InstantWatch | Watchpoint | DeleteWatch | DeleteWatches
|
||||
| Trace | History | Breakpoint | ClearBreakpoints | BreakErrors | SetStatement
|
||||
| RunToCursor | Step | ProcedureStep | HistoryBack | HistoryForward => Some(6),
|
||||
HelpIndex | HelpContents | Keyboard | Topic | UsingHelp | Tutorial => Some(7),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
687
crates/tb-ide/src/debugger.rs
Normal file
687
crates/tb-ide/src/debugger.rs
Normal file
@@ -0,0 +1,687 @@
|
||||
//! IDE debugger state and commands on the session's existing VM.
|
||||
use crate::{
|
||||
app::{App, Dialog, DialogKind, Execution, Field, Hit, WindowKind},
|
||||
commands::Command,
|
||||
documents::{BreakpointMark, Document, DocumentId},
|
||||
editor::line_start,
|
||||
};
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use crossterm::event::{KeyCode as K, KeyEvent, KeyModifiers as M};
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use tb_frontend::{Diagnostic, SourcePos};
|
||||
use tb_vm::interp::{DebugLocation, DebugWatch, Vm};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Debugger {
|
||||
pub source_modules: std::collections::BTreeMap<DocumentId, DocumentId>,
|
||||
pub watches: Vec<DebugWatch>,
|
||||
pub frame: Option<u64>,
|
||||
pub trace: bool,
|
||||
pub history: bool,
|
||||
pub break_errors: bool,
|
||||
pub historical: Option<usize>,
|
||||
pub immediate: String,
|
||||
pub motion: Option<Command>,
|
||||
pub instant: Option<DebugWatch>,
|
||||
pub last_location: Option<DebugLocation>,
|
||||
}
|
||||
fn module_name(doc: &Document) -> String {
|
||||
match doc.content() {
|
||||
tb_vm::project_io::Content::Form(form) => form.root.name.clone(),
|
||||
_ => doc
|
||||
.source_path()
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
pub fn physical_line(doc: &Document, at: usize) -> u32 {
|
||||
let first = if let tb_vm::project_io::Content::Form(form) = doc.content() {
|
||||
tb_ui::frm::read_text(
|
||||
&doc.source_path().display().to_string(),
|
||||
&tb_ui::frm::write_text(form),
|
||||
)
|
||||
.map_or(1, |f| f.code_line())
|
||||
} else {
|
||||
1
|
||||
};
|
||||
first
|
||||
+ doc.code()[..at.min(doc.code().len())]
|
||||
.bytes()
|
||||
.filter(|c| *c == b'\n')
|
||||
.count() as u32
|
||||
}
|
||||
impl App {
|
||||
pub(crate) fn debug_setup(&mut self, vm: &mut Vm) {
|
||||
vm.debug.enabled = true;
|
||||
vm.debug.compiler = Some(self.editor.compiler.debug_compiler());
|
||||
vm.debug.trace = self.debugger.trace;
|
||||
vm.debug.history_on = self.debugger.history;
|
||||
vm.debug.break_errors = self.debugger.break_errors;
|
||||
for w in &mut self.debugger.watches {
|
||||
w.value = Err("Noch kein aktueller Halt".into());
|
||||
w.frame = None;
|
||||
}
|
||||
vm.debug.watches = self.debugger.watches.clone();
|
||||
self.debugger.frame = None;
|
||||
self.debugger.historical = None;
|
||||
self.debugger.last_location = None;
|
||||
self.debugger.instant = None;
|
||||
self.bind_debug_breakpoints(vm);
|
||||
}
|
||||
fn bind_debug_breakpoints(&mut self, vm: &mut Vm) {
|
||||
vm.clear_source_breakpoints();
|
||||
let ids = self
|
||||
.project
|
||||
.documents()
|
||||
.map(|(id, _)| id)
|
||||
.collect::<Vec<_>>();
|
||||
for id in ids {
|
||||
let doc = self.project.document(id).unwrap();
|
||||
let marks = doc
|
||||
.breakpoints
|
||||
.iter()
|
||||
.map(|m| {
|
||||
(
|
||||
m.valid,
|
||||
self.project
|
||||
.document(m.module)
|
||||
.map(module_name)
|
||||
.unwrap_or_default(),
|
||||
physical_line(doc, m.at),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let path = doc.source_path().display().to_string();
|
||||
let bound = marks
|
||||
.into_iter()
|
||||
.map(|(valid, module, line)| {
|
||||
let source = vm
|
||||
.source_files()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, s)| {
|
||||
s.path == path && vm.module_name(s.module).eq_ignore_ascii_case(&module)
|
||||
})
|
||||
.map(|(id, s)| (s.module, id as u32));
|
||||
valid
|
||||
&& source.is_some_and(|(module, source)| {
|
||||
vm.add_source_breakpoint(module, source, line)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (mark, bound) in self
|
||||
.project
|
||||
.breakpoint_marks_mut(id)
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
.zip(bound)
|
||||
{
|
||||
mark.bound = bound;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn debug_cursor(&self) -> Result<(String, u32)> {
|
||||
let view = self.project.view(self.editor_view()?)?;
|
||||
ensure!(
|
||||
!self.editor.expansions.contains_key(&self.editor_view()?),
|
||||
"Included File für einen physischen Quellort öffnen"
|
||||
);
|
||||
let doc = self.project.document(view.document())?;
|
||||
Ok((
|
||||
doc.source_path().display().to_string(),
|
||||
physical_line(doc, view.cursor),
|
||||
))
|
||||
}
|
||||
fn debug_target(&self) -> Result<(u16, u32, u32)> {
|
||||
ensure!(self.current_compilation().is_some() && self.editor.revision == self.session.source_revision, "Cursorziele benötigen den unveränderten Quellstand der pausierten Sitzung; zuerst neu starten");
|
||||
let (path, line) = self.debug_cursor()?;
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Keine pausierte Sitzung"))?;
|
||||
let module = vm.current_module();
|
||||
let candidates = vm
|
||||
.source_files()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.path == path)
|
||||
.collect::<Vec<_>>();
|
||||
let (source, file) = candidates
|
||||
.iter()
|
||||
.find(|(_, s)| s.module == module)
|
||||
.copied()
|
||||
.or_else(|| (candidates.len() == 1).then(|| candidates[0]))
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Quelldatei gehört nicht eindeutig zur pausierten Kompilatrevision")
|
||||
})?;
|
||||
Ok((file.module, source as u32, line))
|
||||
}
|
||||
pub fn toggle_debug_breakpoint(&mut self) -> Result<()> {
|
||||
let view = self.project.view(self.editor_view()?)?;
|
||||
let id = view.document();
|
||||
let at = line_start(self.project.document(id)?.code(), view.cursor);
|
||||
let doc = self.project.document(id)?;
|
||||
let line = physical_line(doc, at);
|
||||
let module = if self.project.members().contains(&id) {
|
||||
Some(id)
|
||||
} else {
|
||||
self.debugger
|
||||
.source_modules
|
||||
.get(&id)
|
||||
.copied()
|
||||
.or_else(|| self.project.members().get(self.selected_member).copied())
|
||||
}
|
||||
.ok_or_else(|| anyhow!("Kein Ursprungsmodul ausgewählt"))?;
|
||||
let existing = doc
|
||||
.breakpoints
|
||||
.iter()
|
||||
.position(|m| physical_line(doc, m.at) == line && m.module == module);
|
||||
if let Some(index) = existing {
|
||||
self.project.breakpoint_marks_mut(id)?.remove(index);
|
||||
} else {
|
||||
self.project.breakpoint_marks_mut(id)?.push(BreakpointMark {
|
||||
at,
|
||||
module,
|
||||
valid: true,
|
||||
bound: false,
|
||||
});
|
||||
}
|
||||
if self.current_compilation().is_some()
|
||||
&& self.editor.revision == self.session.source_revision
|
||||
{
|
||||
if let Some(mut vm) = self.session.vm.take() {
|
||||
self.bind_debug_breakpoints(&mut vm);
|
||||
self.session.vm = Some(vm);
|
||||
}
|
||||
}
|
||||
self.message =
|
||||
"Breakpoint aktualisiert · ● gebunden, ○ inaktiv bis gültiger Übersetzung".into();
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn debug_command(&mut self, c: Command) -> Result<bool> {
|
||||
use Command::*;
|
||||
match c {
|
||||
Breakpoint => self.toggle_debug_breakpoint()?,
|
||||
ClearBreakpoints => {
|
||||
let ids = self
|
||||
.project
|
||||
.documents()
|
||||
.map(|(id, _)| id)
|
||||
.collect::<Vec<_>>();
|
||||
for id in ids {
|
||||
self.project.breakpoint_marks_mut(id)?.clear();
|
||||
}
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.clear_source_breakpoints();
|
||||
}
|
||||
}
|
||||
Step | ProcedureStep | RunToCursor => {
|
||||
ensure!(
|
||||
self.execution != Execution::Paused || self.session.vm.is_some(),
|
||||
"Keine pausierte VM"
|
||||
);
|
||||
if !matches!(
|
||||
self.execution,
|
||||
Execution::Paused | Execution::Running | Execution::Waiting
|
||||
) {
|
||||
self.start_execution()?;
|
||||
}
|
||||
self.debugger.motion = Some(c);
|
||||
if self.execution == Execution::Paused {
|
||||
self.continue_execution()?;
|
||||
} else {
|
||||
self.debug_resume_motion()?;
|
||||
}
|
||||
}
|
||||
NextStatement => {
|
||||
self.debugger.historical = None;
|
||||
self.debug_navigate(None)?;
|
||||
}
|
||||
SetStatement => {
|
||||
ensure!(
|
||||
self.execution == Execution::Paused,
|
||||
"Set Next Statement benötigt Break-Modus"
|
||||
);
|
||||
let (_, source, line) = self.debug_target()?;
|
||||
let vm = self.session.vm.as_mut().unwrap();
|
||||
let frame = vm
|
||||
.debug_location()
|
||||
.ok_or_else(|| anyhow!("Kein Frame"))?
|
||||
.frame;
|
||||
ensure!(
|
||||
self.debugger.frame.is_none_or(|id| id == frame),
|
||||
"Inspektionsrahmen ist nicht der aktive Frame"
|
||||
);
|
||||
vm.set_next_statement(frame, source, line)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
self.debug_navigate(None)?;
|
||||
}
|
||||
AddWatch | InstantWatch | Watchpoint => self.open_dialog(
|
||||
if c == Watchpoint {
|
||||
"Watchpoint"
|
||||
} else {
|
||||
"Watch"
|
||||
},
|
||||
DialogKind::Watch(c),
|
||||
vec![Field::text("BASIC-Ausdruck", "")],
|
||||
),
|
||||
DeleteWatch => self.open_dialog(
|
||||
"Delete Watch",
|
||||
DialogKind::DeleteWatch,
|
||||
vec![Field::choice(
|
||||
"Watch",
|
||||
self.debugger
|
||||
.watches
|
||||
.iter()
|
||||
.map(|w| w.expression.clone())
|
||||
.collect(),
|
||||
0,
|
||||
)],
|
||||
),
|
||||
DeleteWatches => {
|
||||
self.debugger.watches.clear();
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
Trace | History | BreakErrors => {
|
||||
match c {
|
||||
Trace => self.debugger.trace = !self.debugger.trace,
|
||||
History => self.debugger.history = !self.debugger.history,
|
||||
_ => self.debugger.break_errors = !self.debugger.break_errors,
|
||||
}
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.debug.trace = self.debugger.trace;
|
||||
vm.debug.history_on = self.debugger.history;
|
||||
vm.debug.break_errors = self.debugger.break_errors;
|
||||
}
|
||||
self.message = format!(
|
||||
"Trace {} · History {} · Break on Errors {}",
|
||||
self.debugger.trace, self.debugger.history, self.debugger.break_errors
|
||||
);
|
||||
}
|
||||
HistoryBack | HistoryForward => {
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Keine History"))?;
|
||||
ensure!(!vm.debug.history.is_empty(), "History ist leer");
|
||||
let last = vm.debug.history.len();
|
||||
let at = self.debugger.historical.unwrap_or(last);
|
||||
let at = if c == HistoryBack {
|
||||
at.saturating_sub(1)
|
||||
} else {
|
||||
(at + 1).min(last)
|
||||
};
|
||||
let location = vm.debug.history.get(at).cloned();
|
||||
self.debugger.historical = (at < last).then_some(at);
|
||||
self.debug_navigate(location)?;
|
||||
}
|
||||
Calls | Debug => {
|
||||
self.refresh_debug_watches();
|
||||
self.show_tool(if c == Calls {
|
||||
WindowKind::Calls
|
||||
} else {
|
||||
WindowKind::Debug
|
||||
});
|
||||
}
|
||||
Immediate => self.show_tool(WindowKind::Immediate),
|
||||
_ => return Ok(false),
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
pub(crate) fn debug_resume_motion(&mut self) -> Result<()> {
|
||||
let motion = self.debugger.motion.take();
|
||||
let target = if motion == Some(Command::RunToCursor) {
|
||||
Some(self.debug_target()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.set_step(false);
|
||||
vm.debug.cancel_motion();
|
||||
vm.debug.error = None;
|
||||
match motion {
|
||||
Some(Command::Step) => vm.set_step(true),
|
||||
Some(Command::ProcedureStep) => vm.step_over(),
|
||||
Some(Command::RunToCursor) => {
|
||||
let (m, s, l) = target.unwrap();
|
||||
vm.run_to_cursor(m, s, l).map_err(|e| anyhow!(e))?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.debugger.historical = None;
|
||||
Ok(())
|
||||
}
|
||||
fn sync_watchpoints(&mut self) {
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.debug.watches = self.debugger.watches.clone();
|
||||
}
|
||||
}
|
||||
pub fn refresh_debug_watches(&mut self) {
|
||||
let Some(vm) = &mut self.session.vm else {
|
||||
return;
|
||||
};
|
||||
let Some(active) = vm.debug_location() else {
|
||||
return;
|
||||
};
|
||||
let frame = self
|
||||
.debugger
|
||||
.frame
|
||||
.filter(|id| vm.debug_frames().iter().any(|f| f.id == *id))
|
||||
.unwrap_or(active.frame);
|
||||
self.debugger.frame = Some(frame);
|
||||
for watch in &mut self.debugger.watches {
|
||||
watch.frame = Some(frame);
|
||||
watch.value = if watch.condition {
|
||||
vm.evaluate_condition(frame, &watch.expression)
|
||||
} else {
|
||||
vm.evaluate_watch(frame, &watch.expression)
|
||||
};
|
||||
}
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
pub fn select_debug_frame(&mut self, index: usize) -> Result<()> {
|
||||
ensure!(
|
||||
self.execution == Execution::Paused,
|
||||
"Calls-Inspektion benötigt Break-Modus"
|
||||
);
|
||||
let frame = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug_frames().get(index).cloned())
|
||||
.ok_or_else(|| anyhow!("Frame nicht erreichbar"))?;
|
||||
self.debugger.frame = Some(frame.id);
|
||||
self.refresh_debug_watches();
|
||||
self.debug_navigate(Some(frame.location))
|
||||
}
|
||||
pub(crate) fn debug_navigate(&mut self, location: Option<DebugLocation>) -> Result<()> {
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Keine laufende Sitzung"))?;
|
||||
let location = location
|
||||
.or_else(|| vm.execution_location())
|
||||
.ok_or_else(|| anyhow!("Kein Quellort"))?;
|
||||
let file = vm.source_files()[location.source as usize].path.clone();
|
||||
if file == "<Immediate>" {
|
||||
self.show_tool(WindowKind::Immediate);
|
||||
self.message = format!("Direktkommando pausiert · Zeile {}", location.line);
|
||||
return Ok(());
|
||||
}
|
||||
self.debugger.last_location = Some(location.clone());
|
||||
let owner_name = vm.module_name(location.module).to_string();
|
||||
let owner = self.project.members().iter().copied().find(|id| {
|
||||
self.project
|
||||
.document(*id)
|
||||
.is_ok_and(|doc| module_name(doc).eq_ignore_ascii_case(&owner_name))
|
||||
});
|
||||
self.session.fullscreen = false;
|
||||
self.goto_diagnostic(&Diagnostic {
|
||||
file: Some(file.clone()),
|
||||
pos: SourcePos {
|
||||
source: location.source,
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
},
|
||||
message: String::new(),
|
||||
})?;
|
||||
if let (Some(id), Some(owner)) = (self.active_document(), owner) {
|
||||
self.debugger.source_modules.insert(id, owner);
|
||||
}
|
||||
self.message = format!(
|
||||
"{} · {}:{} · Werte vom aktuellen Halt{}",
|
||||
if self.debugger.historical.is_some() {
|
||||
"HISTORIE"
|
||||
} else if self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug_location())
|
||||
.is_some_and(|at| at.frame != location.frame)
|
||||
{
|
||||
"Inspektionsrahmen"
|
||||
} else {
|
||||
"Ausführungsstelle"
|
||||
},
|
||||
file,
|
||||
location.line,
|
||||
if self.session.old_revision {
|
||||
" · ALTES KOMPILAT"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
if let Some(error) = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug.error.as_ref())
|
||||
{
|
||||
self.message.push_str(&format!(
|
||||
" · Behandelter Fehler {} · Handler {:?}",
|
||||
error.code, error.handler
|
||||
));
|
||||
}
|
||||
if let Some(error) = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug.immediate_error.as_ref())
|
||||
{
|
||||
self.message.push_str(&format!(" · Direktfehler: {error}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn submit_immediate(&mut self, text: &str) -> Result<()> {
|
||||
ensure!(
|
||||
self.execution == Execution::Paused,
|
||||
"Direktkommandos benötigen Break-Modus"
|
||||
);
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("Keine Sitzung"))?;
|
||||
let frame = self
|
||||
.debugger
|
||||
.frame
|
||||
.or_else(|| vm.debug_location().map(|l| l.frame))
|
||||
.ok_or_else(|| anyhow!("Kein Frame"))?;
|
||||
vm.start_immediate(frame, text).map_err(|e| anyhow!(e))?;
|
||||
self.execution = Execution::Running;
|
||||
self.message = "Direktkommando läuft · Unterbrechen über Run/Break".into();
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn debug_submit(&mut self, d: &Dialog) -> Result<()> {
|
||||
match d.kind {
|
||||
DialogKind::Watch(command) => {
|
||||
let text = d.fields[0].string();
|
||||
ensure!(!text.trim().is_empty(), "Ausdruck fehlt");
|
||||
let mut watch = DebugWatch {
|
||||
expression: text,
|
||||
condition: command == Command::Watchpoint,
|
||||
frame: None,
|
||||
value: Err("Noch kein Halt".into()),
|
||||
};
|
||||
if self.execution == Execution::Paused {
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
if let Some(frame) = self
|
||||
.debugger
|
||||
.frame
|
||||
.or_else(|| vm.debug_location().map(|p| p.frame))
|
||||
{
|
||||
watch.frame = Some(frame);
|
||||
watch.value = if watch.condition {
|
||||
vm.evaluate_condition(frame, &watch.expression)
|
||||
} else {
|
||||
vm.evaluate_watch(frame, &watch.expression)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if command == Command::InstantWatch {
|
||||
self.debugger.instant = Some(watch);
|
||||
} else {
|
||||
self.debugger.watches.push(watch);
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
self.show_tool(WindowKind::Debug);
|
||||
}
|
||||
DialogKind::DeleteWatch => {
|
||||
let i = d.fields[0].index();
|
||||
if i < self.debugger.watches.len() {
|
||||
self.debugger.watches.remove(i);
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn debug_key(&mut self, key: KeyEvent) -> Result<bool> {
|
||||
match self.active_window().map(|w| w.kind) {
|
||||
Some(WindowKind::Immediate) => {
|
||||
match key.code {
|
||||
K::Enter => {
|
||||
let text = self.debugger.immediate.clone();
|
||||
self.submit_immediate(&text)?;
|
||||
self.debugger.immediate.clear();
|
||||
}
|
||||
K::Backspace => {
|
||||
self.debugger.immediate.pop();
|
||||
}
|
||||
K::Char(c) if !key.modifiers.intersects(M::CONTROL | M::ALT) => {
|
||||
self.debugger.immediate.push(c)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
Some(WindowKind::Calls) => {
|
||||
let frames = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.map(|vm| vm.debug_frames())
|
||||
.unwrap_or_default();
|
||||
let at = frames
|
||||
.iter()
|
||||
.position(|f| Some(f.id) == self.debugger.frame)
|
||||
.unwrap_or(0);
|
||||
let next = match key.code {
|
||||
K::Down => (at + 1).min(frames.len().saturating_sub(1)),
|
||||
K::Up => at.saturating_sub(1),
|
||||
K::Enter => at,
|
||||
_ => return Ok(true),
|
||||
};
|
||||
self.select_debug_frame(next)?;
|
||||
self.show_tool(WindowKind::Calls);
|
||||
Ok(true)
|
||||
}
|
||||
Some(WindowKind::Debug) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
pub(crate) fn debug_render(&mut self, f: &mut Frame, kind: WindowKind, r: Rect) {
|
||||
let mut lines = Vec::new();
|
||||
match kind {
|
||||
WindowKind::Immediate => {
|
||||
lines.push(format!("> {}", self.debugger.immediate));
|
||||
lines.push("Enter: ausführen · Run/Break: unterbrechen · F5: fortsetzen".into());
|
||||
lines.push(self.message.clone());
|
||||
if let Some(vm) = &self.session.vm {
|
||||
if let Some(error) = &vm.debug.immediate_error {
|
||||
lines.push(error.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowKind::Calls => {
|
||||
if let Some(vm) = &self.session.vm {
|
||||
for (row, frame) in vm.debug_frames().iter().enumerate() {
|
||||
lines.push(format!(
|
||||
"{} {} · {}:{}",
|
||||
if Some(frame.id) == self.debugger.frame {
|
||||
"▶"
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
frame.procedure,
|
||||
frame.file,
|
||||
frame.location.line
|
||||
));
|
||||
if row < r.height as usize {
|
||||
self.hits.push((
|
||||
Rect::new(r.x, r.y + row as u16, r.width, 1),
|
||||
Hit::DebugFrame(row),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
lines.push(if self.debugger.historical.is_some() {
|
||||
"HISTORIE · Werte vom aktuellen Halt".into()
|
||||
} else {
|
||||
"Watches · Werte vom aktuellen Halt (read-only)".into()
|
||||
});
|
||||
for watch in self
|
||||
.debugger
|
||||
.watches
|
||||
.iter()
|
||||
.chain(self.debugger.instant.iter())
|
||||
{
|
||||
lines.push(format!(
|
||||
"{}{} = {}",
|
||||
if watch.condition { "[Watchpoint] " } else { "" },
|
||||
watch.expression,
|
||||
match &watch.value {
|
||||
Ok(v) => format!("{v:?}"),
|
||||
Err(e) => format!("FEHLER: {e}"),
|
||||
}
|
||||
));
|
||||
}
|
||||
if let Some(vm) = &self.session.vm {
|
||||
if let Some(error) = &vm.debug.error {
|
||||
lines.push(format!(
|
||||
"Behandelter Fehler {} · Ursache {:?} · Handler {:?}",
|
||||
error.code, error.origin, error.handler
|
||||
));
|
||||
}
|
||||
}
|
||||
for (_, doc) in self.project.documents() {
|
||||
for mark in &doc.breakpoints {
|
||||
lines.push(format!(
|
||||
"{} {}:{}",
|
||||
if mark.bound { "●" } else { "○ inaktiv" },
|
||||
doc.source_path().display(),
|
||||
physical_line(doc, mark.at)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f.render_widget(
|
||||
Paragraph::new(lines.join("\n"))
|
||||
.style(Style::default().fg(Color::White).bg(Color::Blue)),
|
||||
r,
|
||||
);
|
||||
if kind == WindowKind::Immediate && r.height > 4 {
|
||||
f.render_widget(
|
||||
tb_ui::screen::ScreenWidget(self.session.screen()),
|
||||
Rect::new(r.x, r.y + 4, r.width, r.height - 4),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,13 +50,23 @@ impl View {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Edit {
|
||||
breakpoints: Vec<BreakpointMark>,
|
||||
content: Content,
|
||||
design_ids: BTreeMap<(String, Option<i32>), u64>,
|
||||
group: Option<(u64, Vec<DocumentId>)>,
|
||||
views: Vec<(ViewId, View)>,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BreakpointMark {
|
||||
pub at: usize,
|
||||
pub module: DocumentId,
|
||||
pub valid: bool,
|
||||
pub bound: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Document {
|
||||
pub breakpoints: Vec<BreakpointMark>,
|
||||
source_path: PathBuf,
|
||||
path: Option<PathBuf>,
|
||||
content: Content,
|
||||
@@ -68,6 +78,29 @@ pub struct Document {
|
||||
undo: Vec<Edit>,
|
||||
design_ids: BTreeMap<(String, Option<i32>), u64>,
|
||||
}
|
||||
fn remap_breakpoints(
|
||||
marks: &[BreakpointMark],
|
||||
edits: &[(Range<usize>, String)],
|
||||
) -> Vec<BreakpointMark> {
|
||||
marks
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut mark| {
|
||||
for (range, text) in edits.iter().rev() {
|
||||
if range.contains(&mark.at) {
|
||||
mark.valid = false;
|
||||
}
|
||||
if mark.at >= range.end {
|
||||
mark.at = mark.at - range.len() + text.len();
|
||||
} else if mark.at > range.start {
|
||||
mark.at = range.start;
|
||||
}
|
||||
}
|
||||
mark.bound = false;
|
||||
mark
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
impl Document {
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
@@ -251,6 +284,7 @@ impl Project {
|
||||
self.documents.insert(
|
||||
id,
|
||||
Document {
|
||||
breakpoints: Vec::new(),
|
||||
source_path: path.clone(),
|
||||
path: Some(path.clone()),
|
||||
saved: Some(read.content.clone()),
|
||||
@@ -303,6 +337,7 @@ impl Project {
|
||||
self.documents.insert(
|
||||
id,
|
||||
Document {
|
||||
breakpoints: Vec::new(),
|
||||
source_path: path.clone(),
|
||||
path: None,
|
||||
content,
|
||||
@@ -542,7 +577,15 @@ impl Project {
|
||||
at
|
||||
}
|
||||
};
|
||||
let marks = remap_breakpoints(
|
||||
&old.breakpoints,
|
||||
&[(
|
||||
removed.clone(),
|
||||
after[removed.start..removed.start + inserted].to_owned(),
|
||||
)],
|
||||
);
|
||||
let undo = Edit {
|
||||
breakpoints: old.breakpoints.clone(),
|
||||
content: old.content.clone(),
|
||||
design_ids: old.design_ids.clone(),
|
||||
group: None,
|
||||
@@ -554,6 +597,7 @@ impl Project {
|
||||
.collect(),
|
||||
};
|
||||
let doc = self.documents.get_mut(&id).unwrap();
|
||||
doc.breakpoints = marks;
|
||||
doc.undo.push(undo);
|
||||
doc.content = content;
|
||||
doc.revision += 1;
|
||||
@@ -567,6 +611,13 @@ impl Project {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn breakpoint_marks_mut(&mut self, id: DocumentId) -> Result<&mut Vec<BreakpointMark>> {
|
||||
Ok(&mut self
|
||||
.documents
|
||||
.get_mut(&id)
|
||||
.ok_or_else(|| anyhow!("Unbekanntes Dokument"))?
|
||||
.breakpoints)
|
||||
}
|
||||
pub fn replace_text(&mut self, id: DocumentId, range: Range<usize>, text: &str) -> Result<()> {
|
||||
self.replace_ranges(id, &[(range, text.to_owned())])
|
||||
}
|
||||
@@ -576,6 +627,7 @@ impl Project {
|
||||
id: DocumentId,
|
||||
edits: &[(Range<usize>, String)],
|
||||
) -> Result<()> {
|
||||
let marks = remap_breakpoints(&self.document(id)?.breakpoints, edits);
|
||||
let mut content = self.document(id)?.content.clone();
|
||||
let mut end = 0;
|
||||
for (range, _) in edits {
|
||||
@@ -597,6 +649,7 @@ impl Project {
|
||||
content.code_mut().replace_range(range.clone(), text);
|
||||
}
|
||||
self.commit_edit(id, content)?;
|
||||
self.documents.get_mut(&id).unwrap().breakpoints = marks;
|
||||
for (view_id, old) in views {
|
||||
let map = |mut at: usize| {
|
||||
for (range, text) in edits.iter().rev() {
|
||||
@@ -659,6 +712,7 @@ impl Project {
|
||||
let Some(edit) = doc.undo.pop() else {
|
||||
return Ok(false);
|
||||
};
|
||||
doc.breakpoints = edit.breakpoints;
|
||||
doc.content = edit.content;
|
||||
doc.design_ids = edit.design_ids;
|
||||
doc.revision += 1;
|
||||
|
||||
@@ -501,7 +501,15 @@ impl App {
|
||||
.resolve(doc.source_path().parent().unwrap(), &path)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
self.editor.history.push((id, view.cursor));
|
||||
let owner = if self.project.members().contains(&id) {
|
||||
Some(id)
|
||||
} else {
|
||||
self.debugger.source_modules.get(&id).copied()
|
||||
};
|
||||
let id = self.project.open_document(&path)?;
|
||||
if let Some(owner) = owner {
|
||||
self.debugger.source_modules.insert(id, owner);
|
||||
}
|
||||
self.show_document(id, false)?;
|
||||
}
|
||||
IncludedLines => {
|
||||
@@ -809,6 +817,7 @@ impl App {
|
||||
}
|
||||
/// Immer aktuelle Quellen einschließlich externer Includes prüfen; alte Erfolge werden nie herausgegeben.
|
||||
pub fn compile_current(&mut self) -> Result<&CompiledModule> {
|
||||
self.editor.compiler.debug_symbols = true;
|
||||
self.editor.compiled = None;
|
||||
self.editor.revision = None;
|
||||
self.editor.diagnostics.clear();
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct Session {
|
||||
pub forms_phase: bool,
|
||||
pub file_shell: bool,
|
||||
revision: Vec<u8>,
|
||||
source_revision: Option<crate::editor::Revision>,
|
||||
pub(crate) source_revision: Option<crate::editor::Revision>,
|
||||
output: TextScreen,
|
||||
}
|
||||
impl Default for Session {
|
||||
@@ -109,7 +109,7 @@ impl App {
|
||||
.map_err(|e| anyhow!(e))?
|
||||
};
|
||||
let revision = module.to_tbc();
|
||||
let vm = new_execution(
|
||||
let mut vm = new_execution(
|
||||
module,
|
||||
&self.session.command,
|
||||
Some((self.size.0 as usize, self.size.1 as usize)),
|
||||
@@ -126,6 +126,7 @@ impl App {
|
||||
} else {
|
||||
target
|
||||
};
|
||||
self.debug_setup(&mut vm);
|
||||
self.session.finish();
|
||||
self.basic_events.clear();
|
||||
self.session.vm = Some(vm);
|
||||
@@ -165,6 +166,11 @@ impl App {
|
||||
)
|
||||
},
|
||||
);
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.debug.cancel_motion();
|
||||
}
|
||||
self.debugger.frame = None;
|
||||
self.refresh_debug_watches();
|
||||
}
|
||||
}
|
||||
pub fn continue_execution(&mut self) -> Result<()> {
|
||||
@@ -208,6 +214,10 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
pub fn resume_execution(&mut self, old: bool) {
|
||||
if let Err(e) = self.debug_resume_motion() {
|
||||
self.message = e.to_string();
|
||||
return;
|
||||
}
|
||||
if let Some(vm) = self.session.vm.as_mut() {
|
||||
vm.rt.abbruch = false;
|
||||
self.session.old_revision = old;
|
||||
@@ -272,6 +282,11 @@ impl App {
|
||||
} else {
|
||||
vm.poll(&mut self.session.host, 4096)
|
||||
};
|
||||
for (watch, live) in self.debugger.watches.iter_mut().zip(&vm.debug.watches) {
|
||||
if watch.condition {
|
||||
watch.value = live.value.clone();
|
||||
}
|
||||
}
|
||||
match result {
|
||||
PollResult::Yield => self.execution = Execution::Running,
|
||||
PollResult::Waiting { .. } => self.execution = Execution::Waiting,
|
||||
@@ -289,10 +304,11 @@ impl App {
|
||||
self.execution = Execution::Ended;
|
||||
self.message = "Ended".into();
|
||||
}
|
||||
RunEvent::Stopped { .. }
|
||||
| RunEvent::Interrupted { .. }
|
||||
| RunEvent::Breakpoint { .. }
|
||||
| RunEvent::Stepped { .. } => self.pause_execution(),
|
||||
RunEvent::Stopped { .. } | RunEvent::Interrupted { .. } => self.pause_execution(),
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => {
|
||||
self.pause_execution();
|
||||
let _ = self.debug_navigate(None);
|
||||
}
|
||||
RunEvent::Error {
|
||||
code,
|
||||
line,
|
||||
@@ -323,5 +339,12 @@ impl App {
|
||||
}
|
||||
},
|
||||
}
|
||||
if self.debugger.trace && matches!(self.execution, Execution::Running | Execution::Waiting)
|
||||
{
|
||||
let location = self.session.vm.as_ref().and_then(|vm| vm.debug_location());
|
||||
if location != self.debugger.last_location {
|
||||
let _ = self.debug_navigate(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,5 @@ pub mod editor;
|
||||
pub mod execution;
|
||||
|
||||
pub mod designer;
|
||||
|
||||
pub mod debugger;
|
||||
|
||||
@@ -222,6 +222,81 @@ impl App {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
f.render_widget(Paragraph::new(text).style(content_style), inner);
|
||||
if expansion.is_none() {
|
||||
for mark in &doc.breakpoints {
|
||||
let row = doc.code()[..mark.at.min(doc.code().len())]
|
||||
.bytes()
|
||||
.filter(|c| *c == b'\n')
|
||||
.count();
|
||||
if row >= v.scroll_line
|
||||
&& row < v.scroll_line + inner.height as usize
|
||||
{
|
||||
put(
|
||||
f,
|
||||
Rect::new(
|
||||
rect.x,
|
||||
inner.y + (row - v.scroll_line) as u16,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
if mark.bound { "●" } else { "○" },
|
||||
Style::default().fg(Color::Red),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expansion.is_none() {
|
||||
if let Some(vm) = &self.session.vm {
|
||||
let actual = vm.execution_location();
|
||||
let selected = self.debugger.last_location.as_ref();
|
||||
for (location, mark) in
|
||||
actual.as_ref().map(|p| (p, "▶")).into_iter().chain(
|
||||
selected.map(|p| {
|
||||
(
|
||||
p,
|
||||
if self.debugger.historical.is_some() {
|
||||
"H"
|
||||
} else if actual
|
||||
.as_ref()
|
||||
.is_some_and(|a| a.frame != p.frame)
|
||||
{
|
||||
"I"
|
||||
} else {
|
||||
"▶"
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
{
|
||||
if vm.source_files().get(location.source as usize).is_some_and(
|
||||
|source| {
|
||||
std::path::Path::new(&source.path) == doc.source_path()
|
||||
},
|
||||
) {
|
||||
let row = location
|
||||
.line
|
||||
.saturating_sub(crate::debugger::physical_line(doc, 0))
|
||||
as usize;
|
||||
if row >= v.scroll_line
|
||||
&& row < v.scroll_line + inner.height as usize
|
||||
{
|
||||
put(
|
||||
f,
|
||||
Rect::new(
|
||||
rect.x,
|
||||
inner.y + (row - v.scroll_line) as u16,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
mark,
|
||||
Style::default().fg(Color::Yellow),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if count > inner.height as usize {
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
@@ -335,6 +410,9 @@ impl App {
|
||||
WindowKind::Output => {
|
||||
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), inner)
|
||||
}
|
||||
kind @ (WindowKind::Calls | WindowKind::Debug | WindowKind::Immediate) => {
|
||||
self.debug_render(f, kind, inner)
|
||||
}
|
||||
kind => put(
|
||||
f,
|
||||
inner,
|
||||
|
||||
Reference in New Issue
Block a user