From 1034dc1aaffd91efc65d68ee2ff9ef8464663f6e Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Sun, 23 May 2021 02:45:22 +0200 Subject: add syntax highlighting (#727) --- src/ui/mod.rs | 2 + src/ui/syntax_text.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 src/ui/syntax_text.rs (limited to 'src/ui') diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1131c7fc..4734f8f3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,9 +1,11 @@ mod scrollbar; mod scrolllist; pub mod style; +mod syntax_text; pub use scrollbar::draw_scrollbar; pub use scrolllist::{draw_list, draw_list_block}; +pub use syntax_text::{AsyncSyntaxJob, SyntaxText}; use tui::layout::{Constraint, Direction, Layout, Rect}; /// return the scroll position (line) necessary to have the `selection` in view if it is not already diff --git a/src/ui/syntax_text.rs b/src/ui/syntax_text.rs new file mode 100644 index 00000000..44a0efbf --- /dev/null +++ b/src/ui/syntax_text.rs @@ -0,0 +1,171 @@ +use async_utils::AsyncJob; +use lazy_static::lazy_static; +use scopetime::scope_time; +use std::{ + ffi::OsStr, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, +}; +use syntect::{ + highlighting::{ + FontStyle, HighlightState, Highlighter, + RangedHighlightIterator, Style, ThemeSet, + }, + parsing::{ParseState, ScopeStack, SyntaxSet}, +}; +use tui::text::{Span, Spans}; + +//TODO: no clone, make user consume result +#[derive(Clone)] +struct SyntaxLine { + items: Vec<(Style, usize, Range)>, +} + +//TODO: no clone, make user consume result +#[derive(Clone)] +pub struct SyntaxText { + text: String, + lines: Vec, + path: PathBuf, +} + +lazy_static! { + static ref SYNTAX_SET: SyntaxSet = + SyntaxSet::load_defaults_nonewlines(); + static ref THEME_SET: ThemeSet = ThemeSet::load_defaults(); +} + +impl SyntaxText { + pub fn new(text: String, file_path: &Path) -> Self { + scope_time!("syntax_highlighting"); + log::debug!("syntax: {:?}", file_path); + + let mut state = { + let syntax = file_path + .extension() + .and_then(OsStr::to_str) + .map_or_else( + || { + SYNTAX_SET.find_syntax_by_path( + file_path.to_str().unwrap_or_default(), + ) + }, + |ext| SYNTAX_SET.find_syntax_by_extension(ext), + ); + + ParseState::new(syntax.unwrap_or_else(|| { + SYNTAX_SET.find_syntax_plain_text() + })) + }; + + let highlighter = Highlighter::new( + &THEME_SET.themes["base16-eighties.dark"], + ); + + let mut syntax_lines: Vec = Vec::new(); + + let mut highlight_state = + HighlightState::new(&highlighter, ScopeStack::new()); + + for (number, line) in text.lines().enumerate() { + let ops = state.parse_line(line, &SYNTAX_SET); + let iter = RangedHighlightIterator::new( + &mut highlight_state, + &ops[..], + line, + &highlighter, + ); + + syntax_lines.push(SyntaxLine { + items: iter + .map(|(style, _, range)| (style, number, range)) + .collect(), + }); + } + + Self { + text, + lines: syntax_lines, + path: file_path.into(), + } + } + + /// + pub fn path(&self) -> &Path { + &self.path + } +} + +impl<'a> From<&'a SyntaxText> for tui::text::Text<'a> { + fn from(v: &'a SyntaxText) -> Self { + let mut result_lines: Vec = + Vec::with_capacity(v.lines.len()); + + for (syntax_line, line_content) in + v.lines.iter().zip(v.text.lines()) + { + let mut line_span = + Spans(Vec::with_capacity(syntax_line.items.len())); + + for (style, _, range) in &syntax_line.items { + let item_content = &line_content[range.clone()]; + let item_style = syntact_style_to_tui(style); + + line_span + .0 + .push(Span::styled(item_content, item_style)); + } + + result_lines.push(line_span); + } + + result_lines.into() + } +} + +fn syntact_style_to_tui(style: &Style) -> tui::style::Style { + let mut res = + tui::style::Style::default().fg(tui::style::Color::Rgb( + style.foreground.r, + style.foreground.g, + style.foreground.b, + )); + + if style.font_style.contains(FontStyle::BOLD) { + res = res.add_modifier(tui::style::Modifier::BOLD); + } + if style.font_style.contains(FontStyle::ITALIC) { + res = res.add_modifier(tui::style::Modifier::ITALIC); + } + if style.font_style.contains(FontStyle::UNDERLINE) { + res = res.add_modifier(tui::style::Modifier::UNDERLINED); + } + + res +} + +#[derive(Clone, Default)] +pub struct AsyncSyntaxJob { + //TODO: can we merge input and text into a single enum to represent the state transition? + pub input: Option<(String, String)>, + pub text: Arc>, +} + +impl AsyncSyntaxJob { + pub fn new(content: String, path: String) -> Self { + Self { + input: Some((content, path)), + text: Arc::new(None), + } + } +} + +impl AsyncJob for AsyncSyntaxJob { + fn run(&mut self) { + if let Some((text, path)) = self.input.take() { + let syntax = SyntaxText::new(text, Path::new(&path)); + self.text = Arc::new(Some(syntax)); + } + } +} -- cgit v1.2.3