add syntax highlighting (#727)

This commit is contained in:
Stephan Dilly 2021-05-23 02:45:22 +02:00 committed by GitHub
parent a31f185154
commit 1034dc1aaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 700 additions and 32 deletions

225
Cargo.lock generated
View File

@ -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",
]

View File

@ -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]

View File

@ -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
View 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
View File

@ -0,0 +1 @@
../LICENSE.md

200
async_utils/src/lib.rs Normal file
View 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
);
}
}

View File

@ -77,6 +77,9 @@ pub enum AsyncNotification {
Fetch,
///
Blame,
///
//TODO: this does not belong here
SyntaxHighlighting,
}
/// current working directory `./`

View File

@ -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()
}
///

View File

@ -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)

View File

@ -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
View 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));
}
}
}