This commit is contained in:
Conrad Ludgate 2023-04-09 09:44:31 +01:00
parent e1745c7dae
commit f6ee8fcd2e
No known key found for this signature in database
GPG Key ID: 197E3CACA1C980B5
11 changed files with 252 additions and 120 deletions

10
Cargo.lock generated
View File

@ -82,6 +82,7 @@ dependencies = [
"atuin-client", "atuin-client",
"atuin-common", "atuin-common",
"atuin-server", "atuin-server",
"atuin-syntect",
"base64 0.20.0", "base64 0.20.0",
"bitflags", "bitflags",
"cassowary", "cassowary",
@ -105,7 +106,6 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"syntect",
"tiny-bip39", "tiny-bip39",
"tokio", "tokio",
"tracing-subscriber", "tracing-subscriber",
@ -189,6 +189,14 @@ dependencies = [
"whoami", "whoami",
] ]
[[package]]
name = "atuin-syntect"
version = "14.0.0"
dependencies = [
"once_cell",
"syntect",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"

View File

@ -32,7 +32,7 @@ buildflags = ["--release"]
atuin = { path = "/usr/bin/atuin" } atuin = { path = "/usr/bin/atuin" }
[workspace] [workspace]
members = ["./atuin-client", "./atuin-server", "./atuin-common"] members = ["./atuin-client", "./atuin-server", "./atuin-common", "./atuin-syntect"]
[features] [features]
# TODO(conradludgate) # TODO(conradludgate)
@ -47,6 +47,7 @@ server = ["atuin-server", "tracing-subscriber"]
atuin-server = { path = "atuin-server", version = "14.0.0", optional = true } atuin-server = { path = "atuin-server", version = "14.0.0", optional = true }
atuin-client = { path = "atuin-client", version = "14.0.0", optional = true, default-features = false } atuin-client = { path = "atuin-client", version = "14.0.0", optional = true, default-features = false }
atuin-common = { path = "atuin-common", version = "14.0.0" } atuin-common = { path = "atuin-common", version = "14.0.0" }
atuin-syntect = { path = "atuin-syntect", version = "14.0.0" }
log = "0.4" log = "0.4"
env_logger = "0.10.0" env_logger = "0.10.0"
@ -74,7 +75,6 @@ tiny-bip39 = "1"
futures-util = "0.3" futures-util = "0.3"
fuzzy-matcher = "0.3.7" fuzzy-matcher = "0.3.7"
colored = "2.0.0" colored = "2.0.0"
syntect = { version = "5.0.0", default-features = false, features = ["dump-load", "parsing", "regex-fancy"] }
# ratatui # ratatui
bitflags = "1.3" bitflags = "1.3"

15
atuin-syntect/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "atuin-syntect"
version = "14.0.0"
authors = ["Ellie Huxtable <ellie@elliehuxtable.com>"]
edition = "2018"
license = "MIT"
description = "common library for atuin"
homepage = "https://atuin.sh"
repository = "https://github.com/ellie/atuin"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
syntect = { version = "5.0.0", default-features = false, features = ["dump-load", "parsing", "regex-fancy"] }
once_cell = "1"

View File

@ -1,6 +1,120 @@
use syntect::parsing::{ParseScopeError, Scope}; use once_cell::sync::OnceCell;
use syntect::{
dumps::from_uncompressed_data,
parsing::{
BasicScopeStackOp, ParseScopeError, Scope, ScopeStack, ScopeStackOp, SyntaxReference,
SyntaxSet,
},
};
use crate::ratatui::style::{Color, Style}; mod style;
pub use style::*;
impl Theme {
// this is a manual/simpler implementation of
// syntect::highlight::HighlightIterator
// to use a custom theme using `ratatui::Style`.
// This is so we don't have to care about RGB and can instead use
// terminal colours
pub fn highlight(&self, h: &str, parsed: &ParsedSyntax, draw: &mut dyn FnMut(&str, Style)) {
let mut stack = ScopeStack::default();
let mut styles: Vec<(f64, Style)> = vec![];
for (line, parsed_line) in h.lines().zip(parsed) {
draw("", Style::default());
let mut last = 0;
for &(index, ref op) in parsed_line {
let style = styles.last().copied().unwrap_or_default().1;
stack
.apply_with_hook(op, |op, stack| {
highlight_hook(&op, stack, &self.rules, &mut styles);
})
.unwrap();
draw(&line[last..index], style);
last = index;
}
let style = styles.last().copied().unwrap_or_default().1;
draw(&line[last..], style);
}
}
}
#[allow(clippy::cast_possible_truncation)]
fn highlight_hook(
op: &BasicScopeStackOp,
stack: &[Scope],
rules: &[ThemeRule],
styles: &mut Vec<(f64, Style)>,
) {
match op {
BasicScopeStackOp::Push(scope) => {
let mut scored_style = styles
.last()
.copied()
.unwrap_or_else(|| (-1.0, Style::default()));
for rule in rules.iter().filter(|a| a.scope.is_prefix_of(*scope)) {
let single_score =
f64::from(rule.scope.len()) * f64::from(3 * ((stack.len() - 1) as u32)).exp2();
if single_score > scored_style.0 {
scored_style.0 = single_score;
scored_style.1 = rule.style;
}
}
styles.push(scored_style);
}
BasicScopeStackOp::Pop => {
styles.pop();
}
}
}
pub fn get_syntax() -> ShellSyntax<'static> {
static SYNTAX: OnceCell<SyntaxSet> = OnceCell::new();
let syntax = SYNTAX.get_or_init(|| {
from_uncompressed_data(include_bytes!("default_nonewlines.packdump")).unwrap()
});
ShellSyntax {
syntax,
sh: syntax.find_syntax_by_extension("sh").unwrap(),
fish: syntax.find_syntax_by_extension("fish").unwrap(),
nu: syntax.find_syntax_by_extension("nu").unwrap(),
}
}
#[derive(Clone, Copy)]
pub struct ShellSyntax<'s> {
syntax: &'s SyntaxSet,
sh: &'s SyntaxReference,
fish: &'s SyntaxReference,
nu: &'s SyntaxReference,
}
pub type ParsedSyntax = Vec<Vec<(usize, ScopeStackOp)>>;
impl ShellSyntax<'_> {
pub fn parse_shell(self, h: &str) -> ParsedSyntax {
let mut sh = syntect::parsing::ParseState::new(self.sh);
let mut fish = syntect::parsing::ParseState::new(self.fish);
let mut nu = syntect::parsing::ParseState::new(self.nu);
let mut lines = vec![];
for line in h.lines() {
if let Ok(line) = sh.parse_line(line, self.syntax) {
lines.push(line);
} else if let Ok(line) = fish.parse_line(line, self.syntax) {
lines.push(line);
} else if let Ok(line) = nu.parse_line(line, self.syntax) {
lines.push(line);
} else {
lines.push(Vec::new());
}
}
lines
}
}
pub struct Theme { pub struct Theme {
pub rules: Vec<ThemeRule>, pub rules: Vec<ThemeRule>,

View File

@ -0,0 +1,61 @@
//! `style` contains the primitives used to control how your user interface will look.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
White,
Rgb(u8, u8, u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
}
impl Style {
/// Changes the foreground color.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Style};
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
pub fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
}
/// Changes the background color.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Style};
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
pub fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
}
}

