Skip to content

Commit

Permalink
Merge pull request #2776 from iced-rs/fix/markdown
Browse files Browse the repository at this point in the history
Incremental `markdown` parsing and various fixes
  • Loading branch information
hecrj authored Feb 1, 2025
2 parents 30ee9d0 + ed0ffb5 commit 91f94f3
Show file tree
Hide file tree
Showing 11 changed files with 548 additions and 169 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/markdown/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ publish = false

[dependencies]
iced.workspace = true
iced.features = ["markdown", "highlighter", "debug"]
iced.features = ["markdown", "highlighter", "tokio", "debug"]

open = "5.3"
111 changes: 99 additions & 12 deletions examples/markdown/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
use iced::highlighter;
use iced::widget::{self, markdown, row, scrollable, text_editor};
use iced::{Element, Fill, Font, Task, Theme};
use iced::time::{self, milliseconds};
use iced::widget::{
self, hover, markdown, right, row, scrollable, text_editor, toggler,
};
use iced::{Element, Fill, Font, Subscription, Task, Theme};

pub fn main() -> iced::Result {
iced::application("Markdown - Iced", Markdown::update, Markdown::view)
.subscription(Markdown::subscription)
.theme(Markdown::theme)
.run_with(Markdown::new)
}

struct Markdown {
content: text_editor::Content,
items: Vec<markdown::Item>,
mode: Mode,
theme: Theme,
}

enum Mode {
Preview(Vec<markdown::Item>),
Stream {
pending: String,
parsed: markdown::Content,
},
}

#[derive(Debug, Clone)]
enum Message {
Edit(text_editor::Action),
LinkClicked(markdown::Url),
ToggleStream(bool),
NextToken,
}

impl Markdown {
Expand All @@ -29,27 +43,71 @@ impl Markdown {
(
Self {
content: text_editor::Content::with_text(INITIAL_CONTENT),
items: 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.items =
markdown::parse(&self.content.text()).collect();
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) => {
if enable_stream {
self.mode = Mode::Stream {
pending: self.content.text(),
parsed: markdown::Content::new(),
};

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

Task::none()
}
}
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(" ");
}
}
}

Task::none()
}
}
}
Expand All @@ -63,20 +121,49 @@ impl Markdown {
.font(Font::MONOSPACE)
.highlight("markdown", highlighter::Theme::Base16Ocean);

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

let preview = markdown(
&self.items,
items,
markdown::Settings::default(),
markdown::Style::from_palette(self.theme.palette()),
)
.map(Message::LinkClicked);

row![editor, scrollable(preview).spacing(10).height(Fill)]
.spacing(10)
.padding(10)
.into()
row![
editor,
hover(
scrollable(preview)
.spacing(10)
.width(Fill)
.height(Fill)
.id("preview"),
right(
toggler(matches!(self.mode, Mode::Stream { .. }))
.label("Stream")
.on_toggle(Message::ToggleStream)
)
.padding([0, 20])
)
]
.spacing(10)
.padding(10)
.into()
}

fn theme(&self) -> Theme {
self.theme.clone()
}

fn subscription(&self) -> Subscription<Message> {
match self.mode {
Mode::Preview(_) => Subscription::none(),
Mode::Stream { .. } => {
time::every(milliseconds(20)).map(|_| Message::NextToken)
}
}
}
}
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
1 change: 1 addition & 0 deletions widget/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ iced_renderer.workspace = true
iced_runtime.workspace = true

num-traits.workspace = true
log.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
Expand Down
14 changes: 11 additions & 3 deletions widget/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ where
}

/// Sets the [`Id`] of the [`Container`].
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
pub fn id(mut self, id: impl Into<Id>) -> Self {
self.id = Some(id.into());
self
}

Expand Down Expand Up @@ -480,9 +480,17 @@ impl From<Id> for widget::Id {
}
}

impl From<&'static str> for Id {
fn from(value: &'static str) -> Self {
Id::new(value)
}
}

/// Produces a [`Task`] that queries the visible screen bounds of the
/// [`Container`] with the given [`Id`].
pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
pub fn visible_bounds(id: impl Into<Id>) -> Task<Option<Rectangle>> {
let id = id.into();

struct VisibleBounds {
target: widget::Id,
depth: usize,
Expand Down
Loading

0 comments on commit 91f94f3

Please sign in to comment.