use crate::model::event::BufferId;
use serde::{Deserialize, Serialize};
use std::ops::Range;
#[derive(Debug, Clone)]
pub struct CompositeBuffer {
pub id: BufferId,
pub name: String,
pub layout: CompositeLayout,
pub sources: Vec<SourcePane>,
pub alignment: LineAlignment,
pub active_pane: usize,
pub mode: String,
}
impl CompositeBuffer {
pub fn new(
id: BufferId,
name: String,
mode: String,
layout: CompositeLayout,
sources: Vec<SourcePane>,
) -> Self {
let pane_count = sources.len();
Self {
id,
name,
mode,
layout,
sources,
alignment: LineAlignment::empty(pane_count),
active_pane: 0,
}
}
pub fn pane_count(&self) -> usize {
self.sources.len()
}
pub fn get_pane(&self, index: usize) -> Option<&SourcePane> {
self.sources.get(index)
}
pub fn focused_pane(&self) -> Option<&SourcePane> {
self.sources.get(self.active_pane)
}
pub fn focus_next(&mut self) {
if !self.sources.is_empty() {
self.active_pane = (self.active_pane + 1) % self.sources.len();
}
}
pub fn focus_prev(&mut self) {
if !self.sources.is_empty() {
self.active_pane = (self.active_pane + self.sources.len() - 1) % self.sources.len();
}
}
pub fn set_alignment(&mut self, alignment: LineAlignment) {
self.alignment = alignment;
}
pub fn row_count(&self) -> usize {
self.alignment.rows.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompositeLayout {
SideBySide {
ratios: Vec<f32>,
show_separator: bool,
},
Stacked {
spacing: u16,
},
Unified,
}
impl Default for CompositeLayout {
fn default() -> Self {
CompositeLayout::SideBySide {
ratios: vec![0.5, 0.5],
show_separator: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourcePane {
pub buffer_id: BufferId,
pub label: String,
pub editable: bool,
pub style: PaneStyle,
pub range: Option<Range<usize>>,
}
impl SourcePane {
pub fn new(buffer_id: BufferId, label: impl Into<String>, editable: bool) -> Self {
Self {
buffer_id,
label: label.into(),
editable,
style: PaneStyle::default(),
range: None,
}
}
pub fn with_style(mut self, style: PaneStyle) -> Self {
self.style = style;
self
}
pub fn with_range(mut self, range: Range<usize>) -> Self {
self.range = Some(range);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PaneStyle {
pub add_bg: Option<(u8, u8, u8)>,
pub remove_bg: Option<(u8, u8, u8)>,
pub modify_bg: Option<(u8, u8, u8)>,
pub gutter_style: GutterStyle,
}
impl PaneStyle {
pub fn old_diff() -> Self {
Self {
remove_bg: Some((80, 30, 30)),
gutter_style: GutterStyle::Both,
..Default::default()
}
}
pub fn new_diff() -> Self {
Self {
add_bg: Some((30, 80, 30)),
modify_bg: Some((80, 80, 30)),
gutter_style: GutterStyle::Both,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum GutterStyle {
#[default]
LineNumbers,
DiffMarkers,
Both,
None,
}
#[derive(Debug, Clone, Default)]
pub struct LineAlignment {
pub rows: Vec<AlignedRow>,
}
impl LineAlignment {
pub fn empty(_pane_count: usize) -> Self {
Self { rows: Vec::new() }
}
pub fn simple(line_count: usize, pane_count: usize) -> Self {
let rows = (0..line_count)
.map(|line| AlignedRow {
pane_lines: (0..pane_count)
.map(|_| {
Some(SourceLineRef {
line,
byte_range: 0..0, })
})
.collect(),
row_type: RowType::Context,
})
.collect();
Self { rows }
}
pub fn from_hunks(hunks: &[DiffHunk], old_line_count: usize, new_line_count: usize) -> Self {
let mut rows = Vec::new();
let mut old_line = 0usize;
let mut new_line = 0usize;
for hunk in hunks {
while old_line < hunk.old_start && new_line < hunk.new_start {
rows.push(AlignedRow::context(old_line, new_line));
old_line += 1;
new_line += 1;
}
rows.push(AlignedRow::hunk_header());
let old_end = hunk.old_start + hunk.old_count;
let new_end = hunk.new_start + hunk.new_count;
let old_hunk_lines = old_end - hunk.old_start;
let new_hunk_lines = new_end - hunk.new_start;
let max_lines = old_hunk_lines.max(new_hunk_lines);
for i in 0..max_lines {
let old_idx = if i < old_hunk_lines {
Some(hunk.old_start + i)
} else {
None
};
let new_idx = if i < new_hunk_lines {
Some(hunk.new_start + i)
} else {
None
};
let row_type = match (old_idx, new_idx) {
(Some(_), Some(_)) => RowType::Modification,
(Some(_), None) => RowType::Deletion,
(None, Some(_)) => RowType::Addition,
(None, None) => continue,
};
rows.push(AlignedRow {
pane_lines: vec![
old_idx.map(|l| SourceLineRef {
line: l,
byte_range: 0..0,
}),
new_idx.map(|l| SourceLineRef {
line: l,
byte_range: 0..0,
}),
],
row_type,
});
}
old_line = old_end;
new_line = new_end;
}
while old_line < old_line_count && new_line < new_line_count {
rows.push(AlignedRow::context(old_line, new_line));
old_line += 1;
new_line += 1;
}
while old_line < old_line_count {
rows.push(AlignedRow {
pane_lines: vec![
Some(SourceLineRef {
line: old_line,
byte_range: 0..0,
}),
None,
],
row_type: RowType::Deletion,
});
old_line += 1;
}
while new_line < new_line_count {
rows.push(AlignedRow {
pane_lines: vec![
None,
Some(SourceLineRef {
line: new_line,
byte_range: 0..0,
}),
],
row_type: RowType::Addition,
});
new_line += 1;
}
Self { rows }
}
pub fn get_row(&self, display_row: usize) -> Option<&AlignedRow> {
self.rows.get(display_row)
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn next_hunk_row(&self, after_row: usize) -> Option<usize> {
self.rows
.iter()
.enumerate()
.skip(after_row + 1)
.find(|(_, row)| row.row_type == RowType::HunkHeader)
.map(|(i, _)| i)
}
pub fn prev_hunk_row(&self, before_row: usize) -> Option<usize> {
self.rows
.iter()
.enumerate()
.take(before_row)
.rev()
.find(|(_, row)| row.row_type == RowType::HunkHeader)
.map(|(i, _)| i)
}
}
#[derive(Debug, Clone)]
pub struct AlignedRow {
pub pane_lines: Vec<Option<SourceLineRef>>,
pub row_type: RowType,
}
impl AlignedRow {
pub fn context(old_line: usize, new_line: usize) -> Self {
Self {
pane_lines: vec![
Some(SourceLineRef {
line: old_line,
byte_range: 0..0,
}),
Some(SourceLineRef {
line: new_line,
byte_range: 0..0,
}),
],
row_type: RowType::Context,
}
}
pub fn hunk_header() -> Self {
Self {
pane_lines: vec![None, None],
row_type: RowType::HunkHeader,
}
}
pub fn get_pane_line(&self, pane_index: usize) -> Option<&SourceLineRef> {
self.pane_lines.get(pane_index).and_then(|opt| opt.as_ref())
}
pub fn has_content(&self, pane_index: usize) -> bool {
self.pane_lines
.get(pane_index)
.map(|opt| opt.is_some())
.unwrap_or(false)
}
}
#[derive(Debug, Clone)]
pub struct SourceLineRef {
pub line: usize,
pub byte_range: Range<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RowType {
Context,
Deletion,
Addition,
Modification,
HunkHeader,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffHunk {
pub old_start: usize,
pub old_count: usize,
pub new_start: usize,
pub new_count: usize,
pub header: Option<String>,
}
impl DiffHunk {
pub fn new(old_start: usize, old_count: usize, new_start: usize, new_count: usize) -> Self {
Self {
old_start,
old_count,
new_start,
new_count,
header: None,
}
}
pub fn with_header(mut self, header: impl Into<String>) -> Self {
self.header = Some(header.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_alignment_from_hunks() {
let hunks = vec![DiffHunk::new(2, 2, 2, 3)];
let alignment = LineAlignment::from_hunks(&hunks, 5, 6);
assert!(alignment.rows.len() >= 7);
assert_eq!(alignment.rows[0].row_type, RowType::Context);
assert_eq!(alignment.rows[1].row_type, RowType::Context);
assert_eq!(alignment.rows[2].row_type, RowType::HunkHeader);
}
#[test]
fn test_composite_buffer_focus() {
let sources = vec![
SourcePane::new(BufferId(1), "OLD", false),
SourcePane::new(BufferId(2), "NEW", true),
];
let mut composite = CompositeBuffer::new(
BufferId(0),
"Test".to_string(),
"diff-view".to_string(),
CompositeLayout::default(),
sources,
);
assert_eq!(composite.active_pane, 0);
composite.focus_next();
assert_eq!(composite.active_pane, 1);
composite.focus_next();
assert_eq!(composite.active_pane, 0);
composite.focus_prev();
assert_eq!(composite.active_pane, 1);
}
}