From a87dcf2ec860f1ff1823a2a540bf633fa11ee71b Mon Sep 17 00:00:00 2001 From: Carter Canedy Date: Fri, 20 Sep 2024 10:27:08 -0700 Subject: [PATCH] implement syntax highlighting for preview text --- tui/Cargo.toml | 5 ++ tui/src/floating_text.rs | 154 +++++++++++++++++++++++++++++++-------- tui/src/state.rs | 7 +- 3 files changed, 128 insertions(+), 38 deletions(-) diff --git a/tui/Cargo.toml b/tui/Cargo.toml index b6e57edcd..65dd2488d 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -30,6 +30,11 @@ tui-term = "0.1.12" unicode-width = "0.1.13" rand = "0.8.5" linutil_core = { path = "../core", version = "24.9.19" } +tree-sitter-highlight = "0.23.0" +tree-sitter-bash = "0.23.1" +anstyle = "1.0.8" +ansi-to-tui = "6.0.0" +zips = "0.1.7" [build-dependencies] chrono = "0.4.33" diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs index 878066f79..30d6e5b12 100644 --- a/tui/src/floating_text.rs +++ b/tui/src/floating_text.rs @@ -1,56 +1,159 @@ +use std::io::{ + Cursor, + Write as _, + Read as _, + Seek, + SeekFrom, +}; + use crate::{ float::FloatContent, hint::{Shortcut, ShortcutList}, }; -use crossterm::event::{KeyCode, KeyEvent}; + use linutil_core::Command; + +use crossterm::event::{KeyCode, KeyEvent}; + use ratatui::{ layout::Rect, style::{Style, Stylize}, - text::Line, widgets::{Block, Borders, Clear, List}, Frame, }; + +use ansi_to_tui::IntoText; + +use tree_sitter_highlight::{self as hl, HighlightEvent}; +use tree_sitter_bash as hl_bash; +use zips::zip_result; + pub enum FloatingTextMode { Preview, Description, } + pub struct FloatingText { - text: Vec, - mode: FloatingTextMode, + src: String, scroll: usize, + n_lines: usize, + mode: FloatingTextMode, +} + +macro_rules! style { + ($r:literal, $g:literal, $b:literal) => ({ + use anstyle::{Color, Style, RgbColor}; + Style::new().fg_color(Some(Color::Rgb(RgbColor($r, $g, $b)))) + }) +} + +const SYNTAX_HIGHLIGHT_STYLES: [(&'static str, anstyle::Style); 8] = [ + ("function", style!(220, 220, 170)), // yellow + ("string", style!(206, 145, 120)), // brown + ("property", style!(156, 220, 254)), // light blue + ("comment", style!(92, 131, 75)), // green + ("embedded", style!(206, 145, 120)), // blue (string expansions) + ("constant", style!(79, 193, 255)), // dark blue + ("keyword", style!(197, 134, 192)), // magenta + ("number", style!(181, 206, 168)) // light green +]; + +fn get_highlighted_string<'a>(s: &'a str) -> Option { + let mut hl_conf = hl::HighlightConfiguration::new( + hl_bash::LANGUAGE.into(), + "bash", + hl_bash::HIGHLIGHT_QUERY, + "", + "" + ).ok()?; + + let matched_tokens = &SYNTAX_HIGHLIGHT_STYLES + .iter() + .map(|hl| hl.0) + .collect::>(); + + hl_conf.configure(matched_tokens); + + let mut hl = hl::Highlighter::new(); + + let mut style_stack = vec![anstyle::Style::new()]; + let src = s.as_bytes(); + + let events = hl.highlight(&hl_conf, src, None, |_| None).ok()?; + + let mut buf = Cursor::new(vec![]); + + for event in events { + match event.unwrap() { + HighlightEvent::HighlightStart(h) => { + style_stack.push(SYNTAX_HIGHLIGHT_STYLES.get(h.0)?.1); + } + + HighlightEvent::HighlightEnd => { + style_stack.pop(); + } + + HighlightEvent::Source { start, end } => { + let style = style_stack.last()?; + zip_result!( + write!(&mut buf, "{}", style), + buf.write_all(&src[start..end]), + write!(&mut buf, "{style:#}"), + )?; + } + } + } + + let mut output = String::new(); + + zip_result!( + buf.seek(SeekFrom::Start(0)), + buf.read_to_string(&mut output), + )?; + + Some(output) } impl FloatingText { - pub fn new(text: Vec, mode: FloatingTextMode) -> Self { + pub fn new(text: String, mode: FloatingTextMode) -> Self { + let mut n_lines = 0; + + text.split("\n") + .for_each(|_| n_lines += 1); + Self { - text, + src: text, scroll: 0, + n_lines, mode, } } pub fn from_command(command: &Command, mode: FloatingTextMode) -> Option { - let lines = match command { + let src = match command { Command::Raw(cmd) => { - // Reconstruct the line breaks and file formatting after the - // 'include_str!()' call in the node - cmd.lines().map(|line| line.to_string()).collect() + // just apply highlights directly + get_highlighted_string(cmd) } + Command::LocalFile(file_path) => { + // have to read from tmp dir to get cmd src let file_contents = std::fs::read_to_string(file_path) .map_err(|_| format!("File not found: {:?}", file_path)) .unwrap(); - file_contents.lines().map(|line| line.to_string()).collect() + + get_highlighted_string(&file_contents) } + // If command is a folder, we don't display a preview - Command::None => return None, + Command::None => None }; - Some(Self::new(lines, mode)) + + Some(Self::new(src?, mode)) } fn scroll_down(&mut self) { - if self.scroll + 1 < self.text.len() { + if self.scroll + 1 < self.n_lines { self.scroll += 1; } } @@ -82,25 +185,12 @@ impl FloatContent for FloatingText { // Calculate the inner area to ensure text is not drawn over the border let inner_area = block.inner(area); - - // Create the list of lines to be displayed - let lines: Vec = self - .text - .iter() + let lines = self + .src + .lines() .skip(self.scroll) - .flat_map(|line| { - if line.is_empty() { - return vec![String::new()]; - } - line.chars() - .collect::>() - .chunks(inner_area.width as usize) - .map(|chunk| chunk.iter().collect()) - .collect::>() - }) - .take(inner_area.height as usize) - .map(Line::from) - .collect(); + .map(|l| l.into_text().unwrap()) + .collect::>(); // Create list widget let list = List::new(lines) diff --git a/tui/src/state.rs b/tui/src/state.rs index 2a8297470..1221a517f 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -494,12 +494,7 @@ impl AppState { } fn enable_description(&mut self) { if let Some(command_description) = self.get_selected_description() { - let description_content: Vec = vec![] - .into_iter() - .chain(command_description.lines().map(|line| line.to_string())) // New line when \n is given in toml - .collect(); - - let description = FloatingText::new(description_content, FloatingTextMode::Description); + let description = FloatingText::new(command_description, FloatingTextMode::Description); self.spawn_float(description, 80, 80); } }