246 lines
6.8 KiB
Rust
246 lines
6.8 KiB
Rust
use ratatui::style::{Modifier, Style};
|
|
|
|
use crate::model::{SourceSpan, WordCompile, WORDS};
|
|
use crate::theme::syntax;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum TokenKind {
|
|
Number,
|
|
String,
|
|
Comment,
|
|
Keyword,
|
|
StackOp,
|
|
Operator,
|
|
Sound,
|
|
Param,
|
|
Context,
|
|
Note,
|
|
Interval,
|
|
Variable,
|
|
Emit,
|
|
Vary,
|
|
Generator,
|
|
Default,
|
|
}
|
|
|
|
impl TokenKind {
|
|
pub fn style(self) -> Style {
|
|
let (fg, bg) = match self {
|
|
TokenKind::Emit => syntax::EMIT,
|
|
TokenKind::Number => syntax::NUMBER,
|
|
TokenKind::String => syntax::STRING,
|
|
TokenKind::Comment => syntax::COMMENT,
|
|
TokenKind::Keyword => syntax::KEYWORD,
|
|
TokenKind::StackOp => syntax::STACK_OP,
|
|
TokenKind::Operator => syntax::OPERATOR,
|
|
TokenKind::Sound => syntax::SOUND,
|
|
TokenKind::Param => syntax::PARAM,
|
|
TokenKind::Context => syntax::CONTEXT,
|
|
TokenKind::Note => syntax::NOTE,
|
|
TokenKind::Interval => syntax::INTERVAL,
|
|
TokenKind::Variable => syntax::VARIABLE,
|
|
TokenKind::Vary => syntax::VARY,
|
|
TokenKind::Generator => syntax::GENERATOR,
|
|
TokenKind::Default => syntax::DEFAULT,
|
|
};
|
|
let style = Style::default().fg(fg).bg(bg);
|
|
if matches!(self, TokenKind::Emit) {
|
|
style.add_modifier(Modifier::BOLD)
|
|
} else {
|
|
style
|
|
}
|
|
}
|
|
|
|
pub fn gap_style() -> Style {
|
|
Style::default().bg(syntax::GAP_BG)
|
|
}
|
|
}
|
|
|
|
pub struct Token {
|
|
pub start: usize,
|
|
pub end: usize,
|
|
pub kind: TokenKind,
|
|
pub varargs: bool,
|
|
}
|
|
|
|
fn lookup_word_kind(word: &str) -> Option<(TokenKind, bool)> {
|
|
if word == "." {
|
|
return Some((TokenKind::Emit, false));
|
|
}
|
|
if word == ".!" {
|
|
return Some((TokenKind::Emit, true));
|
|
}
|
|
|
|
for w in WORDS {
|
|
if w.name == word || w.aliases.contains(&word) {
|
|
let kind = match &w.compile {
|
|
WordCompile::Param => TokenKind::Param,
|
|
WordCompile::Context(_) => TokenKind::Context,
|
|
_ => match w.category {
|
|
"Stack" => TokenKind::StackOp,
|
|
"Arithmetic" | "Comparison" | "Music" => TokenKind::Operator,
|
|
"Logic" if matches!(w.name, "and" | "or" | "not" | "xor" | "nand" | "nor") => {
|
|
TokenKind::Operator
|
|
}
|
|
"Sound" => TokenKind::Sound,
|
|
"Randomness" | "Probability" | "Selection" => TokenKind::Vary,
|
|
"Generator" => TokenKind::Generator,
|
|
_ => TokenKind::Keyword,
|
|
},
|
|
};
|
|
return Some((kind, w.varargs));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn is_note(word: &str) -> bool {
|
|
let bytes = word.as_bytes();
|
|
if bytes.len() < 2 {
|
|
return false;
|
|
}
|
|
let base = matches!(bytes[0], b'c' | b'd' | b'e' | b'f' | b'g' | b'a' | b'b');
|
|
if !base {
|
|
return false;
|
|
}
|
|
match bytes[1] {
|
|
b'#' | b's' | b'b' => bytes.len() > 2 && bytes[2..].iter().all(|b| b.is_ascii_digit()),
|
|
b'0'..=b'9' => bytes[1..].iter().all(|b| b.is_ascii_digit()),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
const INTERVALS: &[&str] = &[
|
|
"P1", "unison", "m2", "M2", "m3", "M3", "P4", "aug4", "dim5", "tritone", "P5", "m6", "M6",
|
|
"m7", "M7", "P8", "oct", "m9", "M9", "m10", "M10", "P11", "aug11", "P12", "m13", "M13", "m14",
|
|
"M14", "P15",
|
|
];
|
|
|
|
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 == ';' && chars.peek().map(|(_, ch)| *ch) == Some(';') {
|
|
tokens.push(Token {
|
|
start,
|
|
end: line.len(),
|
|
kind: TokenKind::Comment,
|
|
varargs: false,
|
|
});
|
|
break;
|
|
}
|
|
|
|
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,
|
|
varargs: false,
|
|
});
|
|
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, varargs) = classify_word(word);
|
|
tokens.push(Token { start, end, kind, varargs });
|
|
}
|
|
|
|
tokens
|
|
}
|
|
|
|
fn classify_word(word: &str) -> (TokenKind, bool) {
|
|
if word.parse::<f64>().is_ok() || word.parse::<i64>().is_ok() {
|
|
return (TokenKind::Number, false);
|
|
}
|
|
|
|
if let Some((kind, varargs)) = lookup_word_kind(word) {
|
|
return (kind, varargs);
|
|
}
|
|
|
|
if INTERVALS.contains(&word) {
|
|
return (TokenKind::Interval, false);
|
|
}
|
|
|
|
if is_note(&word.to_ascii_lowercase()) {
|
|
return (TokenKind::Note, false);
|
|
}
|
|
|
|
if word.len() > 1 && (word.starts_with('@') || word.starts_with('!')) {
|
|
return (TokenKind::Variable, false);
|
|
}
|
|
|
|
(TokenKind::Default, false)
|
|
}
|
|
|
|
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
|
|
highlight_line_with_runtime(line, &[], &[])
|
|
}
|
|
|
|
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 gap_style = TokenKind::gap_style();
|
|
|
|
for token in tokens {
|
|
if token.start > last_end {
|
|
result.push((gap_style, line[last_end..token.start].to_string()));
|
|
}
|
|
|
|
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 token.varargs {
|
|
style = style.add_modifier(Modifier::UNDERLINED);
|
|
}
|
|
if is_selected {
|
|
style = style.bg(syntax::SELECTED_BG).add_modifier(Modifier::BOLD);
|
|
} else if is_executed {
|
|
style = style.bg(syntax::EXECUTED_BG);
|
|
}
|
|
|
|
result.push((style, line[token.start..token.end].to_string()));
|
|
last_end = token.end;
|
|
}
|
|
|
|
if last_end < line.len() {
|
|
result.push((gap_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
|
|
}
|