mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 02:12:58 +03:00
add syntax highlighting (#727)
This commit is contained in:
parent
a31f185154
commit
1034dc1aaf
225
Cargo.lock
generated
225
Cargo.lock
generated
@ -26,6 +26,15 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -50,6 +59,17 @@ dependencies = [
|
||||
"nodrop",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rayon-core",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asyncgit"
|
||||
version = "0.15.0"
|
||||
@ -107,6 +127,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"
|
||||
@ -191,6 +235,15 @@ dependencies = [
|
||||
"glob",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -320,6 +373,16 @@ version = "1.6.1"
|
||||
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"
|
||||
@ -328,6 +391,24 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -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",
|
||||
@ -528,6 +612,12 @@ version = "1.4.0"
|
||||
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"
|
||||
@ -574,6 +664,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"
|
||||
@ -808,6 +913,20 @@ version = "0.3.19"
|
||||
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"
|
||||
@ -949,6 +1068,23 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -984,6 +1120,27 @@ version = "0.1.19"
|
||||
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"
|
||||
@ -1017,6 +1174,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -1133,6 +1301,28 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -1310,6 +1500,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"
|
||||
@ -1342,8 +1543,32 @@ 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"
|
||||
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]
|
||||
|
4
Makefile
4
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
|
||||
|
21
async_utils/Cargo.toml
Normal file
21
async_utils/Cargo.toml
Normal file
@ -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"
|
1
async_utils/LICENSE.md
Symbolic link
1
async_utils/LICENSE.md
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE.md
|
200
async_utils/src/lib.rs
Normal file
200
async_utils/src/lib.rs
Normal file
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
171
src/ui/syntax_text.rs
Normal file
171
src/ui/syntax_text.rs
Normal file
@ -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
|
||||
.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
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AsyncSyntaxJob {
|
||||
//TODO: can we merge input and text into a single enum to represent the state transition?
|
||||
pub input: Option<(String, String)>,
|
||||
pub text: Arc<Option<SyntaxText>>,
|
||||
}
|
||||
|
||||
impl AsyncSyntaxJob {
|
||||
pub fn new(content: String, path: String) -> Self {
|
||||
Self {
|
||||
input: Some((content, path)),
|
||||
text: Arc::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncJob for AsyncSyntaxJob {
|
||||
fn run(&mut self) {
|
||||
if let Some((text, path)) = self.input.take() {
|
||||
let syntax = SyntaxText::new(text, Path::new(&path));
|
||||
self.text = Arc::new(Some(syntax));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user