Skip to content

Commit

Permalink
implement syntax highlighting for preview text
Browse files Browse the repository at this point in the history
  • Loading branch information
cartercanedy committed Sep 20, 2024
1 parent f46d313 commit a87dcf2
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 38 deletions.
5 changes: 5 additions & 0 deletions tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
154 changes: 122 additions & 32 deletions tui/src/floating_text.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
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<String> {
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::<Vec<_>>();

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<String>, 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<Self> {
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;
}
}
Expand Down Expand Up @@ -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<Line> = 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::<Vec<char>>()
.chunks(inner_area.width as usize)
.map(|chunk| chunk.iter().collect())
.collect::<Vec<String>>()
})
.take(inner_area.height as usize)
.map(Line::from)
.collect();
.map(|l| l.into_text().unwrap())
.collect::<Vec<_>>();

// Create list widget
let list = List::new(lines)
Expand Down
7 changes: 1 addition & 6 deletions tui/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,12 +494,7 @@ impl AppState {
}
fn enable_description(&mut self) {
if let Some(command_description) = self.get_selected_description() {
let description_content: Vec<String> = 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);
}
}
Expand Down

0 comments on commit a87dcf2

Please sign in to comment.