fix: blend diff highlights with terminal theme colors
This commit is contained in:
@@ -46,10 +46,12 @@ Frontends borrow `snippet_selections()` for the active argument's end-exclusive
|
||||
|
||||
## Terminal lifecycle and layout
|
||||
|
||||
The terminal adapter owns raw mode, alternate-screen state, bracketed paste, mouse capture, and its panic hook for one session lifetime. Entry failure, normal return, I/O errors, and unwinding panics restore the caller's hook; active-terminal cleanup runs before caller panic diagnostics. Panics retain their original payload and propagate rather than being swallowed. Terminal state and panic hooks are process-global: run one TUI at a time and do not replace its hook while that session is active.
|
||||
The terminal adapter owns raw mode, alternate-screen state, bracketed paste, mouse capture, focus reporting, and its panic hook for one session lifetime. Entry failure, normal return, I/O errors, and unwinding panics restore the caller's hook; active-terminal cleanup runs before caller panic diagnostics. Panics retain their original payload and propagate rather than being swallowed. Terminal state and panic hooks are process-global: run one TUI at a time and do not replace its hook while that session is active.
|
||||
|
||||
Redraws stage Ratatui's buffer diff, cursor placement, and DEC synchronized-update commands (mode 2026) in a reusable byte buffer, then write and flush the complete frame to the terminal. Frame computation never suspends terminal presentation; synchronization covers transmitted painting rather than layout/highlighting work. The hardware cursor is hidden before painting even when synchronization is unsupported. Terminal cleanup releases synchronization before leaving the alternate screen or invoking caller panic diagnostics. Backend-writer access is enabled solely for this staging seam.
|
||||
|
||||
`terminal_colors` intercepts crossterm's key-shaped OSC 11/4 replies before editor dispatch, assembles a bounded RGB background/red/green triple, and preserves unrelated events and opaque bracketed paste. Queries never block input and start after painting, so syntax initialization does not spend their reply deadline. Startup and focus-regain queries replace cached diff tints atomically; an unanswered/partial query expires after 250 ms and removes stale tints rather than assuming a dark theme. Valid late replies are consumed without becoming editing commands. The frontend precomputes rounded RGB blends from the original background (12% row, 28% character), propagating them to active, parked, and newly created panes.
|
||||
|
||||
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row status bar, and a one-row prompt/message area. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three rows the status bar is hidden; at one row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
|
||||
|
||||
Mouse wheel input uses the last rendered document/panel rectangles for hit-testing. Document offsets and explorer scroll state remain frontend-owned; three-row wheel steps never become document cursor motions. Manual document views persist until logical caret-reveal, caret/selection, mode, revision, or buffer changes; offscreen primary and browsing-selection cursors are not placed. Hover wheel input reuses the existing editor-owned popup scrolling.
|
||||
|
||||
@@ -300,6 +300,8 @@ In split diffs, changed text rows use muted red backgrounds in Before and muted
|
||||
|
||||
Character-wise highlighting is enabled automatically within paired changed lines: only changed graphemes receive a stronger red/green background, while common text retains the muted row tint. Combining sequences, wide characters, and expanded tabs keep their source geometry through horizontal scrolling and word wrap. Syntax/semantic foregrounds remain intact, and cursor/search/selection styles keep their normal precedence. Character ranges are cached with aligned rows; stable live edits refresh only the edited paired lines rather than computing character diffs during painting.
|
||||
|
||||
Diff backgrounds blend the terminal's default background with its red/green palette colors: a subtle 12% accent for changed rows and a stronger 28% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint and again on terminal focus regain; cached colors apply across editor panes. If you switch terminal themes while vamp remains focused, switch away and back to refresh the tints. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.
|
||||
|
||||
Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column. Unlike the text slash `/`, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched.
|
||||
|
||||
In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input in either split synchronizes the Before/After horizontal offsets, bounded by the longer side, without changing focus, cursor, or selections; over Files it is consumed. Added/deleted files retain single-pane scrolling. Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; **it never closes the diff**. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+164
-95
@@ -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,8 +4234,32 @@ 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() {
|
||||
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 mut diff = diff_comparison(vec![syntax_diff_file(
|
||||
"changes.rs",
|
||||
"let 界e\u{301} = old + old;",
|
||||
@@ -4159,19 +4267,21 @@ mod tests {
|
||||
)]);
|
||||
diff.files_visible = false;
|
||||
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(theme),
|
||||
..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)),
|
||||
] {
|
||||
let pane = panels.diff.as_ref().unwrap().regions[side].0;
|
||||
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, base);
|
||||
assert_eq!(buffer[(pane.x + 4 + column, pane.y + 1)].bg, row[index]);
|
||||
}
|
||||
for column in [10, 11, 12, 16, 17, 18] {
|
||||
assert_eq!(buffer[(pane.x + 4 + column, pane.y + 1)].bg, strong);
|
||||
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(), "界");
|
||||
@@ -4181,18 +4291,10 @@ mod tests {
|
||||
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)
|
||||
);
|
||||
let buffer = terminal.backend().buffer();
|
||||
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);
|
||||
@@ -4207,13 +4309,15 @@ mod tests {
|
||||
{
|
||||
words += 1;
|
||||
for column in x..x + 3 {
|
||||
assert_eq!(buffer[(column, y)].bg, Color::Rgb(32, 88, 48));
|
||||
assert_eq!(buffer[(column, y)].bg, character[1]);
|
||||
assert_eq!(buffer[(column, y)].fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(words, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_picker_keeps_working_pinned_while_windowing_searching_and_changing_stage() {
|
||||
@@ -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