Skip to content

Commit

Permalink
Implement markdown incremental code highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
hecrj committed Jan 31, 2025
1 parent 128058e commit 4b8fc23
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 104 deletions.
81 changes: 47 additions & 34 deletions examples/markdown/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct Markdown {
}

enum Mode {
Oneshot(Vec<markdown::Item>),
Preview(Vec<markdown::Item>),
Stream {
pending: String,
parsed: markdown::Content,
Expand All @@ -43,63 +43,72 @@ impl Markdown {
(
Self {
content: text_editor::Content::with_text(INITIAL_CONTENT),
mode: Mode::Oneshot(markdown::parse(INITIAL_CONTENT).collect()),
mode: Mode::Preview(markdown::parse(INITIAL_CONTENT).collect()),
theme,
},
widget::focus_next(),
)
}

fn update(&mut self, message: Message) {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Edit(action) => {
let is_edit = action.is_edit();

self.content.perform(action);

if is_edit {
self.mode = match self.mode {
Mode::Oneshot(_) => Mode::Oneshot(
markdown::parse(&self.content.text()).collect(),
),
Mode::Stream { .. } => Mode::Stream {
pending: self.content.text(),
parsed: markdown::Content::parse(""),
},
}
self.mode = Mode::Preview(
markdown::parse(&self.content.text()).collect(),
);
}

Task::none()
}
Message::LinkClicked(link) => {
let _ = open::that_in_background(link.to_string());

Task::none()
}
Message::ToggleStream(enable_stream) => {
self.mode = if enable_stream {
Mode::Stream {
if enable_stream {
self.mode = Mode::Stream {
pending: self.content.text(),
parsed: markdown::Content::parse(""),
}
};

scrollable::snap_to(
"preview",
scrollable::RelativeOffset::END,
)
} else {
Mode::Oneshot(
self.mode = Mode::Preview(
markdown::parse(&self.content.text()).collect(),
)
};
);

Task::none()
}
}
Message::NextToken => match &mut self.mode {
Mode::Oneshot(_) => {}
Mode::Stream { pending, parsed } => {
if pending.is_empty() {
self.mode = Mode::Oneshot(parsed.items().to_vec());
} else {
let mut tokens = pending.split(' ');

if let Some(token) = tokens.next() {
parsed.push_str(&format!("{token} "));
Message::NextToken => {
match &mut self.mode {
Mode::Preview(_) => {}
Mode::Stream { pending, parsed } => {
if pending.is_empty() {
self.mode = Mode::Preview(parsed.items().to_vec());
} else {
let mut tokens = pending.split(' ');

if let Some(token) = tokens.next() {
parsed.push_str(&format!("{token} "));
}

*pending = tokens.collect::<Vec<_>>().join(" ");
}

*pending = tokens.collect::<Vec<_>>().join(" ");
}
}
},

Task::none()
}
}
}

Expand All @@ -113,7 +122,7 @@ impl Markdown {
.highlight("markdown", highlighter::Theme::Base16Ocean);

let items = match &self.mode {
Mode::Oneshot(items) => items.as_slice(),
Mode::Preview(items) => items.as_slice(),
Mode::Stream { parsed, .. } => parsed.items(),
};

Expand All @@ -127,7 +136,11 @@ impl Markdown {
row![
editor,
hover(
scrollable(preview).spacing(10).width(Fill).height(Fill),
scrollable(preview)
.spacing(10)
.width(Fill)
.height(Fill)
.id("preview"),
right(
toggler(matches!(self.mode, Mode::Stream { .. }))
.label("Stream")
Expand All @@ -147,7 +160,7 @@ impl Markdown {

fn subscription(&self) -> Subscription<Message> {
match self.mode {
Mode::Oneshot(_) => Subscription::none(),
Mode::Preview(_) => Subscription::none(),
Mode::Stream { .. } => {
time::every(milliseconds(20)).map(|_| Message::NextToken)
}
Expand Down
112 changes: 88 additions & 24 deletions highlighter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::core::Color;

use std::ops::Range;
use std::sync::LazyLock;

use syntect::highlighting;
use syntect::parsing;

Expand Down Expand Up @@ -104,37 +105,100 @@ impl highlighter::Highlighter for Highlighter {

let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default();

let highlighter = &self.highlighter;

Box::new(
ScopeRangeIterator {
ops,
line_length: line.len(),
index: 0,
last_str_index: 0,
}
.filter_map(move |(range, scope)| {
let _ = stack.apply(&scope);

if range.is_empty() {
None
} else {
Some((
range,
Highlight(
highlighter.style_mod_for_stack(&stack.scopes),
),
))
}
}),
)
Box::new(scope_iterator(ops, line, stack, &self.highlighter))
}

fn current_line(&self) -> usize {
self.current_line
}
}

fn scope_iterator<'a>(
ops: Vec<(usize, parsing::ScopeStackOp)>,
line: &str,
stack: &'a mut parsing::ScopeStack,
highlighter: &'a highlighting::Highlighter<'static>,
) -> impl Iterator<Item = (Range<usize>, Highlight)> + 'a {
ScopeRangeIterator {
ops,
line_length: line.len(),
index: 0,
last_str_index: 0,
}
.filter_map(move |(range, scope)| {
let _ = stack.apply(&scope);

if range.is_empty() {
None
} else {
Some((
range,
Highlight(highlighter.style_mod_for_stack(&stack.scopes)),
))
}
})
}

/// A streaming syntax highlighter.
///
/// It can efficiently highlight an immutable stream of tokens.
#[derive(Debug)]
pub struct Stream {
syntax: &'static parsing::SyntaxReference,
highlighter: highlighting::Highlighter<'static>,
commit: (parsing::ParseState, parsing::ScopeStack),
state: parsing::ParseState,
stack: parsing::ScopeStack,
}

impl Stream {
/// Creates a new [`Stream`] highlighter.
pub fn new(settings: &Settings) -> Self {
let syntax = SYNTAXES
.find_syntax_by_token(&settings.token)
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());

let highlighter = highlighting::Highlighter::new(
&THEMES.themes[settings.theme.key()],
);

let state = parsing::ParseState::new(syntax);
let stack = parsing::ScopeStack::new();

Self {
syntax,
highlighter,
commit: (state.clone(), stack.clone()),
state,
stack,
}
}

/// Highlights the given line from the last commit.
pub fn highlight_line(
&mut self,
line: &str,
) -> impl Iterator<Item = (Range<usize>, Highlight)> + '_ {
self.state = self.commit.0.clone();
self.stack = self.commit.1.clone();

let ops = self.state.parse_line(line, &SYNTAXES).unwrap_or_default();
scope_iterator(ops, line, &mut self.stack, &self.highlighter)
}

/// Commits the last highlighted line.
pub fn commit(&mut self) {
self.commit = (self.state.clone(), self.stack.clone());
}

/// Resets the [`Stream`] highlighter.
pub fn reset(&mut self) {
self.state = parsing::ParseState::new(self.syntax);
self.stack = parsing::ScopeStack::new();
self.commit = (self.state.clone(), self.stack.clone());
}
}

/// The settings of a [`Highlighter`].
#[derive(Debug, Clone, PartialEq)]
pub struct Settings {
Expand Down
Loading

0 comments on commit 4b8fc23

Please sign in to comment.