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 if let Some(ops) = hunk.ops.as_deref() {
283 let mut o = hunk.old_start;
288 let mut n = hunk.new_start;
289 for op in ops.chars() {
290 match op {
291 ' ' => {
292 rows.push(AlignedRow::context(o, n));
293 o += 1;
294 n += 1;
295 }
296 '-' => {
297 rows.push(AlignedRow {
298 pane_lines: vec![
299 Some(SourceLineRef {
300 line: o,
301 byte_range: 0..0,
302 }),
303 None,
304 ],
305 row_type: RowType::Deletion,
306 });
307 o += 1;
308 }
309 '+' => {
310 rows.push(AlignedRow {
311 pane_lines: vec![
312 None,
313 Some(SourceLineRef {
314 line: n,
315 byte_range: 0..0,
316 }),
317 ],
318 row_type: RowType::Addition,
319 });
320 n += 1;
321 }
322 _ => {} }
324 }
325 } else {
326 let old_hunk_lines = old_end - hunk.old_start;
328 let new_hunk_lines = new_end - hunk.new_start;
329 let max_lines = old_hunk_lines.max(new_hunk_lines);
330
331 for i in 0..max_lines {
332 let old_idx = if i < old_hunk_lines {
333 Some(hunk.old_start + i)
334 } else {
335 None
336 };
337 let new_idx = if i < new_hunk_lines {
338 Some(hunk.new_start + i)
339 } else {
340 None
341 };
342
343 let row_type = match (old_idx, new_idx) {
344 (Some(_), Some(_)) => RowType::Modification,
345 (Some(_), None) => RowType::Deletion,
346 (None, Some(_)) => RowType::Addition,
347 (None, None) => continue,
348 };
349
350 rows.push(AlignedRow {
351 pane_lines: vec![
352 old_idx.map(|l| SourceLineRef {
353 line: l,
354 byte_range: 0..0,
355 }),
356 new_idx.map(|l| SourceLineRef {
357 line: l,
358 byte_range: 0..0,
359 }),
360 ],
361 row_type,
362 });
363 }
364 }
365
366 old_line = old_end;
367 new_line = new_end;
368 }
369
370 while old_line < old_line_count && new_line < new_line_count {
372 rows.push(AlignedRow::context(old_line, new_line));
373 old_line += 1;
374 new_line += 1;
375 }
376
377 while old_line < old_line_count {
379 rows.push(AlignedRow {
380 pane_lines: vec![
381 Some(SourceLineRef {
382 line: old_line,
383 byte_range: 0..0,
384 }),
385 None,
386 ],
387 row_type: RowType::Deletion,
388 });
389 old_line += 1;
390 }
391 while new_line < new_line_count {
392 rows.push(AlignedRow {
393 pane_lines: vec![
394 None,
395 Some(SourceLineRef {
396 line: new_line,
397 byte_range: 0..0,
398 }),
399 ],
400 row_type: RowType::Addition,
401 });
402 new_line += 1;
403 }
404
405 Self { rows }
406 }
407
408 pub fn get_row(&self, display_row: usize) -> Option<&AlignedRow> {
410 self.rows.get(display_row)
411 }
412
413 pub fn row_count(&self) -> usize {
415 self.rows.len()
416 }
417
418 pub fn next_hunk_row(&self, after_row: usize) -> Option<usize> {
420 self.rows
421 .iter()
422 .enumerate()
423 .skip(after_row + 1)
424 .find(|(_, row)| row.row_type == RowType::HunkHeader)
425 .map(|(i, _)| i)
426 }
427
428 pub fn prev_hunk_row(&self, before_row: usize) -> Option<usize> {
430 self.rows
431 .iter()
432 .enumerate()
433 .take(before_row)
434 .rev()
435 .find(|(_, row)| row.row_type == RowType::HunkHeader)
436 .map(|(i, _)| i)
437 }
438}
439
440#[derive(Debug, Clone)]
442pub struct AlignedRow {
443 pub pane_lines: Vec<Option<SourceLineRef>>,
445 pub row_type: RowType,
447}
448
449impl AlignedRow {
450 pub fn context(old_line: usize, new_line: usize) -> Self {
452 Self {
453 pane_lines: vec![
454 Some(SourceLineRef {
455 line: old_line,
456 byte_range: 0..0,
457 }),
458 Some(SourceLineRef {
459 line: new_line,
460 byte_range: 0..0,
461 }),
462 ],
463 row_type: RowType::Context,
464 }
465 }
466
467 pub fn hunk_header() -> Self {
469 Self {
470 pane_lines: vec![None, None],
471 row_type: RowType::HunkHeader,
472 }
473 }
474
475 pub fn get_pane_line(&self, pane_index: usize) -> Option<&SourceLineRef> {
477 self.pane_lines.get(pane_index).and_then(|opt| opt.as_ref())
478 }
479
480 pub fn has_content(&self, pane_index: usize) -> bool {
482 self.pane_lines
483 .get(pane_index)
484 .map(|opt| opt.is_some())
485 .unwrap_or(false)
486 }
487}
488
489#[derive(Debug, Clone)]
491pub struct SourceLineRef {
492 pub line: usize,
494 pub byte_range: Range<usize>,
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
500pub enum RowType {
501 Context,
503 Deletion,
505 Addition,
507 Modification,
509 HunkHeader,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct DiffHunk {
516 pub old_start: usize,
518 pub old_count: usize,
520 pub new_start: usize,
522 pub new_count: usize,
524 pub header: Option<String>,
526 pub ops: Option<String>,
530}
531
532impl DiffHunk {
533 pub fn new(old_start: usize, old_count: usize, new_start: usize, new_count: usize) -> Self {
535 Self {
536 old_start,
537 old_count,
538 new_start,
539 new_count,
540 header: None,
541 ops: None,
542 }
543 }
544
545 pub fn with_ops(mut self, ops: Option<String>) -> Self {
547 self.ops = ops;
548 self
549 }
550
551 pub fn with_header(mut self, header: impl Into<String>) -> Self {
553 self.header = Some(header.into());
554 self
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn test_line_alignment_from_hunks() {
564 let hunks = vec![DiffHunk::new(2, 2, 2, 3)];
566 let alignment = LineAlignment::from_hunks(&hunks, 5, 6);
567
568 assert!(alignment.rows.len() >= 7);
574
575 assert_eq!(alignment.rows[0].row_type, RowType::Context);
577 assert_eq!(alignment.rows[1].row_type, RowType::Context);
578
579 assert_eq!(alignment.rows[2].row_type, RowType::HunkHeader);
581 }
582
583 #[test]
584 fn test_composite_buffer_focus() {
585 let sources = vec![
586 SourcePane::new(BufferId(1), "OLD", false),
587 SourcePane::new(BufferId(2), "NEW", true),
588 ];
589 let mut composite = CompositeBuffer::new(
590 BufferId(0),
591 "Test".to_string(),
592 "diff-view".to_string(),
593 CompositeLayout::default(),
594 sources,
595 );
596
597 assert_eq!(composite.active_pane, 0);
598
599 composite.focus_next();
600 assert_eq!(composite.active_pane, 1);
601
602 composite.focus_next();
603 assert_eq!(composite.active_pane, 0); composite.focus_prev();
606 assert_eq!(composite.active_pane, 1);
607 }
608}