View File

@ -16,7 +16,6 @@ mod duration;
mod engines; mod engines;
mod history_list; mod history_list;
mod interactive; mod interactive;
mod syntax;
pub use duration::{format_duration, format_duration_into}; pub use duration::{format_duration, format_duration_into};
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]

View File

@ -7,13 +7,9 @@ use crate::ratatui::{
widgets::{Block, StatefulWidget, Widget}, widgets::{Block, StatefulWidget, Widget},
}; };
use atuin_client::history::History; use atuin_client::history::History;
use syntect::parsing::{BasicScopeStackOp, Scope, ScopeStack}; use atuin_syntect::{ParsedSyntax, Theme};
use super::{ use super::format_duration;
format_duration,
interactive::ParsedSyntax,
syntax::{Theme, ThemeRule},
};
pub struct HistoryList<'a> { pub struct HistoryList<'a> {
history: &'a [History], history: &'a [History],
@ -176,39 +172,22 @@ impl DrawState<'_> {
let selected = self.y as usize + self.state.offset == self.state.selected; let selected = self.y as usize + self.state.offset == self.state.selected;
let with_select = move |style: Style| { let with_select = move |style: Style| {
if selected { if selected {
style.bg(theme.selection).add_modifier(Modifier::BOLD) style
.bg(map_color(theme.selection))
.add_modifier(Modifier::BOLD)
} else { } else {
style style
} }
}; };
if let Some(parsed) = parsed { if let Some(parsed) = parsed {
// this is a manual/simpler implementation of theme.highlight(&h.command, parsed, &mut |t, style| {
// syntect::highlight::HighlightIterator if t.is_empty() {
// to use a custom theme using `ratatui::Style`.
// This is so we don't have to care about RGB and can instead use
// terminal colours
let mut stack = ScopeStack::default();
let mut styles: Vec<(f64, Style)> = vec![];
for (line, parsed_line) in h.command.lines().zip(parsed) {
self.x += 1; self.x += 1;
} else {
let mut last = 0; self.draw(t, with_select(map_style(style)));
for &(index, ref op) in parsed_line {
let style = styles.last().copied().unwrap_or_default().1;
stack
.apply_with_hook(op, |op, stack| {
highlight_hook(&op, stack, &theme.rules, &mut styles);
})
.unwrap();
self.draw(&line[last..index], with_select(style));
last = index;
}
let style = styles.last().copied().unwrap_or_default().1;
self.draw(&line[last..], with_select(style));
} }
});
} else { } else {
let style = with_select(Style::default()); let style = with_select(Style::default());
for section in h.command.split_ascii_whitespace() { for section in h.command.split_ascii_whitespace() {
@ -231,34 +210,33 @@ impl DrawState<'_> {
} }
} }
#[allow(clippy::cast_possible_truncation)] fn map_color(c: atuin_syntect::Color) -> Color {
fn highlight_hook( match c {
op: &BasicScopeStackOp, atuin_syntect::Color::Black => Color::Black,
stack: &[Scope], atuin_syntect::Color::Red => Color::Red,
rules: &[ThemeRule], atuin_syntect::Color::Green => Color::Green,
styles: &mut Vec<(f64, Style)>, atuin_syntect::Color::Yellow => Color::Yellow,
) { atuin_syntect::Color::Blue => Color::Blue,
match op { atuin_syntect::Color::Magenta => Color::Magenta,
BasicScopeStackOp::Push(scope) => { atuin_syntect::Color::Cyan => Color::Cyan,
let mut scored_style = styles atuin_syntect::Color::Gray => Color::Gray,
.last() atuin_syntect::Color::DarkGray => Color::DarkGray,
.copied() atuin_syntect::Color::LightRed => Color::LightRed,
.unwrap_or_else(|| (-1.0, Style::default())); atuin_syntect::Color::LightGreen => Color::LightGreen,
atuin_syntect::Color::LightYellow => Color::LightYellow,
for rule in rules.iter().filter(|a| a.scope.is_prefix_of(*scope)) { atuin_syntect::Color::LightBlue => Color::LightBlue,
let single_score = atuin_syntect::Color::LightMagenta => Color::LightMagenta,
f64::from(rule.scope.len()) * f64::from(3 * ((stack.len() - 1) as u32)).exp2(); atuin_syntect::Color::LightCyan => Color::LightCyan,
atuin_syntect::Color::White => Color::White,
if single_score > scored_style.0 { atuin_syntect::Color::Rgb(r, g, b) => Color::Rgb(r, g, b),
scored_style.0 = single_score; }
scored_style.1 = rule.style; }
}
} fn map_style(c: atuin_syntect::Style) -> Style {
Style {
styles.push(scored_style); fg: c.fg.map(map_color),
} bg: c.bg.map(map_color),
BasicScopeStackOp::Pop => { add_modifier: Modifier::empty(),
styles.pop(); sub_modifier: Modifier::empty(),
}
} }
} }

