Skip to content

Commit 4b8fc23

Browse files
committed
Implement markdown incremental code highlighting
1 parent 128058e commit 4b8fc23

File tree

3 files changed

+264
-104
lines changed

3 files changed

+264
-104
lines changed

examples/markdown/src/main.rs

+47-34
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct Markdown {
1919
}
2020

2121
enum Mode {
22-
Oneshot(Vec<markdown::Item>),
22+
Preview(Vec<markdown::Item>),
2323
Stream {
2424
pending: String,
2525
parsed: markdown::Content,
@@ -43,63 +43,72 @@ impl Markdown {
4343
(
4444
Self {
4545
content: text_editor::Content::with_text(INITIAL_CONTENT),
46-
mode: Mode::Oneshot(markdown::parse(INITIAL_CONTENT).collect()),
46+
mode: Mode::Preview(markdown::parse(INITIAL_CONTENT).collect()),
4747
theme,
4848
},
4949
widget::focus_next(),
5050
)
5151
}
5252

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

5858
self.content.perform(action);
5959

6060
if is_edit {
61-
self.mode = match self.mode {
62-
Mode::Oneshot(_) => Mode::Oneshot(
63-
markdown::parse(&self.content.text()).collect(),
64-
),
65-
Mode::Stream { .. } => Mode::Stream {
66-
pending: self.content.text(),
67-
parsed: markdown::Content::parse(""),
68-
},
69-
}
61+
self.mode = Mode::Preview(
62+
markdown::parse(&self.content.text()).collect(),
63+
);
7064
}
65+
66+
Task::none()
7167
}
7268
Message::LinkClicked(link) => {
7369
let _ = open::that_in_background(link.to_string());
70+
71+
Task::none()
7472
}
7573
Message::ToggleStream(enable_stream) => {
76-
self.mode = if enable_stream {
77-
Mode::Stream {
74+
if enable_stream {
75+
self.mode = Mode::Stream {
7876
pending: self.content.text(),
7977
parsed: markdown::Content::parse(""),
80-
}
78+
};
79+
80+
scrollable::snap_to(
81+
"preview",
82+
scrollable::RelativeOffset::END,
83+
)
8184
} else {
82-
Mode::Oneshot(
85+
self.mode = Mode::Preview(
8386
markdown::parse(&self.content.text()).collect(),
84-
)
85-
};
87+
);
88+
89+
Task::none()
90+
}
8691
}
87-
Message::NextToken => match &mut self.mode {
88-
Mode::Oneshot(_) => {}
89-
Mode::Stream { pending, parsed } => {
90-
if pending.is_empty() {
91-
self.mode = Mode::Oneshot(parsed.items().to_vec());
92-
} else {
93-
let mut tokens = pending.split(' ');
94-
95-
if let Some(token) = tokens.next() {
96-
parsed.push_str(&format!("{token} "));
92+
Message::NextToken => {
93+
match &mut self.mode {
94+
Mode::Preview(_) => {}
95+
Mode::Stream { pending, parsed } => {
96+
if pending.is_empty() {
97+
self.mode = Mode::Preview(parsed.items().to_vec());
98+
} else {
99+
let mut tokens = pending.split(' ');
100+
101+
if let Some(token) = tokens.next() {
102+
parsed.push_str(&format!("{token} "));
103+
}
104+
105+
*pending = tokens.collect::<Vec<_>>().join(" ");
97106
}
98-
99-
*pending = tokens.collect::<Vec<_>>().join(" ");
100107
}
101108
}
102-
},
109+
110+
Task::none()
111+
}
103112
}
104113
}
105114

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

115124
let items = match &self.mode {
116-
Mode::Oneshot(items) => items.as_slice(),
125+
Mode::Preview(items) => items.as_slice(),
117126
Mode::Stream { parsed, .. } => parsed.items(),
118127
};
119128

@@ -127,7 +136,11 @@ impl Markdown {
127136
row![
128137
editor,
129138
hover(
130-
scrollable(preview).spacing(10).width(Fill).height(Fill),
139+
scrollable(preview)
140+
.spacing(10)
141+
.width(Fill)
142+
.height(Fill)
143+
.id("preview"),
131144
right(
132145
toggler(matches!(self.mode, Mode::Stream { .. }))
133146
.label("Stream")
@@ -147,7 +160,7 @@ impl Markdown {
147160

148161
fn subscription(&self) -> Subscription<Message> {
149162
match self.mode {
150-
Mode::Oneshot(_) => Subscription::none(),
163+
Mode::Preview(_) => Subscription::none(),
151164
Mode::Stream { .. } => {
152165
time::every(milliseconds(20)).map(|_| Message::NextToken)
153166
}

highlighter/src/lib.rs

+88-24
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::core::Color;
77

88
use std::ops::Range;
99
use std::sync::LazyLock;
10+
1011
use syntect::highlighting;
1112
use syntect::parsing;
1213

@@ -104,37 +105,100 @@ impl highlighter::Highlighter for Highlighter {
104105

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

107-
let highlighter = &self.highlighter;
108-
109-
Box::new(
110-
ScopeRangeIterator {
111-
ops,
112-
line_length: line.len(),
113-
index: 0,
114-
last_str_index: 0,
115-
}
116-
.filter_map(move |(range, scope)| {
117-
let _ = stack.apply(&scope);
118-
119-
if range.is_empty() {
120-
None
121-
} else {
122-
Some((
123-
range,
124-
Highlight(
125-
highlighter.style_mod_for_stack(&stack.scopes),
126-
),
127-
))
128-
}
129-
}),
130-
)
108+
Box::new(scope_iterator(ops, line, stack, &self.highlighter))
131109
}
132110

133111
fn current_line(&self) -> usize {
134112
self.current_line
135113
}
136114
}
137115

116+
fn scope_iterator<'a>(
117+
ops: Vec<(usize, parsing::ScopeStackOp)>,
118+
line: &str,
119+
stack: &'a mut parsing::ScopeStack,
120+
highlighter: &'a highlighting::Highlighter<'static>,
121+
) -> impl Iterator<Item = (Range<usize>, Highlight)> + 'a {
122+
ScopeRangeIterator {
123+
ops,
124+
line_length: line.len(),
125+
index: 0,
126+
last_str_index: 0,
127+
}
128+
.filter_map(move |(range, scope)| {
129+
let _ = stack.apply(&scope);
130+
131+
if range.is_empty() {
132+
None
133+
} else {
134+
Some((
135+
range,
136+
Highlight(highlighter.style_mod_for_stack(&stack.scopes)),
137+
))
138+
}
139+
})
140+
}
141+
142+
/// A streaming syntax highlighter.
143+
///
144+
/// It can efficiently highlight an immutable stream of tokens.
145+
#[derive(Debug)]
146+
pub struct Stream {
147+
syntax: &'static parsing::SyntaxReference,
148+
highlighter: highlighting::Highlighter<'static>,
149+
commit: (parsing::ParseState, parsing::ScopeStack),
150+
state: parsing::ParseState,
151+
stack: parsing::ScopeStack,
152+
}
153+
154+
impl Stream {
155+
/// Creates a new [`Stream`] highlighter.
156+
pub fn new(settings: &Settings) -> Self {
157+
let syntax = SYNTAXES
158+
.find_syntax_by_token(&settings.token)
159+
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
160+
161+
let highlighter = highlighting::Highlighter::new(
162+
&THEMES.themes[settings.theme.key()],
163+
);
164+
165+
let state = parsing::ParseState::new(syntax);
166+
let stack = parsing::ScopeStack::new();
167+
168+
Self {
169+
syntax,
170+
highlighter,
171+
commit: (state.clone(), stack.clone()),
172+
state,
173+
stack,
174+
}
175+
}
176+
177+
/// Highlights the given line from the last commit.
178+
pub fn highlight_line(
179+
&mut self,
180+
line: &str,
181+
) -> impl Iterator<Item = (Range<usize>, Highlight)> + '_ {
182+
self.state = self.commit.0.clone();
183+
self.stack = self.commit.1.clone();
184+
185+
let ops = self.state.parse_line(line, &SYNTAXES).unwrap_or_default();
186+
scope_iterator(ops, line, &mut self.stack, &self.highlighter)
187+
}
188+
189+
/// Commits the last highlighted line.
190+
pub fn commit(&mut self) {
191+
self.commit = (self.state.clone(), self.stack.clone());
192+
}
193+
194+
/// Resets the [`Stream`] highlighter.
195+
pub fn reset(&mut self) {
196+
self.state = parsing::ParseState::new(self.syntax);
197+
self.stack = parsing::ScopeStack::new();
198+
self.commit = (self.state.clone(), self.stack.clone());
199+
}
200+
}
201+
138202
/// The settings of a [`Highlighter`].
139203
#[derive(Debug, Clone, PartialEq)]
140204
pub struct Settings {

0 commit comments

Comments
 (0)