summaryrefslogtreecommitdiffstats
path: root/src/ui
diff options
context:
space:
mode:
authorStephan Dilly <dilly.stephan@gmail.com>2021-05-23 02:45:22 +0200
committerGitHub <noreply@github.com>2021-05-23 02:45:22 +0200
commit1034dc1aaffd91efc65d68ee2ff9ef8464663f6e (patch)
tree3fcbb7ceed6bc9580ea4d1026cfd4d6c7aec4ff6 /src/ui
parenta31f18515429b2ad395856e48ce101a9e88bac59 (diff)
add syntax highlighting (#727)
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/mod.rs2
-rw-r--r--src/ui/syntax_text.rs171
2 files changed, 173 insertions, 0 deletions
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<usize>)>,
+}
+
+//TODO: no clone, make user consume result
+#[derive(Clone)]
+pub struct SyntaxText {
+ text: String,
+ lines: Vec<SyntaxLine>,
+ 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<SyntaxLine> = 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<Spans> =
+ 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<Option<SyntaxText>>,
+}
+
+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));
+ }
+ }
+}