Implement and archive Phase 5 debugger

This commit is contained in:
2026-09-06 21:24:38 +02:00
parent ce97700a98
commit e236306657
25 changed files with 3102 additions and 52 deletions

View File

@@ -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(());
}

View File

@@ -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,
}

View 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),
);
}
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);
}
}
}
}

View File

@@ -12,3 +12,5 @@ pub mod editor;
pub mod execution;
pub mod designer;
pub mod debugger;

View File

@@ -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,