Files
storybook/storybook-editor/src/highlighter.rs

124 lines
3.8 KiB
Rust
Raw Normal View History

//! Syntax highlighter for Storybook DSL
//!
//! Implements iced's Highlighter trait to provide line-by-line syntax
//! highlighting.
use std::ops::Range;
use iced::{
Color,
advanced::text::highlighter,
};
use crate::syntax_highlight::{
token_to_color_contextual,
tokenize,
};
/// Settings for the Storybook highlighter
#[derive(Debug, Clone, PartialEq)]
pub struct Settings;
/// Storybook syntax highlighter
#[derive(Debug)]
pub struct StorybookHighlighter {
current_line: usize,
in_prose_block: bool, // Track if we're inside a prose block
}
impl iced::widget::text::Highlighter for StorybookHighlighter {
type Highlight = Color;
type Iterator<'a> = std::vec::IntoIter<(Range<usize>, Self::Highlight)>;
type Settings = Settings;
fn new(_settings: &Self::Settings) -> Self {
Self {
current_line: 0,
in_prose_block: false,
}
}
fn update(&mut self, _new_settings: &Self::Settings) {
// No settings to update
}
fn change_line(&mut self, line: usize) {
self.current_line = line;
// Reset prose block state when jumping to a different line
self.in_prose_block = false;
}
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
let mut highlights = Vec::new();
// Check if this line starts or ends a prose block
let trimmed = line.trim_start();
if trimmed.starts_with("---") {
// This is a prose marker line
if self.in_prose_block {
// Ending a prose block
self.in_prose_block = false;
} else {
// Starting a prose block
self.in_prose_block = true;
}
// Color the entire line as a prose marker (gray)
if !line.is_empty() {
highlights.push((0..line.len(), Color::from_rgb8(0x6d, 0x6d, 0x6d)));
}
} else if self.in_prose_block {
// Inside a prose block - render as plain text in peach color
if !line.is_empty() {
highlights.push((0..line.len(), Color::from_rgb8(0xff, 0xb8, 0x6c)));
}
} else {
// Regular code - tokenize and highlight
let tokens = tokenize(line);
// Track if we're after a colon for context-aware coloring
let mut after_colon = false;
for (token, range) in tokens {
let color = token_to_color_contextual(&token, after_colon);
highlights.push((range, color));
// Update context: set after_colon when we see a colon,
// reset it when we see an identifier (field value) or certain other tokens
use storybook::syntax::lexer::Token;
match &token {
| Token::Colon => after_colon = true,
| Token::Ident(_) |
Token::IntLit(_) |
Token::FloatLit(_) |
Token::StringLit(_) |
Token::True |
Token::False |
Token::TimeLit(_) |
Token::DurationLit(_) => {
// Reset after consuming a value
after_colon = false;
},
// Don't reset for whitespace, commas, or other punctuation
| _ => {},
}
}
}
self.current_line += 1;
highlights.into_iter()
}
fn current_line(&self) -> usize {
self.current_line
}
}
/// Convert a highlight (Color) to a Format for rendering
pub fn to_format(color: &Color, _theme: &iced::Theme) -> highlighter::Format<iced::Font> {
highlighter::Format {
color: Some(*color),
font: None,
}
}