diff options
-rw-r--r-- | Cargo.lock | 225 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | async_utils/Cargo.toml | 21 | ||||
l--------- | async_utils/LICENSE.md | 1 | ||||
-rw-r--r-- | async_utils/src/lib.rs | 200 | ||||
-rw-r--r-- | asyncgit/src/lib.rs | 3 | ||||
-rw-r--r-- | src/app.rs | 2 | ||||
-rw-r--r-- | src/components/revision_files.rs | 98 | ||||
-rw-r--r-- | src/ui/mod.rs | 2 | ||||
-rw-r--r-- | src/ui/syntax_text.rs | 171 |
11 files changed, 700 insertions, 32 deletions
@@ -27,6 +27,15 @@ dependencies = [ ] [[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -51,6 +60,17 @@ dependencies = [ ] [[package]] +name = "async_utils" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", + "log", + "pretty_assertions", + "rayon-core", + "thiserror", +] + +[[package]] name = "asyncgit" version = "0.15.0" dependencies = [ @@ -108,6 +128,30 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -192,6 +236,15 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -321,6 +374,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] +name = "fancy-regex" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae91abf6555234338687bb47913978d275539235fcb77ba9863b779090b42b14" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] name = "filetree" version = "0.1.0" dependencies = [ @@ -329,6 +392,24 @@ dependencies = [ ] [[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -397,6 +478,7 @@ name = "gitui" version = "0.15.0" dependencies = [ "anyhow", + "async_utils", "asyncgit", "backtrace", "bitflags", @@ -410,6 +492,7 @@ dependencies = [ "easy-cast", "filetree", "itertools", + "lazy_static", "log", "pprof", "rayon-core", @@ -418,6 +501,7 @@ dependencies = [ "scopetime", "serde", "simplelog", + "syntect", "textwrap 0.13.4", "tui", "unicode-truncate", @@ -529,6 +613,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] name = "libc" version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -575,6 +665,21 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -809,6 +914,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] +name = "plist" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679104537029ed2287c216bfb942bbf723f48ee98f0aef15611634173a74ef21" +dependencies = [ + "base64", + "chrono", + "indexmap", + "line-wrap", + "serde", + "xml-rs", +] + +[[package]] name = "pprof" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -950,6 +1069,23 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -985,6 +1121,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce" [[package]] +name = "ryu" +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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1018,6 +1175,17 @@ dependencies = [ ] [[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] name = "serial_test" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1134,6 +1302,28 @@ dependencies = [ ] [[package]] +name = "syntect" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfac2b23b4d049dc9a89353b4e06bbc85a8f42020cccbe5409a115cf19031e5" +dependencies = [ + "bincode", + "bitflags", + "fancy-regex", + "flate2", + "fnv", + "lazy_static", + "lazycell", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "walkdir", + "yaml-rust", +] + +[[package]] name = "sys-info" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1311,6 +1501,17 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1343,7 +1544,31 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xml-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] @@ -22,6 +22,7 @@ keywords = [ scopetime = { path = "./scopetime", version = "0.1" } asyncgit = { path = "./asyncgit", version = "0.15" } filetree = { path = "./filetree" } +async_utils = { path = "./async_utils" } crossterm = { version = "0.19", features = [ "serde" ] } clap = { version = "2.33", default-features = false } tui = { version = "0.15", default-features = false, features = ['crossterm', 'serde'] } @@ -44,6 +45,8 @@ textwrap = "0.13" unicode-truncate = "0.2" easy-cast = "0.4" bugreport = "0.4" +lazy_static = "1.4" +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" @@ -63,6 +66,8 @@ timing=["scopetime/enabled"] members=[ "asyncgit", "scopetime", + "async_utils", + "filetree", ] [profile.release] @@ -45,12 +45,12 @@ fmt: clippy: touch src/main.rs - cargo clean -p gitui -p asyncgit -p scopetime -p filetree + cargo clean -p gitui -p asyncgit -p scopetime -p filetree -p async_utils cargo clippy --workspace --all-features clippy-nightly: touch src/main.rs - cargo clean -p gitui -p asyncgit -p scopetime -p filetree + cargo clean -p gitui -p asyncgit -p scopetime -p filetree -p async_utils cargo +nightly clippy --all-features check: fmt clippy test diff --git a/async_utils/Cargo.toml b/async_utils/Cargo.toml new file mode 100644 index 00000000..b4b22cbb --- /dev/null +++ b/async_utils/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "async_utils" +version = "0.1.0" +authors = ["Stephan Dilly <dilly.stephan@gmail.com>"] +edition = "2018" +description = "async job utils" +homepage = "https://github.com/extrawurst/gitui" +repository = "https://github.com/extrawurst/gitui" +readme = "README.md" +license-file = "LICENSE.md" +categories = ["asynchronous","concurrency"] +keywords = ["parallel", "thread", "concurrency", "performance"] + +[dependencies] +rayon-core = "1.9" +crossbeam-channel = "0.5" +log = "0.4" +thiserror = "1.0" + +[dev-dependencies] +pretty_assertions = "0.7"
\ No newline at end of file diff --git a/async_utils/LICENSE.md b/async_utils/LICENSE.md new file mode 120000 index 00000000..7eabdb1c --- /dev/null +++ b/async_utils/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md
\ No newline at end of file diff --git a/async_utils/src/lib.rs b/async_utils/src/lib.rs new file mode 100644 index 00000000..c17ff8bf --- /dev/null +++ b/async_utils/src/lib.rs @@ -0,0 +1,200 @@ +use crossbeam_channel::Sender; +use std::sync::{Arc, Mutex}; + +pub trait AsyncJob: Send + Sync + Clone { + fn run(&mut self); +} + +#[derive(Debug, Clone)] +pub struct AsyncSingleJob<J: AsyncJob, T: Copy + Send + 'static> { + next: Arc<Mutex<Option<J>>>, + last: Arc<Mutex<Option<J>>>, + sender: Sender<T>, + pending: Arc<Mutex<()>>, + notification: T, +} + +impl<J: 'static + AsyncJob, T: Copy + Send + 'static> + AsyncSingleJob<J, T> +{ + /// + pub fn new(sender: Sender<T>, value: T) -> Self { + Self { + next: Arc::new(Mutex::new(None)), + last: Arc::new(Mutex::new(None)), + pending: Arc::new(Mutex::new(())), + notification: value, + sender, + } + } + + /// + pub fn is_pending(&self) -> bool { + self.pending.try_lock().is_err() + } + + /// makes sure `next` is cleared and returns `true` if it actually canceled something + pub fn cancel(&mut self) -> bool { + if let Ok(mut next) = self.next.lock() { + if next.is_some() { + *next = None; + return true; + } + } + + false + } + + /// return clone of last result + pub fn get_last(&self) -> Option<J> { + if let Ok(last) = self.last.lock() { + last.clone() + } else { + None + } + } + + /// + pub fn spawn(&mut self, task: J) -> bool { + self.schedule_next(task); + self.check_for_job() + } + + /// + pub fn check_for_job(&self) -> bool { + if self.is_pending() { + return false; + } + + if let Some(task) = self.take_next() { + let self_arc = self.clone(); + + rayon_core::spawn(move || { + self_arc.run_job(task); + }); + + return true; + } + + false + } + + //TODO: return Result + fn run_job(&self, mut task: J) { + //limit the pending scope + { + let _pending = self.pending.lock().expect(""); + + task.run(); + + if let Ok(mut last) = self.last.lock() { + *last = Some(task); + } + + self.sender.send(self.notification).expect("send failed"); + } + + self.check_for_job(); + } + + /// + fn schedule_next(&mut self, task: J) { + if let Ok(mut next) = self.next.lock() { + *next = Some(task); + } + } + + /// + fn take_next(&self) -> Option<J> { + if let Ok(mut next) = self.next.lock() { + next.take() + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crossbeam_channel::unbounded; + use pretty_assertions::assert_eq; + use std::{ + sync::atomic::AtomicU32, thread::sleep, time::Duration, + }; + + #[derive(Clone)] + struct TestJob { + v: Arc<AtomicU32>, + value_to_add: u32, + } + + impl AsyncJob for TestJob { + fn run(&mut self) { + sleep(Duration::from_millis(100)); + + self.v.fetch_add( + self.value_to_add, + std::sync::atomic::Ordering::Relaxed, + ); + } + } + + type Notificaton = (); + + #[test] + fn test_overwrite() { + let (sender, receiver) = unbounded(); + + let mut job: AsyncSingleJob<TestJob, Notificaton> = + AsyncSingleJob::new(sender, ()); + + let task = TestJob { + v: Arc::new(AtomicU32::new(1)), + value_to_add: 1, + }; + + assert!(job.spawn(task.clone())); + sleep(Duration::from_millis(1)); + for _ in 0..5 { + assert!(!job.spawn(task.clone())); + } + + let _foo = receiver.recv().unwrap(); + let _foo = receiver.recv().unwrap(); + assert!(receiver.is_empty()); + + assert_eq!( + task.v.load(std::sync::atomic::Ordering::Relaxed), + 3 + ); + } + + #[test] + fn test_cancel() { + let (sender, receiver) = unbounded(); + + let mut job: AsyncSingleJob<TestJob, Notificaton> = + AsyncSingleJob::new(sender, ()); + + let task = TestJob { + v: Arc::new(AtomicU32::new(1)), + value_to_add: 1, + }; + + assert!(job.spawn(task.clone())); + sleep(Duration::from_millis(1)); + + for _ in 0..5 { + assert!(!job.spawn(task.clone())); + } + assert!(job.cancel()); + + let _foo = receiver.recv().unwrap(); + + assert_eq!( + task.v.load(std::sync::atomic::Ordering::Relaxed), + 2 + ); + } +} diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index bfab69b7..f3f7e76e 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -77,6 +77,9 @@ pub enum AsyncNotification { Fetch, /// Blame, + /// + //TODO: this does not belong here + SyntaxHighlighting, } /// current working directory `./` @@ -338,6 +338,7 @@ impl App { self.push_popup.update_git(ev)?; self.push_tags_popup.update_git(ev)?; self.pull_popup.update_git(ev)?; + self.revision_files_popup.update(ev); //TODO: better system for this // can we simply process the queue here and everyone just uses the queue to schedule a cmd update? @@ -362,6 +363,7 @@ impl App { || self.push_popup.any_work_pending() || self.push_tags_popup.any_work_pending() || self.pull_popup.any_work_pending() + || self.revision_files_popup.any_work_pending() } /// diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index 6a5cf11b..7a1af845 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -1,7 +1,3 @@ -use std::{ - cell::Cell, collections::BTreeSet, convert::From, path::Path, -}; - use super::{ visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, EventState, @@ -10,9 +6,10 @@ use crate::{ keys::SharedKeyConfig, queue::{InternalEvent, Queue}, strings::{self, order}, - ui::{self, style::SharedTheme}, + ui::{self, style::SharedTheme, AsyncSyntaxJob}, }; use anyhow::Result; +use async_utils::AsyncSingleJob; use asyncgit::{ sync::{self, CommitId, TreeFile}, AsyncNotification, CWD, @@ -20,6 +17,10 @@ use asyncgit::{ use crossbeam_channel::Sender; use crossterm::event::Event; use filetree::{FileTree, MoveSelection}; +use itertools::Either; +use std::{ + cell::Cell, collections::BTreeSet, convert::From, path::Path, +}; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -36,8 +37,11 @@ pub struct RevisionFilesComponent { queue: Queue, title: String, theme: SharedTheme, + //TODO: store TreeFiles in `tree` files: Vec<TreeFile>, - current_file: Option<(String, String)>, + current_file: Option<(String, Either<ui::SyntaxText, String>)>, + async_highlighting: + AsyncSingleJob<AsyncSyntaxJob, AsyncNotification>, tree: FileTree, scroll_top: Cell<usize>, revision: Option<CommitId>, @@ -49,7 +53,7 @@ impl RevisionFilesComponent { /// pub fn new( queue: &Queue, - _sender: &Sender<AsyncNotification>, + sender: &Sender<AsyncNotification>, theme: SharedTheme, key_config: SharedKeyConfig, ) -> Self { @@ -57,6 +61,10 @@ impl RevisionFilesComponent { queue: queue.clone(), title: String::new(), tree: FileTree::default(), + async_highlighting: AsyncSingleJob::new( + sender.clone(), + AsyncNotification::SyntaxHighlighting, + ), theme, scroll_top: Cell::new(0), current_file: None, @@ -89,6 +97,28 @@ impl RevisionFilesComponent { Ok(()) } + /// + pub fn update(&mut self, ev: AsyncNotification) { + if ev == AsyncNotification::SyntaxHighlighting { + if let Some(job) = self.async_highlighting.get_last() { + if let Some((path, content)) = + self.current_file.as_mut() + { + if let Some(syntax) = (*job.text).clone() { + if syntax.path() == Path::new(path) { + *content = Either::Left(syntax); + } + } + } + } + } + } + + /// + pub fn any_work_pending(&self) -> bool { + self.async_highlighting.is_pending() + } + fn tree_item_to_span<'a>( item: &'a filetree::FileTreeItem, theme: &SharedTheme, @@ -133,6 +163,7 @@ impl RevisionFilesComponent { } fn selection_changed(&mut self) { + //TODO: retrieve TreeFile from tree datastructure if let Some(file) = self.tree.selected_file().map(|file| { file.full_path() .strip_prefix("./") @@ -154,19 +185,30 @@ impl RevisionFilesComponent { } fn load_file(&mut self, path: String) { - if let Some(item) = self - .files - .iter() - .find(|f| f.path.ends_with(Path::new(&path))) + let path_path = Path::new(&path); + if let Some(item) = + self.files.iter().find(|f| f.path.ends_with(path_path)) { + //TODO: fetch file content async aswell match sync::tree_file_content(CWD, item) { Ok(content) => { - self.current_file = Some((path, 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, - format!("error loading file: {}", e), + Either::Right(format!( + "error loading file: {}", + e + )), )) } } @@ -239,12 +281,15 @@ impl DrawableComponent for RevisionFilesComponent { items, ); - let content = Paragraph::new(Text::from( - self.current_file - .as_ref() - .map(|(_, content)| content.as_str()) - .unwrap_or_default(), - )) + let content = Paragraph::new( + 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()), + }, + ), + ) .wrap(Wrap { trim: false }); f.render_widget(content, chunks[1]); } @@ -290,15 +335,11 @@ impl Component for RevisionFilesComponent { ) -> Result<EventState> { if self.is_visible() { if let Event::Key(key) = event { - let consumed = if key == self.key_config.exit_popup { + if key == self.key_config.exit_popup { self.hide(); - true } else if key == self.key_config.blame { if self.blame() { self.hide(); - true - } else { - false } } else if tree_nav( &mut self.tree, @@ -306,13 +347,10 @@ impl Component for RevisionFilesComponent { key, ) { self.selection_changed(); - true - } else { - false - }; - - return Ok(consumed.into()); + } } + + return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) 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 + |