This commit is contained in:
2026-01-23 10:37:48 +01:00
parent 1bb5ba0061
commit a88904ed0f
4 changed files with 192 additions and 118 deletions

View File

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

View File

@@ -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<SourceSpan> {
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<Line> = 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<Span> = 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<Span> = Vec::new();
let line_start = line_offsets[row];
let line_end = line_start + line.len();
let adjusted_spans: Vec<crate::model::SourceSpan> = 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;