This commit is contained in:
2026-01-21 17:05:30 +01:00
commit 67322381c3
59 changed files with 10421 additions and 0 deletions

299
src/views/highlight.rs Normal file
View File

@@ -0,0 +1,299 @@
use ratatui::style::{Color, Modifier, Style};
use crate::model::SourceSpan;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TokenKind {
Number,
String,
Comment,
Keyword,
StackOp,
Operator,
Sound,
Param,
Context,
Default,
}
impl TokenKind {
pub fn style(self) -> Style {
match self {
TokenKind::Number => Style::default().fg(Color::Rgb(255, 180, 100)),
TokenKind::String => Style::default().fg(Color::Rgb(150, 220, 150)),
TokenKind::Comment => Style::default().fg(Color::Rgb(100, 100, 100)),
TokenKind::Keyword => Style::default().fg(Color::Rgb(220, 120, 220)),
TokenKind::StackOp => Style::default().fg(Color::Rgb(120, 180, 220)),
TokenKind::Operator => Style::default().fg(Color::Rgb(200, 200, 130)),
TokenKind::Sound => Style::default().fg(Color::Rgb(100, 220, 200)),
TokenKind::Param => Style::default().fg(Color::Rgb(180, 150, 220)),
TokenKind::Context => Style::default().fg(Color::Rgb(220, 180, 120)),
TokenKind::Default => Style::default().fg(Color::Rgb(200, 200, 200)),
}
}
}
pub struct Token {
pub start: usize,
pub end: usize,
pub kind: TokenKind,
}
const STACK_OPS: &[&str] = &["dup", "drop", "swap", "over", "rot", "nip", "tuck"];
const OPERATORS: &[&str] = &[
"+", "-", "*", "/", "mod", "neg", "abs", "min", "max", "=", "<>", "<", ">", "<=", ">=", "and",
"or", "not",
];
const KEYWORDS: &[&str] = &[
"if", "else", "then", "emit", "get", "set", "rand", "rrand", "seed", "cycle", "choose",
"chance", "[", "]",
];
const SOUND: &[&str] = &["sound", "s"];
const CONTEXT: &[&str] = &[
"step", "beat", "bank", "pattern", "tempo", "phase", "slot", "runs",
];
const PARAMS: &[&str] = &[
"time",
"repeat",
"dur",
"gate",
"freq",
"detune",
"speed",
"glide",
"pw",
"spread",
"mult",
"warp",
"mirror",
"harmonics",
"timbre",
"morph",
"begin",
"end",
"gain",
"postgain",
"velocity",
"pan",
"attack",
"decay",
"sustain",
"release",
"lpf",
"lpq",
"lpe",
"lpa",
"lpd",
"lps",
"lpr",
"hpf",
"hpq",
"hpe",
"hpa",
"hpd",
"hps",
"hpr",
"bpf",
"bpq",
"bpe",
"bpa",
"bpd",
"bps",
"bpr",
"ftype",
"penv",
"patt",
"pdec",
"psus",
"prel",
"vib",
"vibmod",
"vibshape",
"fm",
"fmh",
"fmshape",
"fme",
"fma",
"fmd",
"fms",
"fmr",
"am",
"amdepth",
"amshape",
"rm",
"rmdepth",
"rmshape",
"phaser",
"phaserdepth",
"phasersweep",
"phasercenter",
"flanger",
"flangerdepth",
"flangerfeedback",
"chorus",
"chorusdepth",
"chorusdelay",
"comb",
"combfreq",
"combfeedback",
"combdamp",
"coarse",
"crush",
"fold",
"wrap",
"distort",
"distortvol",
"delay",
"delaytime",
"delayfeedback",
"delaytype",
"verb",
"verbdecay",
"verbdamp",
"verbpredelay",
"verbdiff",
"voice",
"orbit",
"note",
"size",
"n",
"cut",
"reset",
];
pub fn tokenize_line(line: &str) -> Vec<Token> {
let mut tokens = Vec::new();
let mut chars = line.char_indices().peekable();
while let Some((start, c)) = chars.next() {
if c.is_whitespace() {
continue;
}
if c == '(' {
let end = line.len();
let comment_end = line[start..]
.find(')')
.map(|i| start + i + 1)
.unwrap_or(end);
tokens.push(Token {
start,
end: comment_end,
kind: TokenKind::Comment,
});
while let Some((i, _)) = chars.peek() {
if *i >= comment_end {
break;
}
chars.next();
}
continue;
}
if c == '"' {
let mut end = start + 1;
for (i, ch) in chars.by_ref() {
end = i + ch.len_utf8();
if ch == '"' {
break;
}
}
tokens.push(Token {
start,
end,
kind: TokenKind::String,
});
continue;
}
let mut end = start + c.len_utf8();
while let Some((i, ch)) = chars.peek() {
if ch.is_whitespace() {
break;
}
end = *i + ch.len_utf8();
chars.next();
}
let word = &line[start..end];
let kind = classify_word(word);
tokens.push(Token { start, end, kind });
}
tokens
}
fn classify_word(word: &str) -> TokenKind {
if word.parse::<f64>().is_ok() || word.parse::<i64>().is_ok() {
return TokenKind::Number;
}
if STACK_OPS.contains(&word) {
return TokenKind::StackOp;
}
if OPERATORS.contains(&word) {
return TokenKind::Operator;
}
if KEYWORDS.contains(&word) {
return TokenKind::Keyword;
}
if SOUND.contains(&word) {
return TokenKind::Sound;
}
if CONTEXT.contains(&word) {
return TokenKind::Context;
}
if PARAMS.contains(&word) {
return TokenKind::Param;
}
TokenKind::Default
}
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
highlight_line_with_runtime(line, &[])
}
pub fn highlight_line_with_runtime(line: &str, runtime_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);
for token in tokens {
if token.start > last_end {
result.push((
TokenKind::Default.style(),
line[last_end..token.start].to_string(),
));
}
let is_runtime = runtime_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);
}
result.push((style, line[token.start..token.end].to_string()));
last_end = token.end;
}
if last_end < line.len() {
result.push((TokenKind::Default.style(), line[last_end..].to_string()));
}
result
}
fn overlaps(a_start: usize, a_end: usize, b_start: usize, b_end: usize) -> bool {
a_start < b_end && b_start < a_end
}