summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock225
-rw-r--r--Cargo.toml5
-rw-r--r--Makefile4
-rw-r--r--async_utils/Cargo.toml21
l---------async_utils/LICENSE.md1
-rw-r--r--async_utils/src/lib.rs200
-rw-r--r--asyncgit/src/lib.rs3
-rw-r--r--src/app.rs2
-rw-r--r--src/components/revision_files.rs98
-rw-r--r--src/ui/mod.rs2
-rw-r--r--src/ui/syntax_text.rs171
11 files changed, 700 insertions, 32 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8931b78c..d47fa05e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
+]
diff --git a/Cargo.toml b/Cargo.toml
index fa826da6..2de78b34 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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]
diff --git a/Makefile b/Makefile
index 2e4ec958..70346187 100644
--- a/Makefile
+++ b/Makefile
@@ -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 `./`
diff --git a/src/app.rs b/src/app.rs
index f6bcf605..81ab77aa 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -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
+