diff --git a/Cargo.lock b/Cargo.lock index 6cf1c13..c15aea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.7.4" @@ -92,6 +98,30 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.2.1" @@ -192,6 +222,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.1" @@ -359,6 +398,34 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "fancy-regex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "flate2" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80edafed416a46fb378521624fab1cfa2eb514784fd8921adbe8a8d8321da811" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -522,6 +589,7 @@ dependencies = [ "structopt", "strum", "strum_macros", + "syntect", "tokio", "toml", "tui", @@ -644,6 +712,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lexical-core" version = "0.7.6" @@ -680,6 +754,21 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.4.4" @@ -727,6 +816,16 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + [[package]] name = "mio" version = "0.7.13" @@ -924,6 +1023,20 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "plist" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38d026d73eeaf2ade76309d0c65db5a35ecf649e3cec428db316243ea9d6711" +dependencies = [ + "base64", + "chrono", + "indexmap", + "line-wrap", + "serde", + "xml-rs", +] + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -1131,6 +1244,21 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1480,6 +1608,28 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syntect" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031" +dependencies = [ + "bincode", + "bitflags", + "fancy-regex", + "flate2", + "fnv", + "lazy_static", + "lazycell", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "walkdir", + "yaml-rust", +] + [[package]] name = "tap" version = "1.0.1" @@ -1699,6 +1849,17 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -1825,6 +1986,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1837,6 +2007,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 95f5b91..389f761 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ rust_decimal = "1.15" dirs-next = "2.0" clap = "2.33.3" structopt = "0.3.22" +syntect = { version = "4.5", default-features = false, features = ["metadata", "default-fancy"]} [target.'cfg(all(target_family="unix",not(target_os="macos")))'.dependencies] which = "4.1" diff --git a/src/components/mod.rs b/src/components/mod.rs index 44c6e6d..a95b811 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -6,6 +6,7 @@ pub mod databases; pub mod error; pub mod help; pub mod record_table; +pub mod syntax_text; pub mod tab; pub mod table; pub mod table_filter; diff --git a/src/components/syntax_text.rs b/src/components/syntax_text.rs new file mode 100644 index 0000000..ed57d94 --- /dev/null +++ b/src/components/syntax_text.rs @@ -0,0 +1,206 @@ +use super::{CommandInfo, Component, DrawableComponent, EventState}; +use crate::config::KeyConfig; +use crate::event::Key; +use crate::ui::scrollbar::draw_scrollbar; +use crate::ui::{ + self, common_nav, style::SharedTheme, AsyncSyntaxJob, ParagraphState, ScrollPos, + StatefulParagraph, +}; +use anyhow::Result; +use asyncgit::{ + asyncjob::AsyncSingleJob, + sync::{self, TreeFile}, + AsyncGitNotification, CWD, +}; + +use crossterm::event::Event; +use database_tree::MoveSelection; +use itertools::Either; +use std::{cell::Cell, convert::From, path::Path}; +use tui::{ + backend::Backend, + layout::Rect, + text::Text, + widgets::{Block, Borders, Wrap}, + Frame, +}; + +pub struct SyntaxTextComponent { + current_file: Option<(String, Either)>, + async_highlighting: AsyncSingleJob, + key_config: SharedKeyConfig, + paragraph_state: Cell, + focused: bool, + theme: SharedTheme, +} + +impl SyntaxTextComponent { + /// + pub fn new(key_config: KeyConfig, theme: SharedTheme) -> Self { + Self { + async_highlighting: AsyncSingleJob::new( + sender.clone(), + AsyncGitNotification::SyntaxHighlighting, + ), + current_file: None, + paragraph_state: Cell::new(ParagraphState::default()), + focused: false, + key_config, + theme, + } + } + + /// + pub fn update(&mut self, ev: AsyncGitNotification) { + if ev == AsyncGitNotification::SyntaxHighlighting { + if let Some(job) = self.async_highlighting.take_last() { + if let Some((path, content)) = self.current_file.as_mut() { + if let Some(syntax) = job.result() { + if syntax.path() == Path::new(path) { + *content = Either::Left(syntax); + } + } + } + } + } + } + + /// + pub fn any_work_pending(&self) -> bool { + self.async_highlighting.is_pending() + } + + pub fn clear(&mut self) { + self.current_file = None; + } + + /// + pub fn load_file(&mut self, path: String, item: &TreeFile) { + let already_loaded = self + .current_file + .as_ref() + .map(|(current_file, _)| current_file == &path) + .unwrap_or_default(); + + if !already_loaded { + //TODO: fetch file content async aswell + match sync::tree_file_content(CWD, item) { + Ok(content) => { + self.async_highlighting + .spawn(AsyncSyntaxJob::new(content.clone(), path.clone())); + + self.current_file = Some((path, Either::Right(content))); + } + Err(e) => { + self.current_file = + Some((path, Either::Right(format!("error loading file: {}", e)))); + } + } + } + } + + fn scroll(&self, nav: MoveSelection) -> bool { + let state = self.paragraph_state.get(); + + let new_scroll_pos = match nav { + MoveSelection::Down => state.scroll().y.saturating_add(1), + MoveSelection::Up => state.scroll().y.saturating_sub(1), + MoveSelection::Top => 0, + MoveSelection::End => state + .lines() + .saturating_sub(state.height().saturating_sub(2)), + MoveSelection::PageUp => state + .scroll() + .y + .saturating_sub(state.height().saturating_sub(2)), + MoveSelection::PageDown => state + .scroll() + .y + .saturating_add(state.height().saturating_sub(2)), + _ => state.scroll().y, + }; + + self.set_scroll(new_scroll_pos) + } + + fn set_scroll(&self, pos: u16) -> bool { + let mut state = self.paragraph_state.get(); + + let new_scroll_pos = pos.min( + state + .lines() + .saturating_sub(state.height().saturating_sub(2)), + ); + + if new_scroll_pos == state.scroll().y { + return false; + } + + state.set_scroll(ScrollPos { + x: 0, + y: new_scroll_pos, + }); + self.paragraph_state.set(state); + + true + } +} + +impl DrawableComponent for SyntaxTextComponent { + fn draw(&self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + let text = self.current_file.as_ref().map_or_else( + || Text::from(""), + |(_, content)| match content { + Either::Left(syn) => syn.into(), + Either::Right(s) => Text::from(s.as_str()), + }, + ); + + let content = StatefulParagraph::new(text) + .wrap(Wrap { trim: false }) + .block( + Block::default() + .title( + self.current_file + .as_ref() + .map(|(name, _)| name.clone()) + .unwrap_or_default(), + ) + .borders(Borders::ALL) + .border_style(self.theme.title(self.focused())), + ); + + let mut state = self.paragraph_state.get(); + + f.render_stateful_widget(content, area, &mut state); + + self.paragraph_state.set(state); + + self.set_scroll(state.scroll().y); + + if self.focused() { + draw_scrollbar( + f, + area, + usize::from( + state + .lines() + .saturating_sub(state.height().saturating_sub(2)), + ), + usize::from(state.scroll().y), + false, + false, + ); + } + + Ok(()) + } +} + +impl Component for SyntaxTextComponent { + fn commands(&self, out: &mut Vec) {} + + fn event(&mut self, key: Key) -> Result { + Ok(EventState::NotConsumed) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bc99059..d3d0587 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,6 +4,7 @@ use database_tree::MoveSelection; pub mod scrollbar; pub mod scrolllist; +pub mod syntax_text; pub fn common_nav(key: Key, key_config: &KeyConfig) -> Option { if key == key_config.scroll_down { diff --git a/src/ui/syntax_text.rs b/src/ui/syntax_text.rs new file mode 100644 index 0000000..ee31296 --- /dev/null +++ b/src/ui/syntax_text.rs @@ -0,0 +1,87 @@ +use std::{ops::Range, path::Path}; +use syntect::{ + highlighting::{ + FontStyle, HighlightState, Highlighter, RangedHighlightIterator, Style, ThemeSet, + }, + parsing::{ParseState, ScopeStack, SyntaxSet}, +}; +use tui::text::{Span, Spans}; + +struct SyntaxLine { + items: Vec<(Style, usize, Range)>, +} + +pub struct SyntaxText { + text: String, + lines: Vec, +} + +impl SyntaxText { + pub fn new(text: String) -> Self { + const SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_nonewlines(); + const THEME_SET: ThemeSet = ThemeSet::load_defaults(); + + let mut state = ParseState::new(SYNTAX_SET.find_syntax_by_extension("sql").unwrap()); + 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, + } + } +} + +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 +}