View File

@ -4,6 +4,7 @@ use std::{
time::Duration, time::Duration,
}; };
use atuin_syntect::{ParsedSyntax, Theme, ShellSyntax, get_syntax, get_theme};
use crossterm::{ use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent}, event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
execute, terminal, execute, terminal,
@ -11,11 +12,10 @@ use crossterm::{
use eyre::Result; use eyre::Result;
use futures_util::FutureExt; use futures_util::FutureExt;
use semver::Version; use semver::Version;
use syntect::{ // use syntect::{
dumps::{from_binary, from_uncompressed_data}, // dumps::from_uncompressed_data,
highlighting::Highlighter, // parsing::{ScopeStackOp, SyntaxReference, SyntaxSet},
parsing::{ScopeStackOp, SyntaxReference, SyntaxSet}, // };
};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use atuin_client::{ use atuin_client::{
@ -28,11 +28,9 @@ use super::{
cursor::Cursor, cursor::Cursor,
engines::{SearchEngine, SearchState}, engines::{SearchEngine, SearchState},
history_list::{HistoryList, ListState, PREFIX_LENGTH}, history_list::{HistoryList, ListState, PREFIX_LENGTH},
syntax::Theme,
}; };
use crate::{command::client::search::engines, VERSION}; use crate::{command::client::search::engines, VERSION};
use crate::{ use crate::{
command::client::search::syntax::get_theme,
ratatui::{ ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout}, layout::{Alignment, Constraint, Direction, Layout},
@ -46,9 +44,8 @@ use crate::{
const RETURN_ORIGINAL: usize = usize::MAX; const RETURN_ORIGINAL: usize = usize::MAX;
const RETURN_QUERY: usize = usize::MAX - 1; const RETURN_QUERY: usize = usize::MAX - 1;
pub type ParsedSyntax = Vec<Vec<(usize, ScopeStackOp)>>;
struct State<'s> { struct State {
history_count: i64, history_count: i64,
update_needed: Option<Version>, update_needed: Option<Version>,
results_state: ListState, results_state: ListState,
@ -61,17 +58,17 @@ struct State<'s> {
// highlighting // highlighting
results_parsed: HashMap<String, ParsedSyntax>, results_parsed: HashMap<String, ParsedSyntax>,
theme: Theme, theme: Theme,
syntax: ShellSyntax<'s>, syntax: ShellSyntax<'static>,
} }
impl State<'_> { impl State {
async fn query_results(&mut self, db: &mut dyn Database) -> Result<Vec<History>> { async fn query_results(&mut self, db: &mut dyn Database) -> Result<Vec<History>> {
let results = self.engine.query(&self.search, db).await?; let results = self.engine.query(&self.search, db).await?;
self.results_state.select(0); self.results_state.select(0);
for h in &results { for h in &results {
self.results_parsed self.results_parsed
.entry(h.id.clone()) .entry(h.id.clone())
.or_insert_with(|| parse_shell(h, self.syntax)); .or_insert_with(|| self.syntax.parse_shell(&h.command));
} }
Ok(results) Ok(results)
} }
@ -534,14 +531,6 @@ pub async fn history(
let history_count = db.history_count().await?; let history_count = db.history_count().await?;
let syntax: SyntaxSet =
from_uncompressed_data(include_bytes!("syntax/default_nonewlines.packdump")).unwrap();
// let themes: ThemeSet = from_binary(include_bytes!("syntax/default.themedump"));
// let highlighter = Highlighter::new(&themes.themes["base16-ocean.dark"]);
// let syntax = SyntaxSet::load_defaults_nonewlines();
// let mut themes = ThemeSet::load_defaults();
let mut app = State { let mut app = State {
history_count, history_count,
results_state: ListState::default(), results_state: ListState::default(),
@ -563,12 +552,7 @@ pub async fn history(
results_parsed: HashMap::new(), results_parsed: HashMap::new(),
theme: get_theme().unwrap(), theme: get_theme().unwrap(),
syntax: ShellSyntax { syntax: get_syntax(),
syntaxs: &syntax,
sh: syntax.find_syntax_by_extension("sh").unwrap(),
fish: syntax.find_syntax_by_extension("fish").unwrap(),
nu: syntax.find_syntax_by_extension("nu").unwrap(),
},
}; };
// let mut hi = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); // let mut hi = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
@ -638,30 +622,3 @@ pub async fn history(
res res
} }
#[derive(Clone, Copy)]
struct ShellSyntax<'s> {
syntaxs: &'s SyntaxSet,
sh: &'s SyntaxReference,
fish: &'s SyntaxReference,
nu: &'s SyntaxReference,
}
fn parse_shell(h: &History, syntax: ShellSyntax<'_>) -> ParsedSyntax {
let mut sh = syntect::parsing::ParseState::new(syntax.sh);
let mut fish = syntect::parsing::ParseState::new(syntax.fish);
let mut nu = syntect::parsing::ParseState::new(syntax.nu);
let mut lines = vec![];
for line in h.command.lines() {
if let Ok(line) = sh.parse_line(line, syntax.syntaxs) {
lines.push(line);
} else if let Ok(line) = fish.parse_line(line, syntax.syntaxs) {
lines.push(line);
} else if let Ok(line) = nu.parse_line(line, syntax.syntaxs) {
lines.push(line);
} else {
lines.push(Vec::new());
}
}
lines
}