on master help sign in
ui: Syntax highlight code inside markdown popups.
d8599f3Blaž Hrastnik
helix-term/src/ui/markdown.rs+75−16
@@ -10,18 +10,22 @@ use tui::{
10use std::borrow::Cow;
10use std::borrow::Cow;
11
11
12use helix_core::Position;
12use helix_core::Position;
13−use helix_view::Editor;
13+use helix_view::{Editor, Theme};
14
14
15pub struct Markdown {
15pub struct Markdown {
16 contents: String,
16 contents: String,
17}
17}
18
18
19+// TODO: pre-render and self reference via Pin
20+// better yet, just use Tendril + subtendril for references
21+
19impl Markdown {
22impl Markdown {
20 pub fn new(contents: String) -> Self {
23 pub fn new(contents: String) -> Self {
21 Self { contents }
24 Self { contents }
22 }
25 }
23}
26}
27+
24−fn parse(contents: &str) -> tui::text::Text {
28+fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
25 use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
29 use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
26 use tui::text::{Span, Spans, Text};
30 use tui::text::{Span, Spans, Text};
27
31
@@ -68,10 +72,73 @@ fn parse(contents: &str) -> tui::text::Text {
68 }
72 }
69 }
73 }
70 Event::Text(text) => {
74 Event::Text(text) => {
75+ // TODO: temp workaround
71− if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(_))) = tags.last() {
76+ if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
72− for line in text.lines() {
77+ if let Some(theme) = theme {
78+ use helix_core::syntax::{self, HighlightEvent, Syntax};
79+ use helix_core::Rope;
80+
81+ let rope = Rope::from(text.as_ref());
82+ let syntax = syntax::LOADER
83+ .language_config_for_scope(&format!("source.{}", language))
84+ .and_then(|config| config.highlight_config(theme.scopes()))
85+ .map(|config| Syntax::new(&rope, config));
86+
87+ if let Some(mut syntax) = syntax {
88+ // if we have a syntax available, highlight_iter and generate spans
89+ let mut highlights = Vec::new();
90+
91+ for event in syntax.highlight_iter(rope.slice(..), None, None, |_| None)
92+ {
93+ match event.unwrap() {
94+ HighlightEvent::HighlightStart(span) => {
95+ highlights.push(span);
96+ }
97+ HighlightEvent::HighlightEnd => {
98+ highlights.pop();
99+ }
100+ HighlightEvent::Source { start, end } => {
101+ let style = match highlights.first() {
102+ Some(span) => {
103+ theme.get(theme.scopes()[span.0].as_str())
104+ }
105+ None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
106+ };
107+
108+ let mut slice = &text[start..end];
109+ while let Some(end) = slice.find('\n') {
110+ // emit span up to newline
111+ let text = &slice[..end];
112+ let span = Span::styled(text.to_owned(), style);
113+ spans.push(span);
114+
115+ // truncate slice to after newline
116+ slice = &slice[end + 1..];
117+
118+ // make a new line
119+ let spans = std::mem::replace(&mut spans, Vec::new());
120+ lines.push(Spans::from(spans));
121+ }
122+
123+ // if there's anything left, emit it too
124+ if !slice.is_empty() {
125+ let span = Span::styled(slice.to_owned(), style);
126+ spans.push(span);
127+ }
128+ }
129+ }
130+ }
131+ } else {
132+ for line in text.lines() {
73− let mut span = Span::styled(line.to_string(), code_style);
133+ let mut span = Span::styled(line.to_string(), code_style);
134+ lines.push(Spans::from(span));
135+ }
136+ }
137+ } else {
138+ for line in text.lines() {
139+ let mut span = Span::styled(line.to_string(), code_style);
74− lines.push(Spans::from(span));
140+ lines.push(Spans::from(span));
141+ }
75 }
142 }
76 } else if let Some(Tag::Heading(_)) = tags.last() {
143 } else if let Some(Tag::Heading(_)) = tags.last() {
77 let mut span = to_span(text);
144 let mut span = to_span(text);
@@ -113,7 +180,7 @@ impl Component for Markdown {
113 fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
180 fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
114 use tui::widgets::{Paragraph, Widget, Wrap};
181 use tui::widgets::{Paragraph, Widget, Wrap};
115
182
116− let text = parse(&self.contents);
183+ let text = parse(&self.contents, Some(&cx.editor.theme));
117
184
118 let par = Paragraph::new(text)
185 let par = Paragraph::new(text)
119 .wrap(Wrap { trim: false })
186 .wrap(Wrap { trim: false })
@@ -124,18 +191,10 @@ impl Component for Markdown {
124 }
191 }
125
192
126 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
193 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
127− let contents = parse(&self.contents);
194+ let contents = parse(&self.contents, None);
128 let padding = 2;
195 let padding = 2;
129 let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
196 let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
130 let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
197 let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
131 Some((width, height))
198 Some((width, height))
132 }
199 }
133}
200}
134−
135−#[cfg(test)]
136−mod test {
137− use super::*;
138−
139− #[test]
140− fn it_works() {}
141−}
helix-view/src/theme.rs−28
@@ -88,34 +88,6 @@ pub struct Theme {
88 mapping: HashMap<&'static str, Style>,
88 mapping: HashMap<&'static str, Style>,
89}
89}
90
90
91−// let highlight_names: Vec<String> = [
92−// "attribute",
93−// "constant.builtin",
94−// "constant",
95−// "function.builtin",
96−// "function.macro",
97−// "function",
98−// "keyword",
99−// "operator",
100−// "property",
101−// "punctuation",
102−// "comment",
103−// "escape",
104−// "label",
105−// // "punctuation.bracket",
106−// "punctuation.delimiter",
107−// "string",
108−// "string.special",
109−// "tag",
110−// "type",
111−// "type.builtin",
112−// "constructor",
113−// "variable",
114−// "variable.builtin",
115−// "variable.parameter",
116−// "path",
117−// ];
118−
119impl Default for Theme {
91impl Default for Theme {
120 fn default() -> Self {
92 fn default() -> Self {
121 let mapping = hashmap! {
93 let mapping = hashmap! {