1use crate::model::event::BufferId;
8use serde::{Deserialize, Serialize};
9use std::ops::Range;
10
11#[derive(Debug, Clone)]
13pub struct CompositeBuffer {
14 pub id: BufferId,
16
17 pub name: String,
19
20 pub layout: CompositeLayout,
22
23 pub sources: Vec<SourcePane>,
25
26 pub alignment: LineAlignment,
29
30 pub active_pane: usize,
32
33 pub mode: String,
35
36 pub initial_focus_hunk: Option<usize>,
40}
41
42impl CompositeBuffer {
43 pub fn new(
45 id: BufferId,
46 name: String,
47 mode: String,
48 layout: CompositeLayout,
49 sources: Vec<SourcePane>,
50 ) -> Self {
51 let pane_count = sources.len();
52 Self {
53 id,
54 name,
55 mode,
56 layout,
57 sources,
58 alignment: LineAlignment::empty(pane_count),
59 active_pane: 0,
60 initial_focus_hunk: None,
61 }
62 }
63
64 pub fn pane_count(&self) -> usize {
66 self.sources.len()
67 }
68
69 pub fn get_pane(&self, index: usize) -> Option<&SourcePane> {
71 self.sources.get(index)
72 }
73
74 pub fn focused_pane(&self) -> Option<&SourcePane> {
76 self.sources.get(self.active_pane)
77 }
78
79 pub fn focus_next(&mut self) {
81 if !self.sources.is_empty() {
82 self.active_pane = (self.active_pane + 1) % self.sources.len();
83 }
84 }
85
86 pub fn focus_prev(&mut self) {
88 if !self.sources.is_empty() {
89 self.active_pane = (self.active_pane + self.sources.len() - 1) % self.sources.len();
90 }
91 }
92
93 pub fn set_alignment(&mut self, alignment: LineAlignment) {
95 self.alignment = alignment;
96 }
97
98 pub fn row_count(&self) -> usize {
100 self.alignment.rows.len()
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub enum CompositeLayout {
107 SideBySide {
109 ratios: Vec<f32>,
111 show_separator: bool,
113 },
114 Stacked {
116 spacing: u16,
118 },
119 Unified,
121}
122
123impl Default for CompositeLayout {
124 fn default() -> Self {
125 CompositeLayout::SideBySide {
126 ratios: vec![0.5, 0.5],
127 show_separator: true,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct SourcePane {
135 pub buffer_id: BufferId,
137
138 pub label: String,
140
141 pub editable: bool,
143
144 pub style: PaneStyle,
146
147 pub range: Option<Range<usize>>,
149}
150
151impl SourcePane {
152 pub fn new(buffer_id: BufferId, label: impl Into<String>, editable: bool) -> Self {
154 Self {
155 buffer_id,
156 label: label.into(),
157 editable,
158 style: PaneStyle::default(),
159 range: None,
160 }
161 }
162
163 pub fn with_style(mut self, style: PaneStyle) -> Self {
165 self.style = style;
166 self
167 }
168
169 pub fn with_range(mut self, range: Range<usize>) -> Self {
171 self.range = Some(range);
172 self
173 }
174}
175
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
178pub struct PaneStyle {
179 pub add_bg: Option<(u8, u8, u8)>,
181 pub remove_bg: Option<(u8, u8, u8)>,
183 pub modify_bg: Option<(u8, u8, u8)>,
185 pub gutter_style: GutterStyle,
187}
188
189impl PaneStyle {
190 pub fn old_diff() -> Self {
192 Self {
193 remove_bg: Some((80, 30, 30)),
194 gutter_style: GutterStyle::Both,
195 ..Default::default()
196 }
197 }
198
199 pub fn new_diff() -> Self {
201 Self {
202 add_bg: Some((30, 80, 30)),
203 modify_bg: Some((80, 80, 30)),
204 gutter_style: GutterStyle::Both,
205 ..Default::default()
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
212pub enum GutterStyle {
213 #[default]
215 LineNumbers,
216 DiffMarkers,
218 Both,
220 None,
222}
223
224#[derive(Debug, Clone, Default)]
230pub struct LineAlignment {
231 pub rows: Vec<AlignedRow>,
234}
235
236impl LineAlignment {
237 pub fn empty(_pane_count: usize) -> Self {
239 Self { rows: Vec::new() }
240 }
241
242 pub fn simple(line_count: usize, pane_count: usize) -> Self {
245 let rows = (0..line_count)
246 .map(|line| AlignedRow {
247 pane_lines: (0..pane_count)
248 .map(|_| {
249 Some(SourceLineRef {
250 line,
251 byte_range: 0..0, })
253 })
254 .collect(),
255 row_type: RowType::Context,
256 })
257 .collect();
258 Self { rows }
259 }
260
261 pub fn from_hunks(hunks: &[DiffHunk], old_line_count: usize, new_line_count: usize) -> Self {
263 let mut rows = Vec::new();
264 let mut old_line = 0usize;
265 let mut new_line = 0usize;
266
267 for hunk in hunks {
268 while old_line < hunk.old_start && new_line < hunk.new_start {
270 rows.push(AlignedRow::context(old_line, new_line));
271 old_line += 1;
272 new_line += 1;
273 }
274
275 rows.push(AlignedRow::hunk_header());
277
278 let old_end = hunk.old_start + hunk.old_count;
280 let new_end = hunk.new_start + hunk.new_count;
281
282 let old_hunk_lines = old_end - hunk.old_start;
284 let new_hunk_lines = new_end - hunk.new_start;
285 let max_lines = old_hunk_lines.max(new_hunk_lines);
286
287 for i in 0..max_lines {
288 let old_idx = if i < old_hunk_lines {
289 Some(hunk.old_start + i)
290 } else {
291 None
292 };
293 let new_idx = if i < new_hunk_lines {
294 Some(hunk.new_start + i)
295 } else {
296 None
297 };
298
299 let row_type = match (old_idx, new_idx) {
300 (Some(_), Some(_)) => RowType::Modification,
301 (Some(_), None) => RowType::Deletion,
302 (None, Some(_)) => RowType::Addition,
303 (None, None) => continue,
304 };
305
306 rows.push(AlignedRow {
307 pane_lines: vec![
308 old_idx.map(|l| SourceLineRef {
309 line: l,
310 byte_range: 0..0,
311 }),
312 new_idx.map(|l| SourceLineRef {
313 line: l,
314 byte_range: 0..0,
315 }),
316 ],
317 row_type,
318 });
319 }
320
321 old_line = old_end;
322 new_line = new_end;
323 }
324
325 while old_line < old_line_count && new_line < new_line_count {
327 rows.push(AlignedRow::context(old_line, new_line));
328 old_line += 1;
329 new_line += 1;
330 }
331
332 while old_line < old_line_count {
334 rows.push(AlignedRow {
335 pane_lines: vec![
336 Some(SourceLineRef {
337 line: old_line,
338 byte_range: 0..0,
339 }),
340 None,
341 ],
342 row_type: RowType::Deletion,
343 });
344 old_line += 1;
345 }
346 while new_line < new_line_count {
347 rows.push(AlignedRow {
348 pane_lines: vec![
349 None,
350 Some(SourceLineRef {
351 line: new_line,
352 byte_range: 0..0,
353 }),
354 ],
355 row_type: RowType::Addition,
356 });
357 new_line += 1;
358 }
359
360 Self { rows }
361 }
362
363 pub fn get_row(&self, display_row: usize) -> Option<&AlignedRow> {
365 self.rows.get(display_row)
366 }
367
368 pub fn row_count(&self) -> usize {
370 self.rows.len()
371 }
372
373 pub fn next_hunk_row(&self, after_row: usize) -> Option<usize> {
375 self.rows
376 .iter()
377 .enumerate()
378 .skip(after_row + 1)
379 .find(|(_, row)| row.row_type == RowType::HunkHeader)
380 .map(|(i, _)| i)
381 }
382
383 pub fn prev_hunk_row(&self, before_row: usize) -> Option<usize> {
385 self.rows
386 .iter()
387 .enumerate()
388 .take(before_row)
389 .rev()
390 .find(|(_, row)| row.row_type == RowType::HunkHeader)
391 .map(|(i, _)| i)
392 }
393}
394
395#[derive(Debug, Clone)]
397pub struct AlignedRow {
398 pub pane_lines: Vec<Option<SourceLineRef>>,
400 pub row_type: RowType,
402}
403
404impl AlignedRow {
405 pub fn context(old_line: usize, new_line: usize) -> Self {
407 Self {
408 pane_lines: vec![
409 Some(SourceLineRef {
410 line: old_line,
411 byte_range: 0..0,
412 }),
413 Some(SourceLineRef {
414 line: new_line,
415 byte_range: 0..0,
416 }),
417 ],
418 row_type: RowType::Context,
419 }
420 }
421
422 pub fn hunk_header() -> Self {
424 Self {
425 pane_lines: vec![None, None],
426 row_type: RowType::HunkHeader,
427 }
428 }
429
430 pub fn get_pane_line(&self, pane_index: usize) -> Option<&SourceLineRef> {
432 self.pane_lines.get(pane_index).and_then(|opt| opt.as_ref())
433 }
434
435 pub fn has_content(&self, pane_index: usize) -> bool {
437 self.pane_lines
438 .get(pane_index)
439 .map(|opt| opt.is_some())
440 .unwrap_or(false)
441 }
442}
443
444#[derive(Debug, Clone)]
446pub struct SourceLineRef {
447 pub line: usize,
449 pub byte_range: Range<usize>,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
455pub enum RowType {
456 Context,
458 Deletion,
460 Addition,
462 Modification,
464 HunkHeader,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct DiffHunk {
471 pub old_start: usize,
473 pub old_count: usize,
475 pub new_start: usize,
477 pub new_count: usize,
479 pub header: Option<String>,
481}
482
483impl DiffHunk {
484 pub fn new(old_start: usize, old_count: usize, new_start: usize, new_count: usize) -> Self {
486 Self {
487 old_start,
488 old_count,
489 new_start,
490 new_count,
491 header: None,
492 }
493 }
494
495 pub fn with_header(mut self, header: impl Into<String>) -> Self {
497 self.header = Some(header.into());
498 self
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_line_alignment_from_hunks() {
508 let hunks = vec![DiffHunk::new(2, 2, 2, 3)];
510 let alignment = LineAlignment::from_hunks(&hunks, 5, 6);
511
512 assert!(alignment.rows.len() >= 7);
518
519 assert_eq!(alignment.rows[0].row_type, RowType::Context);
521 assert_eq!(alignment.rows[1].row_type, RowType::Context);
522
523 assert_eq!(alignment.rows[2].row_type, RowType::HunkHeader);
525 }
526
527 #[test]
528 fn test_composite_buffer_focus() {
529 let sources = vec![
530 SourcePane::new(BufferId(1), "OLD", false),
531 SourcePane::new(BufferId(2), "NEW", true),
532 ];
533 let mut composite = CompositeBuffer::new(
534 BufferId(0),
535 "Test".to_string(),
536 "diff-view".to_string(),
537 CompositeLayout::default(),
538 sources,
539 );
540
541 assert_eq!(composite.active_pane, 0);
542
543 composite.focus_next();
544 assert_eq!(composite.active_pane, 1);
545
546 composite.focus_next();
547 assert_eq!(composite.active_pane, 0); composite.focus_prev();
550 assert_eq!(composite.active_pane, 1);
551 }
552}