From a88904ed0ffb55b5d74bb9991bb1babd39e37005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 23 Jan 2026 10:37:48 +0100 Subject: [PATCH] trace --- src/engine/sequencer.rs | 14 ++-- src/model/forth.rs | 145 ++++++++++++++++++++++++++++------------ src/views/highlight.rs | 71 +++++++++++++------- src/views/render.rs | 80 ++++++++++------------ 4 files changed, 192 insertions(+), 118 deletions(-) diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 241ea51..58e8149 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -6,7 +6,7 @@ use std::time::Duration; use super::LinkState; use crate::config::{MAX_BANKS, MAX_PATTERNS}; -use crate::model::{ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Variables}; +use crate::model::{ExecutionTrace, Rng, ScriptEngine, StepContext, Variables}; use crate::state::LiveKeyState; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] @@ -87,13 +87,13 @@ pub struct ActivePatternState { #[derive(Clone, Default)] pub struct SharedSequencerState { pub active_patterns: Vec, - pub step_traces: HashMap<(usize, usize, usize), Vec>, + pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>, pub event_count: usize, } pub struct SequencerSnapshot { pub active_patterns: Vec, - pub step_traces: HashMap<(usize, usize, usize), Vec>, + pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>, pub event_count: usize, } @@ -118,7 +118,7 @@ impl SequencerSnapshot { .map(|p| p.iter) } - pub fn get_trace(&self, bank: usize, pattern: usize, step: usize) -> Option<&Vec> { + pub fn get_trace(&self, bank: usize, pattern: usize, step: usize) -> Option<&ExecutionTrace> { self.step_traces.get(&(bank, pattern, step)) } } @@ -302,7 +302,7 @@ fn sequencer_loop( let mut audio_state = AudioState::new(); let mut pattern_cache = PatternCache::new(); let mut runs_counter = RunsCounter::new(); - let mut step_traces: HashMap<(usize, usize, usize), Vec> = HashMap::new(); + let mut step_traces: HashMap<(usize, usize, usize), ExecutionTrace> = HashMap::new(); let mut event_count: usize = 0; loop { @@ -409,8 +409,8 @@ fn sequencer_loop( script_engine.evaluate_with_trace(script, &ctx, &mut trace) { step_traces.insert( - (active.bank, active.pattern, step_idx), - std::mem::take(&mut trace.selected_spans), + (active.bank, active.pattern, source_idx), + std::mem::take(&mut trace), ); for cmd in cmds { match audio_tx.try_send(AudioCommand::Evaluate(cmd)) { diff --git a/src/model/forth.rs b/src/model/forth.rs index dd6d46b..c5e5f0c 100644 --- a/src/model/forth.rs +++ b/src/model/forth.rs @@ -11,6 +11,7 @@ pub struct SourceSpan { #[derive(Clone, Debug, Default)] pub struct ExecutionTrace { + pub executed_spans: Vec, pub selected_spans: Vec, } @@ -42,7 +43,7 @@ pub enum Value { Float(f64, Option), Str(String, Option), Marker, - Quotation(Vec), + Quotation(Vec, Option), } impl PartialEq for Value { @@ -52,7 +53,7 @@ impl PartialEq for Value { (Value::Float(a, _), Value::Float(b, _)) => a == b, (Value::Str(a, _), Value::Str(b, _)) => a == b, (Value::Marker, Value::Marker) => true, - (Value::Quotation(a), Value::Quotation(b)) => a == b, + (Value::Quotation(a, _), Value::Quotation(b, _)) => a == b, _ => false, } } @@ -110,7 +111,7 @@ impl Value { Value::Float(f, _) => *f != 0.0, Value::Str(s, _) => !s.is_empty(), Value::Marker => false, - Value::Quotation(_) => true, + Value::Quotation(..) => true, } } @@ -124,14 +125,14 @@ impl Value { Value::Float(f, _) => f.to_string(), Value::Str(s, _) => s.clone(), Value::Marker => String::new(), - Value::Quotation(_) => String::new(), + Value::Quotation(..) => String::new(), } } fn span(&self) -> Option { match self { Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s, - Value::Marker | Value::Quotation(_) => None, + Value::Marker | Value::Quotation(..) => None, } } } @@ -170,7 +171,7 @@ pub enum Op { And, Or, Not, - BranchIfZero(usize), + BranchIfZero(usize, Option, Option), Branch(usize), NewCmd, SetParam(String), @@ -201,7 +202,7 @@ pub enum Op { SetTempo, Each, Every, - Quotation(Vec), + Quotation(Vec, Option), When, Unless, Adsr, @@ -1826,8 +1827,8 @@ enum Token { Float(f64, SourceSpan), Str(String, SourceSpan), Word(String, SourceSpan), - QuoteStart, - QuoteEnd, + QuoteStart(usize), + QuoteEnd(usize), } fn tokenize(input: &str) -> Vec { @@ -1869,13 +1870,13 @@ fn tokenize(input: &str) -> Vec { if c == '{' { chars.next(); - tokens.push(Token::QuoteStart); + tokens.push(Token::QuoteStart(pos)); continue; } if c == '}' { chars.next(); - tokens.push(Token::QuoteEnd); + tokens.push(Token::QuoteEnd(pos)); continue; } @@ -1914,12 +1915,13 @@ fn compile(tokens: &[Token]) -> Result, String> { Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))), Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))), Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))), - Token::QuoteStart => { - let (quote_ops, consumed) = compile_quotation(&tokens[i + 1..])?; + Token::QuoteStart(start_pos) => { + let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..])?; i += consumed; - ops.push(Op::Quotation(quote_ops)); + let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 }; + ops.push(Op::Quotation(quote_ops, Some(body_span))); } - Token::QuoteEnd => { + Token::QuoteEnd(_) => { return Err("unexpected }".into()); } Token::Word(w, span) => { @@ -1932,13 +1934,13 @@ fn compile(tokens: &[Token]) -> Result, String> { } pipe_parity = !pipe_parity; } else if word == "if" { - let (then_ops, else_ops, consumed) = compile_if(&tokens[i + 1..])?; + let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..])?; i += consumed; if else_ops.is_empty() { - ops.push(Op::BranchIfZero(then_ops.len())); + ops.push(Op::BranchIfZero(then_ops.len(), then_span, None)); ops.extend(then_ops); } else { - ops.push(Op::BranchIfZero(then_ops.len() + 1)); + ops.push(Op::BranchIfZero(then_ops.len() + 1, then_span, else_span)); ops.extend(then_ops); ops.push(Op::Branch(else_ops.len())); ops.extend(else_ops); @@ -1954,17 +1956,17 @@ fn compile(tokens: &[Token]) -> Result, String> { Ok(ops) } -fn compile_quotation(tokens: &[Token]) -> Result<(Vec, usize), String> { +fn compile_quotation(tokens: &[Token]) -> Result<(Vec, usize, usize), String> { let mut depth = 1; - let mut end_pos = None; + let mut end_idx = None; for (i, tok) in tokens.iter().enumerate() { match tok { - Token::QuoteStart => depth += 1, - Token::QuoteEnd => { + Token::QuoteStart(_) => depth += 1, + Token::QuoteEnd(_) => { depth -= 1; if depth == 0 { - end_pos = Some(i); + end_idx = Some(i); break; } } @@ -1972,12 +1974,31 @@ fn compile_quotation(tokens: &[Token]) -> Result<(Vec, usize), String> { } } - let end_pos = end_pos.ok_or("missing }")?; - let quote_ops = compile(&tokens[..end_pos])?; - Ok((quote_ops, end_pos + 1)) + let end_idx = end_idx.ok_or("missing }")?; + let byte_pos = match &tokens[end_idx] { + Token::QuoteEnd(pos) => *pos, + _ => unreachable!(), + }; + let quote_ops = compile(&tokens[..end_idx])?; + Ok((quote_ops, end_idx + 1, byte_pos)) } -fn compile_if(tokens: &[Token]) -> Result<(Vec, Vec, usize), String> { +fn token_span(tok: &Token) -> Option { + match tok { + Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s), + Token::QuoteStart(p) => Some(SourceSpan { start: *p, end: *p + 1 }), + Token::QuoteEnd(p) => Some(SourceSpan { start: *p, end: *p + 1 }), + } +} + +fn tokens_span(tokens: &[Token]) -> Option { + let first = tokens.first().and_then(token_span)?; + let last = tokens.last().and_then(token_span)?; + Some(SourceSpan { start: first.start, end: last.end }) +} + +#[allow(clippy::type_complexity)] +fn compile_if(tokens: &[Token]) -> Result<(Vec, Vec, usize, Option, Option), String> { let mut depth = 1; let mut else_pos = None; let mut then_pos = None; @@ -2001,16 +2022,22 @@ fn compile_if(tokens: &[Token]) -> Result<(Vec, Vec, usize), String> { let then_pos = then_pos.ok_or("missing 'then'")?; - let (then_ops, else_ops) = if let Some(ep) = else_pos { - let then_ops = compile(&tokens[..ep])?; - let else_ops = compile(&tokens[ep + 1..then_pos])?; - (then_ops, else_ops) + let (then_ops, else_ops, then_span, else_span) = if let Some(ep) = else_pos { + let then_slice = &tokens[..ep]; + let else_slice = &tokens[ep + 1..then_pos]; + let then_span = tokens_span(then_slice); + let else_span = tokens_span(else_slice); + let then_ops = compile(then_slice)?; + let else_ops = compile(else_slice)?; + (then_ops, else_ops, then_span, else_span) } else { - let then_ops = compile(&tokens[..then_pos])?; - (then_ops, Vec::new()) + let then_slice = &tokens[..then_pos]; + let then_span = tokens_span(then_slice); + let then_ops = compile(then_slice)?; + (then_ops, Vec::new(), then_span, None) }; - Ok((then_ops, else_ops, then_pos + 1)) + Ok((then_ops, else_ops, then_pos + 1, then_span, else_span)) } pub type Stack = Arc>>; @@ -2232,10 +2259,19 @@ impl Forth { stack.push(Value::Int(if v { 0 } else { 1 }, None)); } - Op::BranchIfZero(offset) => { + Op::BranchIfZero(offset, then_span, else_span) => { let v = stack.pop().ok_or("stack underflow")?; if !v.is_truthy() { + if let Some(span) = else_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(*span); + } + } pc += offset; + } else if let Some(span) = then_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(*span); + } } } Op::Branch(offset) => { @@ -2386,7 +2422,12 @@ impl Forth { let val: f64 = self.rng.lock().unwrap().gen(); if val < prob { match quot { - Value::Quotation(quot_ops) => { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; @@ -2402,7 +2443,12 @@ impl Forth { let val: f64 = self.rng.lock().unwrap().gen(); if val < pct / 100.0 { match quot { - Value::Quotation(quot_ops) => { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; @@ -2426,8 +2472,8 @@ impl Forth { stack.push(Value::Int(if result { 1 } else { 0 }, None)); } - Op::Quotation(quote_ops) => { - stack.push(Value::Quotation(quote_ops.clone())); + Op::Quotation(quote_ops, body_span) => { + stack.push(Value::Quotation(quote_ops.clone(), *body_span)); } Op::When => { @@ -2435,7 +2481,12 @@ impl Forth { let quot = stack.pop().ok_or("stack underflow")?; if cond.is_truthy() { match quot { - Value::Quotation(quot_ops) => { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; @@ -2450,7 +2501,12 @@ impl Forth { let quot = stack.pop().ok_or("stack underflow")?; if !cond.is_truthy() { match quot { - Value::Quotation(quot_ops) => { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; @@ -2715,7 +2771,12 @@ impl Forth { .ok_or("for requires subdivide first")?; match quot { - Value::Quotation(quot_ops) => { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } for (i, (sub_start, sub_dur)) in subs.iter().enumerate() { time_stack.push(TimeContext { start: *sub_start, diff --git a/src/views/highlight.rs b/src/views/highlight.rs index 3d7299e..97f5d29 100644 --- a/src/views/highlight.rs +++ b/src/views/highlight.rs @@ -175,22 +175,36 @@ const INTERVALS: &[&str] = &[ "M14", "P15", ]; -fn is_note(word: &str) -> bool { - let bytes = word.as_bytes(); - if bytes.len() < 2 { - return false; - } - if !matches!(bytes[0], b'a'..=b'g' | b'A'..=b'G') { - return false; - } - let rest = &bytes[1..]; - let digits_start = if rest.first().is_some_and(|&b| b == b'#' || b == b's' || b == b'b') { - 1 - } else { - 0 - }; - rest[digits_start..].iter().all(|&b| b.is_ascii_digit()) && digits_start < rest.len() -} +const NOTES: &[&str] = &[ + "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", + "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", + "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", + "g0", "g1", "g2", "g3", "g4", "g5", "g6", "g7", "g8", "g9", + "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", + "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", + "cs0", "cs1", "cs2", "cs3", "cs4", "cs5", "cs6", "cs7", "cs8", "cs9", + "ds0", "ds1", "ds2", "ds3", "ds4", "ds5", "ds6", "ds7", "ds8", "ds9", + "es0", "es1", "es2", "es3", "es4", "es5", "es6", "es7", "es8", "es9", + "fs0", "fs1", "fs2", "fs3", "fs4", "fs5", "fs6", "fs7", "fs8", "fs9", + "gs0", "gs1", "gs2", "gs3", "gs4", "gs5", "gs6", "gs7", "gs8", "gs9", + "as0", "as1", "as2", "as3", "as4", "as5", "as6", "as7", "as8", "as9", + "bs0", "bs1", "bs2", "bs3", "bs4", "bs5", "bs6", "bs7", "bs8", "bs9", + "cb0", "cb1", "cb2", "cb3", "cb4", "cb5", "cb6", "cb7", "cb8", "cb9", + "db0", "db1", "db2", "db3", "db4", "db5", "db6", "db7", "db8", "db9", + "eb0", "eb1", "eb2", "eb3", "eb4", "eb5", "eb6", "eb7", "eb8", "eb9", + "fb0", "fb1", "fb2", "fb3", "fb4", "fb5", "fb6", "fb7", "fb8", "fb9", + "gb0", "gb1", "gb2", "gb3", "gb4", "gb5", "gb6", "gb7", "gb8", "gb9", + "ab0", "ab1", "ab2", "ab3", "ab4", "ab5", "ab6", "ab7", "ab8", "ab9", + "bb0", "bb1", "bb2", "bb3", "bb4", "bb5", "bb6", "bb7", "bb8", "bb9", + "c#0", "c#1", "c#2", "c#3", "c#4", "c#5", "c#6", "c#7", "c#8", "c#9", + "d#0", "d#1", "d#2", "d#3", "d#4", "d#5", "d#6", "d#7", "d#8", "d#9", + "e#0", "e#1", "e#2", "e#3", "e#4", "e#5", "e#6", "e#7", "e#8", "e#9", + "f#0", "f#1", "f#2", "f#3", "f#4", "f#5", "f#6", "f#7", "f#8", "f#9", + "g#0", "g#1", "g#2", "g#3", "g#4", "g#5", "g#6", "g#7", "g#8", "g#9", + "a#0", "a#1", "a#2", "a#3", "a#4", "a#5", "a#6", "a#7", "a#8", "a#9", + "b#0", "b#1", "b#2", "b#3", "b#4", "b#5", "b#6", "b#7", "b#8", "b#9", +]; pub fn tokenize_line(line: &str) -> Vec { let mut tokens = Vec::new(); @@ -287,7 +301,8 @@ fn classify_word(word: &str) -> TokenKind { return TokenKind::Interval; } - if is_note(word) { + let lower = word.to_ascii_lowercase(); + if NOTES.contains(&lower.as_str()) { return TokenKind::Note; } @@ -299,15 +314,20 @@ fn classify_word(word: &str) -> TokenKind { } pub fn highlight_line(line: &str) -> Vec<(Style, String)> { - highlight_line_with_runtime(line, &[]) + highlight_line_with_runtime(line, &[], &[]) } -pub fn highlight_line_with_runtime(line: &str, runtime_spans: &[SourceSpan]) -> Vec<(Style, String)> { +pub fn highlight_line_with_runtime( + line: &str, + executed_spans: &[SourceSpan], + selected_spans: &[SourceSpan], +) -> Vec<(Style, String)> { let tokens = tokenize_line(line); let mut result = Vec::new(); let mut last_end = 0; - let runtime_bg = Color::Rgb(80, 60, 20); + let executed_bg = Color::Rgb(40, 35, 50); + let selected_bg = Color::Rgb(80, 60, 20); for token in tokens { if token.start > last_end { @@ -317,13 +337,18 @@ pub fn highlight_line_with_runtime(line: &str, runtime_spans: &[SourceSpan]) -> )); } - let is_runtime = runtime_spans + let is_selected = selected_spans + .iter() + .any(|span| overlaps(token.start, token.end, span.start, span.end)); + let is_executed = executed_spans .iter() .any(|span| overlaps(token.start, token.end, span.start, span.end)); let mut style = token.kind.style(); - if is_runtime { - style = style.bg(runtime_bg).add_modifier(Modifier::BOLD); + if is_selected { + style = style.bg(selected_bg).add_modifier(Modifier::BOLD); + } else if is_executed { + style = style.bg(executed_bg); } result.push((style, line[token.start..token.end].to_string())); diff --git a/src/views/render.rs b/src/views/render.rs index e1f4153..507b12b 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -6,7 +6,7 @@ use ratatui::Frame; use crate::app::App; use crate::engine::{LinkState, SequencerSnapshot}; -use crate::model::forth::SourceSpan; +use crate::model::SourceSpan; use crate::page::Page; use crate::state::{Modal, PatternField}; use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime}; @@ -14,6 +14,18 @@ use crate::widgets::{ConfirmModal, ModalFrame, TextInputModal}; use super::{audio_view, doc_view, main_view, patterns_view, title_view}; +fn adjust_spans_for_line(spans: &[SourceSpan], line_start: usize, line_len: usize) -> Vec { + spans.iter().filter_map(|s| { + if s.end <= line_start || s.start >= line_start + line_len { + return None; + } + Some(SourceSpan { + start: s.start.max(line_start) - line_start, + end: (s.end.min(line_start + line_len)) - line_start, + }) + }).collect() +} + pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &SequencerSnapshot) { let term = frame.area(); let blank = " ".repeat(term.width as usize); @@ -393,41 +405,25 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term }; frame.render_widget(empty, centered_area); } else { - let runtime_spans = if app.ui.runtime_highlight && app.playback.playing { - snapshot.get_trace( - app.editor_ctx.bank, - app.editor_ctx.pattern, - step_idx, - ) + let trace = if app.ui.runtime_highlight && app.playback.playing { + let source = pattern.resolve_source(step_idx); + snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source) } else { None }; - let mut offset = 0usize; + let mut line_start = 0usize; let lines: Vec = script .lines() .map(|line_str| { - let tokens = if let Some(traces) = runtime_spans { - let shifted: Vec<_> = traces - .iter() - .filter_map(|s| { - let start = s.start.saturating_sub(offset); - let end = s.end.saturating_sub(offset); - if end > 0 && start < line_str.len() { - Some(SourceSpan { - start: start.min(line_str.len()), - end: end.min(line_str.len()), - }) - } else { - None - } - }) - .collect(); - highlight_line_with_runtime(line_str, &shifted) + let tokens = if let Some(t) = trace { + let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len()); + let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len()); + highlight_line_with_runtime(line_str, &exec, &sel) } else { highlight_line(line_str) }; - offset += line_str.len() + 1; + line_start += line_str.len() + 1; let spans: Vec = tokens .into_iter() .map(|(style, text)| Span::styled(text, style)) @@ -459,8 +455,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let (cursor_row, cursor_col) = app.editor_ctx.text.cursor(); - let runtime_spans = if app.ui.runtime_highlight && app.playback.playing { - snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, app.editor_ctx.step) + let trace = if app.ui.runtime_highlight && app.playback.playing { + let source = app.current_edit_pattern().resolve_source(app.editor_ctx.step); + snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source) } else { None }; @@ -480,25 +477,16 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let mut spans: Vec = Vec::new(); let line_start = line_offsets[row]; - let line_end = line_start + line.len(); - let adjusted_spans: Vec = runtime_spans - .map(|rs| { - rs.iter() - .filter_map(|s| { - if s.start < line_end && s.end > line_start { - Some(crate::model::SourceSpan { - start: s.start.saturating_sub(line_start), - end: s.end.saturating_sub(line_start).min(line.len()), - }) - } else { - None - } - }) - .collect() - }) - .unwrap_or_default(); + let (exec_spans, sel_spans) = if let Some(t) = trace { + ( + adjust_spans_for_line(&t.executed_spans, line_start, line.len()), + adjust_spans_for_line(&t.selected_spans, line_start, line.len()), + ) + } else { + (Vec::new(), Vec::new()) + }; - let tokens = highlight::highlight_line_with_runtime(line, &adjusted_spans); + let tokens = highlight::highlight_line_with_runtime(line, &exec_spans, &sel_spans); if row == cursor_row { let mut col = 0;