use std::cmp::Ordering;
use ratatui::{
crossterm::event::KeyEvent,
layout::Rect,
prelude::Buffer,
style::{Color, Modifier},
widgets::{Paragraph, Widget as _},
};
use ratzgo::{
core::*,
scroll::{ScrollAction, repos_y_anchored},
text::Line,
};
use smol_str::SmolStr;
use crate::utils::{
jj::{ExpandRevsetError, JJHandle},
tui::{JJChange, LogText},
};
#[derive(Debug)]
pub struct LogHistory<'a, Message> {
text: &'a LogText,
state: &'a mut LogHistoryState,
activity: bool,
on_key: OnKey<'a, Message>,
}
impl<'a, Message> LogHistory<'a, Message> {
pub fn new(text: &'a LogText, state: &'a mut LogHistoryState) -> Self {
Self {
text,
state,
activity: false,
on_key: Default::default(),
}
}
}
impl<'a, Message> Widget<Message> for LogHistory<'a, Message>
where
Message: std::fmt::Debug,
{
fn activity(&self) -> bool {
self.activity
}
fn area(&self) -> Rect {
self.state.area
}
fn set_area(&mut self, area: Rect) {
self.state.area = area;
}
fn handle_key(&mut self, key: &KeyEvent) -> Option<Message> {
self.on_key.key(key)
}
fn adapt(&mut self, buf: &mut Buffer) {
if self.text.text().height() == 0 {
return;
}
let mut text = self.text.text().clone();
if let Some(v) = &mut self.state.yanking {
match v {
Yanking::One { id } => {
if let Some((_, change)) = self.text.find_by_id(id) {
for line in text
.lines
.get_mut(change.range_line.clone())
.into_iter()
.flatten()
{
for span in line.iter_mut() {
span.style = span.style.fg(Color::Black).bg(Color::Yellow);
}
}
}
}
Yanking::Range { ids, .. } => {
for id in ids.lines() {
let Some((_, change)) = self.text.find_by_long_id(id) else {
continue;
};
for line in text
.lines
.get_mut(change.range_line.clone())
.into_iter()
.flatten()
{
for span in line.iter_mut() {
span.style = span.style.fg(Color::Black).bg(Color::Yellow);
}
}
}
}
}
}
let Some(change) = self.text.beacons().get(self.state.hover) else {
return;
};
for line in text
.lines
.get_mut(change.range_line.clone())
.into_iter()
.flatten()
{
for span in line.iter_mut() {
span.style = span.style.add_modifier(Modifier::REVERSED);
}
}
Paragraph::new(text)
.scroll(self.state.scroll)
.render(self.area(), buf);
}
}
#[derive(Debug, Default)]
pub struct LogHistoryState {
scroll: (u16, u16),
hover: usize,
yanking: Option<Yanking>,
area: Rect,
}
#[derive(Debug)]
pub enum Yanking {
One {
id: SmolStr,
},
Range {
base: (SmolStr, SmolStr),
ids: Box<str>,
},
}
impl LogHistoryState {
pub fn hovered(&self) -> usize {
self.hover
}
pub fn hover(&mut self, index: usize) {
self.hover = index;
}
pub fn reset(&mut self) {
self.hover(0);
self.scroll = (0, 0);
}
pub async fn yank(&mut self, log: &LogText, jj: &JJHandle, id: &str) {
match self.yanking.take() {
None => {
self.yanking = Some(Yanking::One { id: id.into() });
}
Some(Yanking::One { id: prev_id }) => {
let Some((prev_i, _)) = log.find_by_id(&prev_id) else {
return;
};
let Some((i, _)) = log.find_by_id(id) else {
return;
};
let (start, end) = match prev_i.cmp(&i) {
Ordering::Less => (id, prev_id.as_str()),
Ordering::Greater => (prev_id.as_str(), id),
Ordering::Equal => return,
};
let revset = format!("{start}::{end}");
match jj.expand_revset(&revset).await {
Ok(ids) => {
self.yanking = Some(Yanking::Range {
base: (start.into(), end.into()),
ids,
})
}
Err(ExpandRevsetError::Invalid) => {
self.yanking = Some(Yanking::One { id: prev_id });
}
Err(e) => {
let mut text = e.into_text();
text.lines
.insert(0, Line::from(format!("revset: {revset}")));
ratzgo::log::error("expanding revset", text);
}
}
}
Some(Yanking::Range { .. }) => {
self.yanking = Some(Yanking::One { id: id.into() });
}
}
}
pub fn unyank(&mut self) -> Option<Yanking> {
self.yanking.take()
}
pub fn yanking(&self) -> Option<&Yanking> {
self.yanking.as_ref()
}
pub fn area(&self) -> Rect {
self.area
}
pub fn scroll_vertical<'a>(
&mut self,
log: &'a LogText,
action: ScrollAction,
) -> Option<&'a JJChange> {
let (i, change) = match action {
ScrollAction::Fixed(n) => {
let i = self.hover.checked_add_signed(n as isize)?;
let change = log.beacons().get(i)?;
(i, change)
}
ScrollAction::Viewport(n) => {
let beacon = log.beacons().get(self.hover)?;
let offset = (self.area.height as f32 * n as f32 * 0.01) as isize;
let line = beacon.range_line.start.saturating_add_signed(offset);
let (i, change) = log.find_by_line(line)?;
if change.id == beacon.id {
return None;
}
(i, change)
}
};
self.hover(i);
self.scroll.0 = repos_y_anchored(
self.scroll.0,
log.text().height(),
self.area.height,
4,
change.range_line.start,
);
Some(change)
}
}
impl<'a, Message> From<LogHistory<'a, Message>> for Element<'a, Message>
where
Message: std::fmt::Debug + 'a,
{
fn from(widget: LogHistory<'a, Message>) -> Self {
Element::new(widget)
}
}
impl<'a, Message> Activable for LogHistory<'a, Message> {
fn active_mut(&mut self) -> &mut bool {
&mut self.activity
}
}
impl<'a, Message> OnKeyBuilder<'a, Message> for LogHistory<'a, Message> {
fn on_key_mut(&mut self) -> &mut OnKey<'a, Message> {
&mut self.on_key
}
}