fix: blend diff highlights with terminal theme colors
This commit is contained in:
@@ -12,5 +12,6 @@ pub mod ruby;
|
||||
pub mod session;
|
||||
mod snippet;
|
||||
pub mod syntax;
|
||||
mod terminal_colors;
|
||||
pub mod tui;
|
||||
mod wrap;
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{self, Write},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyEventState, KeyModifiers};
|
||||
|
||||
const QUERY_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
const ESCAPE_TIMEOUT: Duration = Duration::from_millis(25);
|
||||
const HEADERS: [&[u8]; 3] = [b"11;", b"4;1;", b"4;2;"];
|
||||
const MAX_BODY: usize = 32;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ThemeColors {
|
||||
pub background: [u8; 3],
|
||||
pub red: [u8; 3],
|
||||
pub green: [u8; 3],
|
||||
}
|
||||
|
||||
struct Query {
|
||||
deadline: Instant,
|
||||
colors: [Option<[u8; 3]>; 3],
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum Capture {
|
||||
#[default]
|
||||
Idle,
|
||||
Escape(Instant),
|
||||
Report(Instant),
|
||||
Terminator,
|
||||
}
|
||||
|
||||
/// Adapts crossterm's key-shaped OSC replies without reading from stdin itself.
|
||||
/// Only complete color reports are consumed; rejected candidates retain their
|
||||
/// original events, including modifiers, kind, and state.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TerminalColors {
|
||||
pub colors: Option<ThemeColors>,
|
||||
query: Option<Query>,
|
||||
capture: Capture,
|
||||
original: Vec<Event>,
|
||||
body: [u8; MAX_BODY],
|
||||
len: usize,
|
||||
ready: VecDeque<Event>,
|
||||
}
|
||||
|
||||
impl TerminalColors {
|
||||
pub fn query(&mut self, output: &mut impl Write, now: Instant) -> io::Result<()> {
|
||||
self.expire(now);
|
||||
self.query = None;
|
||||
if let Err(error) = output
|
||||
.write_all(b"\x1b]11;?\x1b\\\x1b]4;1;?;2;?\x1b\\")
|
||||
.and_then(|()| output.flush())
|
||||
{
|
||||
self.colors = None;
|
||||
return Err(error);
|
||||
}
|
||||
self.query = Some(Query {
|
||||
deadline: now + QUERY_TIMEOUT,
|
||||
colors: [None; 3],
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push(&mut self, event: Event, now: Instant) {
|
||||
self.expire(now);
|
||||
self.accept(event, now);
|
||||
}
|
||||
|
||||
pub fn expire(&mut self, now: Instant) {
|
||||
if self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| now >= query.deadline)
|
||||
{
|
||||
self.query = None;
|
||||
self.colors = None;
|
||||
}
|
||||
let ambiguous_expired = match self.capture {
|
||||
Capture::Escape(deadline) => now >= deadline,
|
||||
Capture::Report(deadline) => {
|
||||
now >= deadline
|
||||
&& !HEADERS
|
||||
.iter()
|
||||
.any(|header| self.body[..self.len].starts_with(header))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if ambiguous_expired {
|
||||
self.replay();
|
||||
}
|
||||
// Retain a bounded, recognized OSC candidate across query expiry: a
|
||||
// late remainder must still be swallowed, not inserted into the editor.
|
||||
// Neither late replies nor partial triples can update the palette.
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<Event> {
|
||||
self.ready.pop_front()
|
||||
}
|
||||
|
||||
fn accept(&mut self, event: Event, now: Instant) {
|
||||
let token = token(&event);
|
||||
match self.capture {
|
||||
Capture::Idle => match token {
|
||||
Token::Start => {
|
||||
self.original.push(event);
|
||||
self.capture = Capture::Report(now + QUERY_TIMEOUT);
|
||||
}
|
||||
Token::Escape => {
|
||||
self.original.push(event);
|
||||
self.capture = Capture::Escape(now + ESCAPE_TIMEOUT);
|
||||
}
|
||||
_ => self.ready.push_back(event),
|
||||
},
|
||||
Capture::Escape(_) if matches!(token, Token::Byte(b']')) => {
|
||||
self.original.push(event);
|
||||
self.capture = Capture::Report(now + QUERY_TIMEOUT);
|
||||
}
|
||||
Capture::Report(_) if matches!(token, Token::Escape) => {
|
||||
self.original.push(event);
|
||||
self.capture = Capture::Terminator;
|
||||
}
|
||||
Capture::Report(_) if matches!(token, Token::End) => {
|
||||
self.original.push(event);
|
||||
self.finish();
|
||||
}
|
||||
Capture::Terminator if matches!(token, Token::Byte(b'\\')) => {
|
||||
self.original.push(event);
|
||||
self.finish();
|
||||
}
|
||||
Capture::Report(_) => {
|
||||
if let Token::Byte(byte) = token
|
||||
&& self.len < MAX_BODY
|
||||
{
|
||||
self.body[self.len] = byte;
|
||||
if report_prefix(&self.body[..self.len + 1]) {
|
||||
self.len += 1;
|
||||
self.original.push(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.replay();
|
||||
self.accept(event, now);
|
||||
}
|
||||
_ => {
|
||||
self.replay();
|
||||
self.accept(event, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
let Some((slot, color)) = parse_report(&self.body[..self.len]) else {
|
||||
self.replay();
|
||||
return;
|
||||
};
|
||||
self.original.clear();
|
||||
self.len = 0;
|
||||
self.capture = Capture::Idle;
|
||||
let Some(query) = self.query.as_mut() else {
|
||||
return;
|
||||
};
|
||||
query.colors[slot] = Some(color);
|
||||
if let [Some(background), Some(red), Some(green)] = query.colors {
|
||||
self.colors = Some(ThemeColors {
|
||||
background,
|
||||
red,
|
||||
green,
|
||||
});
|
||||
self.query = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn replay(&mut self) {
|
||||
self.ready.extend(self.original.drain(..));
|
||||
self.len = 0;
|
||||
self.capture = Capture::Idle;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Token {
|
||||
Start,
|
||||
End,
|
||||
Escape,
|
||||
Byte(u8),
|
||||
Other,
|
||||
}
|
||||
|
||||
fn token(event: &Event) -> Token {
|
||||
let Event::Key(key) = event else {
|
||||
return Token::Other;
|
||||
};
|
||||
if key.kind != KeyEventKind::Press || key.state != KeyEventState::NONE {
|
||||
return Token::Other;
|
||||
}
|
||||
match (key.code, key.modifiers) {
|
||||
(KeyCode::Char(']'), KeyModifiers::ALT) => Token::Start,
|
||||
(KeyCode::Char('\\'), KeyModifiers::ALT) | (KeyCode::Char('g'), KeyModifiers::CONTROL) => {
|
||||
Token::End
|
||||
}
|
||||
(KeyCode::Esc, KeyModifiers::NONE) => Token::Escape,
|
||||
(KeyCode::Char(ch), modifiers)
|
||||
if ch.is_ascii()
|
||||
&& (modifiers == KeyModifiers::NONE
|
||||
|| (ch.is_ascii_uppercase() && modifiers == KeyModifiers::SHIFT)) =>
|
||||
{
|
||||
Token::Byte(ch as u8)
|
||||
}
|
||||
_ => Token::Other,
|
||||
}
|
||||
}
|
||||
|
||||
fn report_prefix(body: &[u8]) -> bool {
|
||||
HEADERS.iter().any(|header| {
|
||||
header.starts_with(body) || body.strip_prefix(*header).is_some_and(color_prefix)
|
||||
})
|
||||
}
|
||||
|
||||
fn color_prefix(color: &[u8]) -> bool {
|
||||
if b"rgb:".starts_with(color) {
|
||||
return true;
|
||||
}
|
||||
if let Some(hex) = color.strip_prefix(b"#") {
|
||||
return hex.len() <= 6 && hex.iter().all(u8::is_ascii_hexdigit);
|
||||
}
|
||||
let Some(rgb) = color.strip_prefix(b"rgb:") else {
|
||||
return false;
|
||||
};
|
||||
let mut components = rgb.split(|&byte| byte == b'/');
|
||||
for index in 0..3 {
|
||||
let Some(component) = components.next() else {
|
||||
return true;
|
||||
};
|
||||
if component.len() > 4 || !component.iter().all(u8::is_ascii_hexdigit) {
|
||||
return false;
|
||||
}
|
||||
if component.is_empty() && (index < 2 && components.clone().next().is_some()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
components.next().is_none()
|
||||
}
|
||||
|
||||
fn parse_report(body: &[u8]) -> Option<(usize, [u8; 3])> {
|
||||
for (slot, header) in HEADERS.iter().enumerate() {
|
||||
if let Some(color) = body.strip_prefix(*header) {
|
||||
return parse_color(color).map(|color| (slot, color));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_color(color: &[u8]) -> Option<[u8; 3]> {
|
||||
let mut result = [0; 3];
|
||||
if let Some(hex) = color.strip_prefix(b"#") {
|
||||
if hex.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
for (channel, component) in result.iter_mut().zip(hex.chunks_exact(2)) {
|
||||
*channel = hex_value(component)? as u8;
|
||||
}
|
||||
return Some(result);
|
||||
}
|
||||
let mut components = color.strip_prefix(b"rgb:")?.split(|&byte| byte == b'/');
|
||||
for channel in &mut result {
|
||||
let component = components.next()?;
|
||||
if component.is_empty() || component.len() > 4 {
|
||||
return None;
|
||||
}
|
||||
let value = hex_value(component)?;
|
||||
let maximum = (1_u32 << (component.len() * 4)) - 1;
|
||||
*channel = ((value * 255 + maximum / 2) / maximum) as u8;
|
||||
}
|
||||
components.next().is_none().then_some(result)
|
||||
}
|
||||
|
||||
fn hex_value(component: &[u8]) -> Option<u32> {
|
||||
component.iter().try_fold(0, |value, &byte| {
|
||||
(byte as char).to_digit(16).map(|digit| value * 16 + digit)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crossterm::event::{KeyEvent, MouseEvent, MouseEventKind};
|
||||
|
||||
fn key(code: KeyCode, modifiers: KeyModifiers) -> Event {
|
||||
Event::Key(KeyEvent::new(code, modifiers))
|
||||
}
|
||||
|
||||
fn bytes(collector: &mut TerminalColors, text: &str, now: Instant) {
|
||||
for ch in text.chars() {
|
||||
let modifiers = if ch.is_ascii_uppercase() {
|
||||
KeyModifiers::SHIFT
|
||||
} else {
|
||||
KeyModifiers::NONE
|
||||
};
|
||||
collector.push(key(KeyCode::Char(ch), modifiers), now);
|
||||
}
|
||||
}
|
||||
|
||||
fn report(collector: &mut TerminalColors, body: &str, now: Instant) {
|
||||
collector.push(key(KeyCode::Char(']'), KeyModifiers::ALT), now);
|
||||
bytes(collector, body, now);
|
||||
collector.push(key(KeyCode::Char('g'), KeyModifiers::CONTROL), now);
|
||||
}
|
||||
|
||||
fn drain(collector: &mut TerminalColors) -> Vec<Event> {
|
||||
std::iter::from_fn(|| collector.pop()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragmented_triple_accepts_bel_and_both_st_encodings() {
|
||||
let now = Instant::now();
|
||||
let mut collector = TerminalColors::default();
|
||||
collector.query(&mut Vec::new(), now).unwrap();
|
||||
collector.push(key(KeyCode::Esc, KeyModifiers::NONE), now);
|
||||
collector.push(
|
||||
key(KeyCode::Char(']'), KeyModifiers::NONE),
|
||||
now + Duration::from_millis(1),
|
||||
);
|
||||
bytes(
|
||||
&mut collector,
|
||||
"11;rgb:0/12/345",
|
||||
now + Duration::from_millis(2),
|
||||
);
|
||||
collector.push(
|
||||
key(KeyCode::Esc, KeyModifiers::NONE),
|
||||
now + Duration::from_millis(3),
|
||||
);
|
||||
collector.push(
|
||||
key(KeyCode::Char('\\'), KeyModifiers::NONE),
|
||||
now + Duration::from_millis(4),
|
||||
);
|
||||
assert_eq!(collector.colors, None);
|
||||
report(
|
||||
&mut collector,
|
||||
"4;2;rgb:f/ffff/ABCD",
|
||||
now + Duration::from_millis(5),
|
||||
);
|
||||
collector.push(
|
||||
key(KeyCode::Char(']'), KeyModifiers::ALT),
|
||||
now + Duration::from_millis(6),
|
||||
);
|
||||
bytes(
|
||||
&mut collector,
|
||||
"4;1;#FF1234",
|
||||
now + Duration::from_millis(7),
|
||||
);
|
||||
collector.push(
|
||||
key(KeyCode::Char('\\'), KeyModifiers::ALT),
|
||||
now + Duration::from_millis(8),
|
||||
);
|
||||
assert_eq!(
|
||||
collector.colors,
|
||||
Some(ThemeColors {
|
||||
background: [0, 18, 52],
|
||||
red: [255, 18, 52],
|
||||
green: [255, 255, 171],
|
||||
})
|
||||
);
|
||||
assert!(drain(&mut collector).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_refresh_atomically_replaces_light_and_dark_triples() {
|
||||
let now = Instant::now();
|
||||
let mut collector = TerminalColors::default();
|
||||
let palettes = [
|
||||
("11;#FDF6E3", [253, 246, 227]),
|
||||
("11;rgb:0000/0000/0000", [0, 0, 0]),
|
||||
];
|
||||
for (cycle, (background, expected)) in palettes.into_iter().enumerate() {
|
||||
let time = now + Duration::from_secs(cycle as u64);
|
||||
let focus = Event::FocusGained;
|
||||
collector.push(focus.clone(), time);
|
||||
assert_eq!(collector.pop(), Some(focus));
|
||||
let previous = collector.colors;
|
||||
collector.query(&mut Vec::new(), time).unwrap();
|
||||
report(&mut collector, "4;1;rgb:ff/00/00", time);
|
||||
report(&mut collector, background, time);
|
||||
assert_eq!(collector.colors, previous);
|
||||
report(&mut collector, "4;2;rgb:00/ff/00", time);
|
||||
assert_eq!(
|
||||
collector.colors,
|
||||
Some(ThemeColors {
|
||||
background: expected,
|
||||
red: [255, 0, 0],
|
||||
green: [0, 255, 0],
|
||||
})
|
||||
);
|
||||
assert!(drain(&mut collector).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_refresh_clears_stale_colors_and_consumes_late_partial_reply() {
|
||||
let now = Instant::now();
|
||||
let mut collector = TerminalColors::default();
|
||||
collector.query(&mut Vec::new(), now).unwrap();
|
||||
report(&mut collector, "11;#101010", now);
|
||||
report(&mut collector, "4;1;#FF0000", now);
|
||||
report(&mut collector, "4;2;#00FF00", now);
|
||||
assert!(collector.colors.is_some());
|
||||
let refresh = now + Duration::from_secs(1);
|
||||
collector.query(&mut Vec::new(), refresh).unwrap();
|
||||
report(&mut collector, "11;#EEEEEE", refresh);
|
||||
collector.push(key(KeyCode::Char(']'), KeyModifiers::ALT), refresh);
|
||||
bytes(&mut collector, "4;1;rgb:ffff/", refresh);
|
||||
collector.expire(refresh + QUERY_TIMEOUT);
|
||||
assert_eq!(collector.colors, None);
|
||||
let late = refresh + Duration::from_secs(1);
|
||||
bytes(&mut collector, "0000/0000", late);
|
||||
collector.push(key(KeyCode::Esc, KeyModifiers::NONE), late);
|
||||
collector.expire(late + Duration::from_secs(1));
|
||||
collector.push(
|
||||
key(KeyCode::Char('\\'), KeyModifiers::NONE),
|
||||
late + Duration::from_secs(1),
|
||||
);
|
||||
report(&mut collector, "4;2;#00FF00", late + Duration::from_secs(1));
|
||||
assert_eq!(collector.colors, None);
|
||||
assert!(drain(&mut collector).is_empty());
|
||||
let typed = key(KeyCode::Char('i'), KeyModifiers::NONE);
|
||||
collector.push(typed.clone(), late + Duration::from_secs(2));
|
||||
assert_eq!(collector.pop(), Some(typed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_events_and_rejected_candidates_replay_exactly() {
|
||||
let now = Instant::now();
|
||||
let mut collector = TerminalColors::default();
|
||||
collector.query(&mut Vec::new(), now).unwrap();
|
||||
let events = vec![
|
||||
key(KeyCode::Char(']'), KeyModifiers::ALT),
|
||||
key(KeyCode::Char('4'), KeyModifiers::NONE),
|
||||
key(KeyCode::Char(';'), KeyModifiers::NONE),
|
||||
key(KeyCode::Char('3'), KeyModifiers::NONE),
|
||||
key(KeyCode::Char('g'), KeyModifiers::CONTROL),
|
||||
key(KeyCode::Char(']'), KeyModifiers::ALT | KeyModifiers::SHIFT),
|
||||
Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char(']'),
|
||||
KeyModifiers::ALT,
|
||||
KeyEventKind::Release,
|
||||
)),
|
||||
Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char('A'),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Repeat,
|
||||
)),
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::CAPS_LOCK,
|
||||
}),
|
||||
Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Moved,
|
||||
column: 2,
|
||||
row: 3,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}),
|
||||
Event::FocusLost,
|
||||
Event::Resize(80, 24),
|
||||
key(KeyCode::Char(']'), KeyModifiers::ALT),
|
||||
Event::Paste("\x1b]11;rgb:ff/ff/ff\x07".into()),
|
||||
Event::FocusGained,
|
||||
];
|
||||
for event in &events {
|
||||
collector.push(event.clone(), now);
|
||||
}
|
||||
assert_eq!(drain(&mut collector), events);
|
||||
assert_eq!(collector.colors, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_oversized_reports_and_lone_escape_do_not_eat_input() {
|
||||
let now = Instant::now();
|
||||
let mut collector = TerminalColors::default();
|
||||
for body in [
|
||||
"11;rgb:12345/0/0",
|
||||
"11;rgb:1//3",
|
||||
"11;#12345",
|
||||
"11;rgb:1/2/3/4",
|
||||
] {
|
||||
let mut expected = vec![key(KeyCode::Char(']'), KeyModifiers::ALT)];
|
||||
expected.extend(
|
||||
body.chars()
|
||||
.map(|ch| key(KeyCode::Char(ch), KeyModifiers::NONE)),
|
||||
);
|
||||
expected.push(key(KeyCode::Char('g'), KeyModifiers::CONTROL));
|
||||
for event in &expected {
|
||||
collector.push(event.clone(), now);
|
||||
}
|
||||
assert_eq!(drain(&mut collector), expected);
|
||||
}
|
||||
let escape = key(KeyCode::Esc, KeyModifiers::NONE);
|
||||
collector.push(escape.clone(), now);
|
||||
assert_eq!(collector.pop(), None);
|
||||
collector.expire(now + ESCAPE_TIMEOUT);
|
||||
assert_eq!(collector.pop(), Some(escape));
|
||||
assert_eq!(collector.pop(), None);
|
||||
let start = key(KeyCode::Char(']'), KeyModifiers::ALT);
|
||||
collector.push(start.clone(), now + Duration::from_secs(1));
|
||||
collector.expire(now + Duration::from_secs(1) + QUERY_TIMEOUT);
|
||||
assert_eq!(collector.pop(), Some(start));
|
||||
assert_eq!(collector.pop(), None);
|
||||
}
|
||||
}
|
||||
+198
-129
@@ -5,14 +5,15 @@ use std::{
|
||||
io::{self, Stdout},
|
||||
panic::{self, AssertUnwindSafe, PanicHookInfo},
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
cursor::Show,
|
||||
event::{
|
||||
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind,
|
||||
self, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
|
||||
KeyModifiers, MouseEvent, MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
terminal::{
|
||||
@@ -43,6 +44,7 @@ use crate::{
|
||||
},
|
||||
session::EditorSession,
|
||||
syntax::{SyntaxCache, SyntaxKind, SyntaxSpan},
|
||||
terminal_colors::{TerminalColors, ThemeColors},
|
||||
wrap::{Segment, WrapCache, caret_segment},
|
||||
};
|
||||
|
||||
@@ -67,6 +69,7 @@ fn restore_terminal() {
|
||||
EndSynchronizedUpdate,
|
||||
DisableBracketedPaste,
|
||||
DisableMouseCapture,
|
||||
DisableFocusChange,
|
||||
LeaveAlternateScreen,
|
||||
Show
|
||||
);
|
||||
@@ -105,7 +108,8 @@ impl TerminalSession {
|
||||
io::stdout(),
|
||||
EnterAlternateScreen,
|
||||
EnableBracketedPaste,
|
||||
EnableMouseCapture
|
||||
EnableMouseCapture,
|
||||
EnableFocusChange
|
||||
)?;
|
||||
body()
|
||||
}));
|
||||
@@ -249,6 +253,7 @@ struct BufferViewports {
|
||||
pane: Option<usize>,
|
||||
parked: BTreeMap<usize, PaneViewport>,
|
||||
rects: Vec<(usize, Rect)>,
|
||||
theme: Option<ThemeColors>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -274,6 +279,18 @@ fn terminal_rect(area: PaneRect) -> Rect {
|
||||
}
|
||||
|
||||
impl BufferViewports {
|
||||
fn set_theme(&mut self, theme: Option<ThemeColors>) {
|
||||
if self.theme == theme {
|
||||
return;
|
||||
}
|
||||
self.theme = theme;
|
||||
let colors = DiffColors::new(theme);
|
||||
self.panels.diff_colors = colors;
|
||||
for pane in self.parked.values_mut() {
|
||||
pane.panels.diff_colors = colors;
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&mut self, id: usize, editor: &Editor) {
|
||||
if self.pane == Some(id) {
|
||||
return;
|
||||
@@ -283,7 +300,7 @@ impl BufferViewports {
|
||||
return;
|
||||
}
|
||||
if !self.parked.contains_key(&id) {
|
||||
let inherited = (|| {
|
||||
let mut inherited = (|| {
|
||||
let buffer = editor.pane_buffer_id(id)?;
|
||||
let source = editor.pane_origin(id);
|
||||
let (offsets, document, document_buffer, documents) = if source == self.pane {
|
||||
@@ -325,6 +342,7 @@ impl BufferViewports {
|
||||
})
|
||||
})()
|
||||
.unwrap_or_default();
|
||||
inherited.panels.diff_colors = self.panels.diff_colors;
|
||||
self.parked.insert(id, inherited);
|
||||
}
|
||||
let outgoing = self
|
||||
@@ -711,34 +729,43 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
let mut output: Stdout = io::stdout();
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(Vec::new()))?;
|
||||
let mut viewports = BufferViewports::default();
|
||||
let mut colors = TerminalColors::default();
|
||||
let mut query_theme = true;
|
||||
let mut redraw = true;
|
||||
let mut pending = None;
|
||||
while !session.editor().should_quit() {
|
||||
redraw |= session.poll();
|
||||
colors.expire(Instant::now());
|
||||
if viewports.theme != colors.colors {
|
||||
viewports.set_theme(colors.colors);
|
||||
redraw = true;
|
||||
}
|
||||
if redraw {
|
||||
draw_frame(&mut terminal, &mut output, |frame| {
|
||||
viewports.draw_session(frame, session)
|
||||
})?;
|
||||
redraw = false;
|
||||
}
|
||||
// Start the reply deadline after painting, not before expensive syntax initialization.
|
||||
if query_theme {
|
||||
colors.query(&mut output, Instant::now())?;
|
||||
query_theme = false;
|
||||
}
|
||||
let input = if let Some(input) = pending.take() {
|
||||
input
|
||||
} else {
|
||||
if !event::poll(Duration::from_millis(25))? {
|
||||
let Some(input) = next_terminal_event(&mut colors, Duration::from_millis(25))?
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
event::read()?
|
||||
};
|
||||
input
|
||||
};
|
||||
match input {
|
||||
Event::Key(key) if key.kind != KeyEventKind::Release => {
|
||||
if session.editor().mode() == Mode::Insert && typing_key(&key) {
|
||||
viewports.panels.follow_files();
|
||||
pending = typing_ready(session, key, || {
|
||||
if event::poll(Duration::ZERO)? {
|
||||
event::read().map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
next_terminal_event(&mut colors, Duration::ZERO)
|
||||
})?;
|
||||
redraw = true;
|
||||
} else if let Some(input) = translate(key) {
|
||||
@@ -754,16 +781,13 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
}
|
||||
Event::Mouse(mouse) => {
|
||||
let (changed, following) = viewports.scroll_ready(session, mouse, || {
|
||||
if event::poll(Duration::ZERO)? {
|
||||
event::read().map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
next_terminal_event(&mut colors, Duration::ZERO)
|
||||
})?;
|
||||
redraw |= changed;
|
||||
pending = following;
|
||||
}
|
||||
Event::Resize(_, _) => redraw = true,
|
||||
Event::FocusGained => query_theme = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -771,6 +795,25 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
fn next_terminal_event(
|
||||
colors: &mut TerminalColors,
|
||||
mut timeout: Duration,
|
||||
) -> io::Result<Option<Event>> {
|
||||
colors.expire(Instant::now());
|
||||
if let Some(input) = colors.pop() {
|
||||
return Ok(Some(input));
|
||||
}
|
||||
while event::poll(timeout)? {
|
||||
colors.push(event::read()?, Instant::now());
|
||||
if let Some(input) = colors.pop() {
|
||||
return Ok(Some(input));
|
||||
}
|
||||
timeout = Duration::ZERO;
|
||||
}
|
||||
colors.expire(Instant::now());
|
||||
Ok(colors.pop())
|
||||
}
|
||||
|
||||
fn typing_key(key: &KeyEvent) -> bool {
|
||||
key.kind != KeyEventKind::Release
|
||||
&& matches!(key.code, KeyCode::Char(ch) if !ch.is_control())
|
||||
@@ -2236,6 +2279,7 @@ fn draw_diff(
|
||||
previous_text_widths.map(|widths| widths[0]),
|
||||
),
|
||||
wrapped,
|
||||
panels.diff_colors,
|
||||
);
|
||||
text_widths[0] = text_area.width;
|
||||
if let Some(editor) =
|
||||
@@ -2268,6 +2312,7 @@ fn draw_diff(
|
||||
previous_text_widths.map(|widths| widths[1]),
|
||||
),
|
||||
wrapped,
|
||||
panels.diff_colors,
|
||||
);
|
||||
text_widths[1] = text_area.width;
|
||||
if let Some(editor) =
|
||||
@@ -2342,6 +2387,7 @@ fn draw_diff_pane(
|
||||
semantic: &[SemanticToken],
|
||||
viewport: (usize, &mut usize, Option<u16>),
|
||||
wrapped: Option<&[WrappedDiffRow]>,
|
||||
colors: DiffColors,
|
||||
) -> Rect {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return area;
|
||||
@@ -2435,11 +2481,7 @@ fn draw_diff_pane(
|
||||
|| (!before && row.kind == RowKind::Added);
|
||||
let background =
|
||||
if changed && !matches!(file.status, FileStatus::Added | FileStatus::Removed) {
|
||||
if before {
|
||||
Color::Rgb(64, 24, 24)
|
||||
} else {
|
||||
Color::Rgb(24, 48, 32)
|
||||
}
|
||||
colors.row[usize::from(!before)]
|
||||
} else {
|
||||
Color::Reset
|
||||
};
|
||||
@@ -2484,11 +2526,7 @@ fn draw_diff_pane(
|
||||
paint_syntax_line(frame, source_text, syntax.line(index), text_area, left);
|
||||
paint_semantic_line(frame, source_text, semantic, index, text_area, left);
|
||||
if !matches!(file.status, FileStatus::Added | FileStatus::Removed) {
|
||||
let style = Style::default().bg(if before {
|
||||
Color::Rgb(112, 40, 40)
|
||||
} else {
|
||||
Color::Rgb(32, 88, 48)
|
||||
});
|
||||
let style = Style::default().bg(colors.character[usize::from(!before)]);
|
||||
for change in &row.changes {
|
||||
let range = &change[usize::from(!before)];
|
||||
if !range.is_empty() {
|
||||
@@ -2920,6 +2958,42 @@ fn diff_visible_rows(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DiffColors {
|
||||
row: [Color; 2],
|
||||
character: [Color; 2],
|
||||
}
|
||||
|
||||
impl DiffColors {
|
||||
fn new(theme: Option<ThemeColors>) -> Self {
|
||||
let Some(theme) = theme else {
|
||||
return Self {
|
||||
row: [Color::Reset; 2],
|
||||
character: [Color::Reset; 2],
|
||||
};
|
||||
};
|
||||
let blend = |tint: [u8; 3], weight: u16| {
|
||||
let channels = std::array::from_fn::<_, 3, _>(|index| {
|
||||
((u16::from(theme.background[index]) * (100 - weight)
|
||||
+ u16::from(tint[index]) * weight
|
||||
+ 50)
|
||||
/ 100) as u8
|
||||
});
|
||||
Color::Rgb(channels[0], channels[1], channels[2])
|
||||
};
|
||||
Self {
|
||||
row: [blend(theme.red, 12), blend(theme.green, 12)],
|
||||
character: [blend(theme.red, 28), blend(theme.green, 28)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DiffColors {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WheelPanels {
|
||||
workspace_managed: bool,
|
||||
@@ -2930,6 +3004,7 @@ struct WheelPanels {
|
||||
files: Option<(Rect, usize, usize)>,
|
||||
manual_files: Option<FileWheelOffset>,
|
||||
diff: Option<DiffGeometry>,
|
||||
diff_colors: DiffColors,
|
||||
diff_syntax: Option<DiffSyntaxCache>,
|
||||
previous_diff: Option<DiffGeometry>,
|
||||
diff_wrap: Option<DiffWrapViewport>,
|
||||
@@ -4026,7 +4101,10 @@ mod tests {
|
||||
#[test]
|
||||
fn diff_status_layouts_hide_absent_panes_and_leave_unchanged_renames_blank() {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 12)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
let mut panels = WheelPanels {
|
||||
diff_colors: DiffColors::new(Some(LIGHT_DIFF_THEME)),
|
||||
..WheelPanels::default()
|
||||
};
|
||||
for status in [FileStatus::Added, FileStatus::Removed] {
|
||||
let added = status == FileStatus::Added;
|
||||
let mut file = deletion_file();
|
||||
@@ -4067,6 +4145,12 @@ mod tests {
|
||||
terminal.backend().buffer()[(present.x, present.y)].fg,
|
||||
Color::Cyan
|
||||
);
|
||||
for x in present.x + 1..present.right() - 1 {
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(x, present.y + 1)].bg,
|
||||
Color::Reset
|
||||
);
|
||||
}
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains(if added {
|
||||
"ded content"
|
||||
@@ -4150,69 +4234,89 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
const LIGHT_DIFF_THEME: ThemeColors = ThemeColors {
|
||||
background: [250, 245, 240],
|
||||
red: [210, 30, 50],
|
||||
green: [20, 150, 60],
|
||||
};
|
||||
const DARK_DIFF_THEME: ThemeColors = ThemeColors {
|
||||
background: [20, 25, 30],
|
||||
red: [220, 60, 80],
|
||||
green: [40, 180, 100],
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn character_diff_backgrounds_preserve_common_unicode_and_syntax_across_viewports() {
|
||||
let mut diff = diff_comparison(vec![syntax_diff_file(
|
||||
"changes.rs",
|
||||
"let 界e\u{301} = old + old;",
|
||||
"let 界e\u{301} = new + new;",
|
||||
)]);
|
||||
diff.files_visible = false;
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
for (side, base, strong) in [
|
||||
(1, Color::Rgb(64, 24, 24), Color::Rgb(112, 40, 40)),
|
||||
(2, Color::Rgb(24, 48, 32), Color::Rgb(32, 88, 48)),
|
||||
for (theme, row, character) in [
|
||||
(
|
||||
Some(LIGHT_DIFF_THEME),
|
||||
[Color::Rgb(245, 219, 217), Color::Rgb(222, 234, 218)],
|
||||
[Color::Rgb(239, 185, 187), Color::Rgb(186, 218, 190)],
|
||||
),
|
||||
(
|
||||
Some(DARK_DIFF_THEME),
|
||||
[Color::Rgb(44, 29, 36), Color::Rgb(22, 44, 38)],
|
||||
[Color::Rgb(76, 35, 44), Color::Rgb(26, 68, 50)],
|
||||
),
|
||||
(None, [Color::Reset; 2], [Color::Reset; 2]),
|
||||
] {
|
||||
let pane = panels.diff.as_ref().unwrap().regions[side].0;
|
||||
let mut diff = diff_comparison(vec![syntax_diff_file(
|
||||
"changes.rs",
|
||||
"let 界e\u{301} = old + old;",
|
||||
"let 界e\u{301} = new + new;",
|
||||
)]);
|
||||
diff.files_visible = false;
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
|
||||
let mut panels = WheelPanels {
|
||||
diff_colors: DiffColors::new(theme),
|
||||
..WheelPanels::default()
|
||||
};
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
for index in 0..2 {
|
||||
let pane = panels.diff.as_ref().unwrap().regions[index + 1].0;
|
||||
let buffer = terminal.backend().buffer();
|
||||
for column in [0, 3, 4, 6, 8, 14, 19] {
|
||||
assert_eq!(buffer[(pane.x + 4 + column, pane.y + 1)].bg, row[index]);
|
||||
}
|
||||
for column in [10, 11, 12, 16, 17, 18] {
|
||||
let cell = &buffer[(pane.x + 4 + column, pane.y + 1)];
|
||||
assert_eq!(cell.bg, character[index]);
|
||||
assert_eq!(cell.fg, Color::Reset);
|
||||
}
|
||||
assert_eq!(buffer[(pane.x + 4, pane.y + 1)].fg, Color::Magenta);
|
||||
assert_eq!(buffer[(pane.x + 8, pane.y + 1)].symbol(), "界");
|
||||
assert_eq!(buffer[(pane.x + 10, pane.y + 1)].symbol(), "e\u{301}");
|
||||
}
|
||||
diff.left_before = 11;
|
||||
diff.left_after = 11;
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let after = panels.diff.as_ref().unwrap().regions[2].0;
|
||||
let buffer = terminal.backend().buffer();
|
||||
for column in [0, 3, 4, 6, 8, 14, 19] {
|
||||
assert_eq!(buffer[(pane.x + 4 + column, pane.y + 1)].bg, base);
|
||||
}
|
||||
for column in [10, 11, 12, 16, 17, 18] {
|
||||
assert_eq!(buffer[(pane.x + 4 + column, pane.y + 1)].bg, strong);
|
||||
}
|
||||
assert_eq!(buffer[(pane.x + 4, pane.y + 1)].fg, Color::Magenta);
|
||||
assert_eq!(buffer[(pane.x + 8, pane.y + 1)].symbol(), "界");
|
||||
assert_eq!(buffer[(pane.x + 10, pane.y + 1)].symbol(), "e\u{301}");
|
||||
}
|
||||
diff.left_before = 11;
|
||||
diff.left_after = 11;
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let after = panels.diff.as_ref().unwrap().regions[2].0;
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(after.x + 4, after.y + 1)].symbol(),
|
||||
"e"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(after.x + 4, after.y + 1)].bg,
|
||||
Color::Rgb(32, 88, 48)
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(after.x + 7, after.y + 1)].bg,
|
||||
Color::Rgb(24, 48, 32)
|
||||
);
|
||||
diff.word_wrap = true;
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(24, 12)).unwrap();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let after = panels.diff.as_ref().unwrap().regions[2].0;
|
||||
let buffer = terminal.backend().buffer();
|
||||
let mut words = 0;
|
||||
for y in after.y + 1..after.bottom() - 1 {
|
||||
for x in after.x + 4..after.right().saturating_sub(3) {
|
||||
if buffer[(x, y)].symbol() == "n"
|
||||
&& buffer[(x + 1, y)].symbol() == "e"
|
||||
&& buffer[(x + 2, y)].symbol() == "w"
|
||||
{
|
||||
words += 1;
|
||||
for column in x..x + 3 {
|
||||
assert_eq!(buffer[(column, y)].bg, Color::Rgb(32, 88, 48));
|
||||
assert_eq!(buffer[(after.x + 4, after.y + 1)].symbol(), "e");
|
||||
assert_eq!(buffer[(after.x + 4, after.y + 1)].bg, character[1]);
|
||||
assert_eq!(buffer[(after.x + 7, after.y + 1)].bg, row[1]);
|
||||
diff.word_wrap = true;
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(24, 12)).unwrap();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let after = panels.diff.as_ref().unwrap().regions[2].0;
|
||||
let buffer = terminal.backend().buffer();
|
||||
let mut words = 0;
|
||||
for y in after.y + 1..after.bottom() - 1 {
|
||||
for x in after.x + 4..after.right().saturating_sub(3) {
|
||||
if buffer[(x, y)].symbol() == "n"
|
||||
&& buffer[(x + 1, y)].symbol() == "e"
|
||||
&& buffer[(x + 2, y)].symbol() == "w"
|
||||
{
|
||||
words += 1;
|
||||
for column in x..x + 3 {
|
||||
assert_eq!(buffer[(column, y)].bg, character[1]);
|
||||
assert_eq!(buffer[(column, y)].fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(words, 2);
|
||||
}
|
||||
assert_eq!(words, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4307,20 +4411,11 @@ mod tests {
|
||||
assert!(!text.contains(hidden), "unexpected {hidden}: {text}");
|
||||
}
|
||||
let before_area = panels.diff.as_ref().unwrap().regions[1].0;
|
||||
let after_area = panels.diff.as_ref().unwrap().regions[2].0;
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(
|
||||
buffer[(before_area.x + 5, before_area.y + 3)].fg,
|
||||
Color::Magenta
|
||||
);
|
||||
assert_eq!(
|
||||
buffer[(before_area.x + 5, before_area.y + 3)].bg,
|
||||
Color::Rgb(64, 24, 24)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer[(after_area.x + 5, after_area.y + 3)].bg,
|
||||
Color::Rgb(24, 48, 32)
|
||||
);
|
||||
diff.handle(Input::Char('['));
|
||||
diff.handle(Input::Home);
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
@@ -4335,7 +4430,10 @@ mod tests {
|
||||
fn diff_view_aligns_deletion_holes_numbers_colors_and_shared_vertical_scroll() {
|
||||
let mut diff = diff_comparison(vec![deletion_file()]);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
let mut panels = WheelPanels {
|
||||
diff_colors: DiffColors::new(Some(LIGHT_DIFF_THEME)),
|
||||
..WheelPanels::default()
|
||||
};
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let geometry = panels.diff.as_ref().unwrap();
|
||||
let before = geometry.regions[1].0;
|
||||
@@ -4346,7 +4444,6 @@ mod tests {
|
||||
assert_eq!(buffer[(before.x + 4, y)].fg, Color::Reset);
|
||||
assert_eq!(buffer[(before.x + 2, y)].symbol(), "-");
|
||||
for x in before.x + 1..before.right() - 1 {
|
||||
assert_eq!(buffer[(x, y)].bg, Color::Rgb(64, 24, 24));
|
||||
assert_eq!(buffer[(x, y + 1)].bg, Color::Reset);
|
||||
}
|
||||
let gap_symbols: Vec<_> = (after.x + 1..after.right() - 1)
|
||||
@@ -4395,7 +4492,10 @@ mod tests {
|
||||
notice: None,
|
||||
}]);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
let mut panels = WheelPanels {
|
||||
diff_colors: DiffColors::new(Some(LIGHT_DIFF_THEME)),
|
||||
..WheelPanels::default()
|
||||
};
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let geometry = panels.diff.as_ref().unwrap();
|
||||
let before = geometry.regions[1].0;
|
||||
@@ -4414,7 +4514,6 @@ mod tests {
|
||||
assert_eq!(buffer[(after.x + 2, y)].fg, Color::Green);
|
||||
assert_eq!(buffer[(after.x + 4, y)].fg, Color::Reset);
|
||||
for x in after.x + 1..after.right() - 1 {
|
||||
assert_eq!(buffer[(x, y)].bg, Color::Rgb(24, 48, 32));
|
||||
if offset == 1 && x >= after.x + 4 {
|
||||
assert_eq!(buffer[(x, y)].symbol(), " ");
|
||||
}
|
||||
@@ -4506,18 +4605,10 @@ mod tests {
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(before.x + 4, before.y + 1)].symbol(), " ");
|
||||
assert_eq!(buffer[(before.x + 4, before.y + 1)].fg, Color::Reset);
|
||||
assert_eq!(
|
||||
buffer[(before.x + 4, before.y + 1)].bg,
|
||||
Color::Rgb(64, 24, 24)
|
||||
);
|
||||
assert_eq!(buffer[(before.x + 5, before.y + 1)].symbol(), "e\u{301}");
|
||||
assert_eq!(buffer[(before.x + 5, before.y + 1)].fg, Color::Green);
|
||||
assert_eq!(buffer[(after.x + 4, after.y + 1)].symbol(), "界");
|
||||
assert_eq!(buffer[(after.x + 4, after.y + 1)].fg, Color::Green);
|
||||
assert_eq!(
|
||||
buffer[(after.x + 4, after.y + 1)].bg,
|
||||
Color::Rgb(24, 48, 32)
|
||||
);
|
||||
assert_eq!(buffer[(after.x + 6, after.y + 1)].symbol(), "e\u{301}");
|
||||
assert_eq!(buffer[(after.x + 6, after.y + 1)].fg, Color::Green);
|
||||
}
|
||||
@@ -4562,14 +4653,6 @@ mod tests {
|
||||
assert_eq!(buffer[(after.x + 4, after.y + 1)].symbol(), " ");
|
||||
assert_eq!(buffer[(after.x + 8, after.y + 1)].symbol(), "界");
|
||||
assert_eq!(buffer[(after.x + 8, after.y + 1)].fg, Color::Reset);
|
||||
assert_eq!(
|
||||
buffer[(after.x + 8, after.y + 1)].bg,
|
||||
Color::Rgb(24, 48, 32)
|
||||
);
|
||||
assert_eq!(
|
||||
buffer[(before.x + 4, before.y + 1)].bg,
|
||||
Color::Rgb(64, 24, 24)
|
||||
);
|
||||
diff.handle(Input::Char('\t'));
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
assert!(screen_text(&terminal).contains("After focus"));
|
||||
@@ -5516,6 +5599,7 @@ mod tests {
|
||||
assert!(!editor.diff_editing());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 14)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
views.set_theme(Some(LIGHT_DIFF_THEME));
|
||||
let caret = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let before = views.panels.diff.as_ref().unwrap().regions[1].0;
|
||||
assert!(before.contains(caret));
|
||||
@@ -5527,10 +5611,6 @@ mod tests {
|
||||
terminal.backend().buffer()[(caret.x, caret.y)].fg,
|
||||
Color::Magenta
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x, caret.y)].bg,
|
||||
Color::Rgb(64, 24, 24)
|
||||
);
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Right);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
@@ -5594,10 +5674,6 @@ mod tests {
|
||||
display_column(&editor.lines()[editor.cursor().row], editor.cursor().column),
|
||||
column
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(switched.x, switched.y)].bg,
|
||||
Color::Rgb(24, 48, 32)
|
||||
);
|
||||
editor.handle(Input::Right);
|
||||
let moved = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
editor.handle(Input::Char('\t'));
|
||||
@@ -5762,6 +5838,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 14)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
views.set_theme(Some(LIGHT_DIFF_THEME));
|
||||
for _ in 0..4 {
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
@@ -5775,14 +5852,6 @@ mod tests {
|
||||
assert_eq!(cell.symbol(), "v");
|
||||
assert_eq!(cell.fg, color);
|
||||
assert_eq!(cell.modifier.contains(ratatui::style::Modifier::BOLD), bold);
|
||||
assert_eq!(
|
||||
cell.bg,
|
||||
if index == 1 {
|
||||
Color::Rgb(64, 24, 24)
|
||||
} else {
|
||||
Color::Rgb(24, 48, 32)
|
||||
}
|
||||
);
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user