pub mod parser;
mod render;
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
use crate::scroll::ScrollState;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum DiffMode {
#[default]
Unified,
SideBySide,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum DiffLineType {
Context,
Added,
Removed,
Header,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct DiffLine {
pub line_type: DiffLineType,
pub content: String,
pub old_line_num: Option<usize>,
pub new_line_num: Option<usize>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct DiffHunk {
pub header: String,
pub old_start: usize,
pub new_start: usize,
pub lines: Vec<DiffLine>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DiffViewerMessage {
SetDiff(String),
SetTexts {
old: String,
new: String,
},
SetHunks(Vec<DiffHunk>),
Clear,
NextHunk,
PrevHunk,
ScrollUp,
ScrollDown,
PageUp(usize),
PageDown(usize),
Home,
End,
ToggleMode,
SetMode(DiffMode),
}
impl Eq for DiffViewerMessage {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiffViewerOutput {
HunkChanged(usize),
ModeChanged(DiffMode),
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct DiffViewerState {
hunks: Vec<DiffHunk>,
mode: DiffMode,
pub(crate) scroll: ScrollState,
current_hunk: usize,
context_lines: usize,
pub(crate) show_line_numbers: bool,
pub(crate) title: Option<String>,
pub(crate) old_label: Option<String>,
pub(crate) new_label: Option<String>,
}
impl Default for DiffViewerState {
fn default() -> Self {
Self {
hunks: Vec::new(),
mode: DiffMode::default(),
scroll: ScrollState::default(),
current_hunk: 0,
context_lines: 3,
show_line_numbers: true,
title: None,
old_label: None,
new_label: None,
}
}
}
impl PartialEq for DiffViewerState {
fn eq(&self, other: &Self) -> bool {
self.hunks == other.hunks
&& self.mode == other.mode
&& self.scroll == other.scroll
&& self.current_hunk == other.current_hunk
&& self.context_lines == other.context_lines
&& self.show_line_numbers == other.show_line_numbers
&& self.title == other.title
&& self.old_label == other.old_label
&& self.new_label == other.new_label
}
}
impl DiffViewerState {
pub fn new() -> Self {
Self::default()
}
pub fn from_diff(diff_text: &str) -> Self {
let hunks = parser::parse_unified_diff(diff_text);
let total_lines = count_total_lines(&hunks);
Self {
hunks,
scroll: ScrollState::new(total_lines),
..Default::default()
}
}
pub fn from_texts(old: &str, new: &str) -> Self {
let context_lines = 3;
let hunks = parser::compute_diff(old, new, context_lines);
let total_lines = count_total_lines(&hunks);
Self {
hunks,
context_lines,
scroll: ScrollState::new(total_lines),
..Default::default()
}
}
pub fn with_mode(mut self, mode: DiffMode) -> Self {
self.mode = mode;
self
}
pub fn with_context_lines(mut self, lines: usize) -> Self {
self.context_lines = lines;
self
}
pub fn with_line_numbers(mut self, show: bool) -> Self {
self.show_line_numbers = show;
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_old_label(mut self, label: impl Into<String>) -> Self {
self.old_label = Some(label.into());
self
}
pub fn with_new_label(mut self, label: impl Into<String>) -> Self {
self.new_label = Some(label.into());
self
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = Some(title.into());
}
pub fn hunks(&self) -> &[DiffHunk] {
&self.hunks
}
pub fn hunk_count(&self) -> usize {
self.hunks.len()
}
pub fn current_hunk(&self) -> usize {
self.current_hunk
}
pub fn total_lines(&self) -> usize {
count_total_lines(&self.hunks)
}
pub fn added_count(&self) -> usize {
self.hunks
.iter()
.flat_map(|h| &h.lines)
.filter(|l| l.line_type == DiffLineType::Added)
.count()
}
pub fn removed_count(&self) -> usize {
self.hunks
.iter()
.flat_map(|h| &h.lines)
.filter(|l| l.line_type == DiffLineType::Removed)
.count()
}
pub fn changed_count(&self) -> usize {
self.added_count() + self.removed_count()
}
pub fn show_line_numbers(&self) -> bool {
self.show_line_numbers
}
pub fn set_show_line_numbers(&mut self, show: bool) {
self.show_line_numbers = show;
}
pub fn context_lines(&self) -> usize {
self.context_lines
}
pub fn old_label(&self) -> Option<&str> {
self.old_label.as_deref()
}
pub fn set_old_label(&mut self, label: impl Into<String>) {
self.old_label = Some(label.into());
}
pub fn new_label(&self) -> Option<&str> {
self.new_label.as_deref()
}
pub fn set_new_label(&mut self, label: impl Into<String>) {
self.new_label = Some(label.into());
}
pub fn mode(&self) -> &DiffMode {
&self.mode
}
pub fn scroll_offset(&self) -> usize {
self.scroll.offset()
}
pub fn update(&mut self, msg: DiffViewerMessage) -> Option<DiffViewerOutput> {
DiffViewer::update(self, msg)
}
pub(crate) fn collect_display_lines(&self) -> Vec<DiffLine> {
self.hunks
.iter()
.flat_map(|h| h.lines.iter().cloned())
.collect()
}
pub(crate) fn collect_side_by_side_pairs(&self) -> Vec<(Option<DiffLine>, Option<DiffLine>)> {
let mut pairs = Vec::new();
for hunk in &self.hunks {
let header = hunk.lines.first().cloned();
if let Some(ref h) = header {
if h.line_type == DiffLineType::Header {
pairs.push((Some(h.clone()), Some(h.clone())));
}
}
let content_lines: Vec<_> = hunk
.lines
.iter()
.filter(|l| l.line_type != DiffLineType::Header)
.collect();
let mut i = 0;
while i < content_lines.len() {
match content_lines[i].line_type {
DiffLineType::Context => {
pairs.push((
Some(content_lines[i].clone()),
Some(content_lines[i].clone()),
));
i += 1;
}
DiffLineType::Removed => {
let mut removed = Vec::new();
while i < content_lines.len()
&& content_lines[i].line_type == DiffLineType::Removed
{
removed.push(content_lines[i].clone());
i += 1;
}
let mut added = Vec::new();
while i < content_lines.len()
&& content_lines[i].line_type == DiffLineType::Added
{
added.push(content_lines[i].clone());
i += 1;
}
let max_len = removed.len().max(added.len());
for j in 0..max_len {
let left = removed.get(j).cloned();
let right = added.get(j).cloned();
pairs.push((left, right));
}
}
DiffLineType::Added => {
pairs.push((None, Some(content_lines[i].clone())));
i += 1;
}
DiffLineType::Header => {
i += 1; }
}
}
}
pairs
}
fn scroll_offset_for_hunk(&self, hunk_idx: usize) -> usize {
let mut offset = 0;
for hunk in self.hunks.iter().take(hunk_idx) {
offset += hunk.lines.len();
}
offset
}
}
fn count_total_lines(hunks: &[DiffHunk]) -> usize {
hunks.iter().map(|h| h.lines.len()).sum()
}
pub struct DiffViewer;
impl Component for DiffViewer {
type State = DiffViewerState;
type Message = DiffViewerMessage;
type Output = DiffViewerOutput;
fn init() -> Self::State {
DiffViewerState::default()
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
let key = event.as_key()?;
let ctrl = key.modifiers.ctrl();
let shift = key.modifiers.shift();
match key.code {
Key::Up | Key::Char('k') if !ctrl => Some(DiffViewerMessage::ScrollUp),
Key::Down | Key::Char('j') if !ctrl => Some(DiffViewerMessage::ScrollDown),
Key::Char('n') if !shift && !ctrl => Some(DiffViewerMessage::NextHunk),
Key::Char('n') if key.modifiers.shift() => Some(DiffViewerMessage::PrevHunk),
Key::Char('p') if !ctrl => Some(DiffViewerMessage::PrevHunk),
Key::PageUp => Some(DiffViewerMessage::PageUp(10)),
Key::PageDown => Some(DiffViewerMessage::PageDown(10)),
Key::Char('u') if ctrl => Some(DiffViewerMessage::PageUp(10)),
Key::Char('d') if ctrl => Some(DiffViewerMessage::PageDown(10)),
Key::Char('g') if key.modifiers.shift() => Some(DiffViewerMessage::End),
Key::Home | Key::Char('g') => Some(DiffViewerMessage::Home),
Key::End => Some(DiffViewerMessage::End),
Key::Char('m') if !ctrl => Some(DiffViewerMessage::ToggleMode),
_ => None,
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
DiffViewerMessage::SetDiff(text) => {
state.hunks = parser::parse_unified_diff(&text);
state.current_hunk = 0;
state.scroll = ScrollState::new(count_total_lines(&state.hunks));
None
}
DiffViewerMessage::SetTexts { old, new } => {
state.hunks = parser::compute_diff(&old, &new, state.context_lines);
state.current_hunk = 0;
state.scroll = ScrollState::new(count_total_lines(&state.hunks));
None
}
DiffViewerMessage::SetHunks(hunks) => {
state.hunks = hunks;
state.current_hunk = 0;
state.scroll = ScrollState::new(count_total_lines(&state.hunks));
None
}
DiffViewerMessage::Clear => {
state.hunks.clear();
state.current_hunk = 0;
state.scroll = ScrollState::new(0);
None
}
DiffViewerMessage::NextHunk => {
if state.hunks.is_empty() {
return None;
}
let new_hunk = if state.current_hunk + 1 < state.hunks.len() {
state.current_hunk + 1
} else {
0 };
if new_hunk != state.current_hunk {
state.current_hunk = new_hunk;
let offset = state.scroll_offset_for_hunk(new_hunk);
state.scroll.set_offset(offset);
Some(DiffViewerOutput::HunkChanged(new_hunk))
} else {
None
}
}
DiffViewerMessage::PrevHunk => {
if state.hunks.is_empty() {
return None;
}
let new_hunk = if state.current_hunk > 0 {
state.current_hunk - 1
} else {
state.hunks.len() - 1 };
if new_hunk != state.current_hunk {
state.current_hunk = new_hunk;
let offset = state.scroll_offset_for_hunk(new_hunk);
state.scroll.set_offset(offset);
Some(DiffViewerOutput::HunkChanged(new_hunk))
} else {
None
}
}
DiffViewerMessage::ScrollUp => {
if state.scroll.scroll_up() {
update_current_hunk_from_scroll(state);
None
} else {
None
}
}
DiffViewerMessage::ScrollDown => {
if state.scroll.scroll_down() {
update_current_hunk_from_scroll(state);
None
} else {
None
}
}
DiffViewerMessage::PageUp(n) => {
if state.scroll.page_up(n) {
update_current_hunk_from_scroll(state);
None
} else {
None
}
}
DiffViewerMessage::PageDown(n) => {
if state.scroll.page_down(n) {
update_current_hunk_from_scroll(state);
None
} else {
None
}
}
DiffViewerMessage::Home => {
state.scroll.scroll_to_start();
state.current_hunk = 0;
None
}
DiffViewerMessage::End => {
state.scroll.scroll_to_end();
if !state.hunks.is_empty() {
state.current_hunk = state.hunks.len() - 1;
}
None
}
DiffViewerMessage::ToggleMode => {
state.mode = match state.mode {
DiffMode::Unified => DiffMode::SideBySide,
DiffMode::SideBySide => DiffMode::Unified,
};
Some(DiffViewerOutput::ModeChanged(state.mode.clone()))
}
DiffViewerMessage::SetMode(mode) => {
if state.mode != mode {
state.mode = mode.clone();
Some(DiffViewerOutput::ModeChanged(mode))
} else {
None
}
}
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
fn update_current_hunk_from_scroll(state: &mut DiffViewerState) {
let offset = state.scroll.offset();
let mut cumulative = 0;
for (i, hunk) in state.hunks.iter().enumerate() {
cumulative += hunk.lines.len();
if offset < cumulative {
state.current_hunk = i;
return;
}
}
if !state.hunks.is_empty() {
state.current_hunk = state.hunks.len() - 1;
}
}
#[cfg(test)]
mod tests;