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
37impl CompositeBuffer {
38 pub fn new(
40 id: BufferId,
41 name: String,
42 mode: String,
43 layout: CompositeLayout,
44 sources: Vec<SourcePane>,
45 ) -> Self {
46 let pane_count = sources.len();
47 Self {
48 id,
49 name,
50 mode,
51 layout,
52 sources,
53 alignment: LineAlignment::empty(pane_count),
54 active_pane: 0,
55 }
56 }
57
58 pub fn pane_count(&self) -> usize {
60 self.sources.len()
61 }
62
63 pub fn get_pane(&self, index: usize) -> Option<&SourcePane> {
65 self.sources.get(index)
66 }
67
68 pub fn focused_pane(&self) -> Option<&SourcePane> {
70 self.sources.get(self.active_pane)
71 }
72
73 pub fn focus_next(&mut self) {
75 if !self.sources.is_empty() {
76 self.active_pane = (self.active_pane + 1) % self.sources.len();
77 }
78 }
79
80 pub fn focus_prev(&mut self) {
82 if !self.sources.is_empty() {
83 self.active_pane = (self.active_pane + self.sources.len() - 1) % self.sources.len();
84 }
85 }
86
87 pub fn set_alignment(&mut self, alignment: LineAlignment) {
89 self.alignment = alignment;
90 }
91
92 pub fn row_count(&self) -> usize {
94 self.alignment.rows.len()
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub enum CompositeLayout {
101 SideBySide {
103 ratios: Vec<f32>,
105 show_separator: bool,
107 },
108 Stacked {
110 spacing: u16,
112 },
113 Unified,
115}
116
117impl Default for CompositeLayout {
118 fn default() -> Self {
119 CompositeLayout::SideBySide {
120 ratios: vec![0.5, 0.5],
121 show_separator: true,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SourcePane {
129 pub buffer_id: BufferId,
131
132 pub label: String,
134
135 pub editable: bool,
137
138 pub style: PaneStyle,
140
141 pub range: Option<Range<usize>>,
143}
144
145impl SourcePane {
146 pub fn new(buffer_id: BufferId, label: impl Into<String>, editable: bool) -> Self {
148 Self {
149 buffer_id,
150 label: label.into(),
151 editable,
152 style: PaneStyle::default(),
153 range: None,
154 }
155 }
156
157 pub fn with_style(mut self, style: PaneStyle) -> Self {
159 self.style = style;
160 self
161 }
162
163 pub fn with_range(mut self, range: Range<usize>) -> Self {
165 self.range = Some(range);
166 self
167 }
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize)]
172pub struct PaneStyle {
173 pub add_bg: Option<(u8, u8, u8)>,
175 pub remove_bg: Option<(u8, u8, u8)>,
177 pub modify_bg: Option<(u8, u8, u8)>,
179 pub gutter_style: GutterStyle,
181}
182
183impl PaneStyle {
184 pub fn old_diff() -> Self {
186 Self {
187 remove_bg: Some((80, 30, 30)),
188 gutter_style: GutterStyle::Both,
189 ..Default::default()
190 }
191 }
192
193 pub fn new_diff() -> Self {
195 Self {
196 add_bg: Some((30, 80, 30)),
197 modify_bg: Some((80, 80, 30)),
198 gutter_style: GutterStyle::Both,
199 ..Default::default()
200 }
201 }
202}
203
204#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
206pub enum GutterStyle {
207 #[default]
209 LineNumbers,
210 DiffMarkers,
212 Both,
214 None,
216}
217
218#[derive(Debug, Clone, Default)]
224pub struct LineAlignment {
225 pub rows: Vec<AlignedRow>,
228}
229
230impl LineAlignment {
231 pub fn empty(_pane_count: usize) -> Self {
233 Self { rows: Vec::new() }
234 }
235
236 pub fn simple(line_count: usize, pane_count: usize) -> Self {
239 let rows = (0..line_count)
240 .map(|line| AlignedRow {
241 pane_lines: (0..pane_count)
242 .map(|_| {
243 Some(SourceLineRef {
244 line,
245 byte_range: 0..0, })
247 })
248 .collect(),
249 row_type: RowType::Context,
250 })
251 .collect();
252 Self { rows }
253 }
254
255 pub fn from_hunks(hunks: &[DiffHunk], old_line_count: usize, new_line_count: usize) -> Self {
257 let mut rows = Vec::new();
258 let mut old_line = 0usize;
259 let mut new_line = 0usize;
260
261 for hunk in hunks {
262 while old_line < hunk.old_start && new_line < hunk.new_start {
264 rows.push(AlignedRow::context(old_line, new_line));
265 old_line += 1;
266 new_line += 1;
267 }
268
269 rows.push(AlignedRow::hunk_header());
271
272 let old_end = hunk.old_start + hunk.old_count;
274 let new_end = hunk.new_start + hunk.new_count;
275
276 let old_hunk_lines = old_end - hunk.old_start;
278 let new_hunk_lines = new_end - hunk.new_start;
279 let max_lines = old_hunk_lines.max(new_hunk_lines);
280
281 for i in 0..max_lines {
282 let old_idx = if i < old_hunk_lines {
283 Some(hunk.old_start + i)
284 } else {
285 None
286 };
287 let new_idx = if i < new_hunk_lines {
288 Some(hunk.new_start + i)
289 } else {
290 None
291 };
292
293 let row_type = match (old_idx, new_idx) {
294 (Some(_), Some(_)) => RowType::Modification,
295 (Some(_), None) => RowType::Deletion,
296 (None, Some(_)) => RowType::Addition,
297 (None, None) => continue,
298 };
299
300 rows.push(AlignedRow {
301 pane_lines: vec![
302 old_idx.map(|l| SourceLineRef {
303 line: l,
304 byte_range: 0..0,
305 }),
306 new_idx.map(|l| SourceLineRef {
307 line: l,
308 byte_range: 0..0,
309 }),
310 ],
311 row_type,
312 });
313 }
314
315 old_line = old_end;
316 new_line = new_end;
317 }
318
319 while old_line < old_line_count && new_line < new_line_count {
321 rows.push(AlignedRow::context(old_line, new_line));
322 old_line += 1;
323 new_line += 1;
324 }
325
326 while old_line < old_line_count {
328 rows.push(AlignedRow {
329 pane_lines: vec![
330 Some(SourceLineRef {
331 line: old_line,
332 byte_range: 0..0,
333 }),
334 None,
335 ],
336 row_type: RowType::Deletion,
337 });
338 old_line += 1;
339 }
340 while new_line < new_line_count {
341 rows.push(AlignedRow {
342 pane_lines: vec![
343 None,
344 Some(SourceLineRef {
345 line: new_line,
346 byte_range: 0..0,
347 }),
348 ],
349 row_type: RowType::Addition,
350 });
351 new_line += 1;
352 }
353
354 Self { rows }
355 }
356
357 pub fn get_row(&self, display_row: usize) -> Option<&AlignedRow> {
359 self.rows.get(display_row)
360 }
361
362 pub fn row_count(&self) -> usize {
364 self.rows.len()
365 }
366
367 pub fn next_hunk_row(&self, after_row: usize) -> Option<usize> {
369 self.rows
370 .iter()
371 .enumerate()
372 .skip(after_row + 1)
373 .find(|(_, row)| row.row_type == RowType::HunkHeader)
374 .map(|(i, _)| i)
375 }
376
377 pub fn prev_hunk_row(&self, before_row: usize) -> Option<usize> {
379 self.rows
380 .iter()
381 .enumerate()
382 .take(before_row)
383 .rev()
384 .find(|(_, row)| row.row_type == RowType::HunkHeader)
385 .map(|(i, _)| i)
386 }
387}
388
389#[derive(Debug, Clone)]
391pub struct AlignedRow {
392 pub pane_lines: Vec<Option<SourceLineRef>>,
394 pub row_type: RowType,
396}
397
398impl AlignedRow {
399 pub fn context(old_line: usize, new_line: usize) -> Self {
401 Self {
402 pane_lines: vec![
403 Some(SourceLineRef {
404 line: old_line,
405 byte_range: 0..0,
406 }),
407 Some(SourceLineRef {
408 line: new_line,
409 byte_range: 0..0,
410 }),
411 ],
412 row_type: RowType::Context,
413 }
414 }
415
416 pub fn hunk_header() -> Self {
418 Self {
419 pane_lines: vec![None, None],
420 row_type: RowType::HunkHeader,
421 }
422 }
423
424 pub fn get_pane_line(&self, pane_index: usize) -> Option<&SourceLineRef> {
426 self.pane_lines.get(pane_index).and_then(|opt| opt.as_ref())
427 }
428
429 pub fn has_content(&self, pane_index: usize) -> bool {
431 self.pane_lines
432 .get(pane_index)
433 .map(|opt| opt.is_some())
434 .unwrap_or(false)
435 }
436}
437
438#[derive(Debug, Clone)]
440pub struct SourceLineRef {
441 pub line: usize,
443 pub byte_range: Range<usize>,
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
449pub enum RowType {
450 Context,
452 Deletion,
454 Addition,
456 Modification,
458 HunkHeader,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct DiffHunk {
465 pub old_start: usize,
467 pub old_count: usize,
469 pub new_start: usize,
471 pub new_count: usize,
473 pub header: Option<String>,
475}
476
477impl DiffHunk {
478 pub fn new(old_start: usize, old_count: usize, new_start: usize, new_count: usize) -> Self {
480 Self {
481 old_start,
482 old_count,
483 new_start,
484 new_count,
485 header: None,
486 }
487 }
488
489 pub fn with_header(mut self, header: impl Into<String>) -> Self {
491 self.header = Some(header.into());
492 self
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_line_alignment_from_hunks() {
502 let hunks = vec![DiffHunk::new(2, 2, 2, 3)];
504 let alignment = LineAlignment::from_hunks(&hunks, 5, 6);
505
506 assert!(alignment.rows.len() >= 7);
512
513 assert_eq!(alignment.rows[0].row_type, RowType::Context);
515 assert_eq!(alignment.rows[1].row_type, RowType::Context);
516
517 assert_eq!(alignment.rows[2].row_type, RowType::HunkHeader);
519 }
520
521 #[test]
522 fn test_composite_buffer_focus() {
523 let sources = vec![
524 SourcePane::new(BufferId(1), "OLD", false),
525 SourcePane::new(BufferId(2), "NEW", true),
526 ];
527 let mut composite = CompositeBuffer::new(
528 BufferId(0),
529 "Test".to_string(),
530 "diff-view".to_string(),
531 CompositeLayout::default(),
532 sources,
533 );
534
535 assert_eq!(composite.active_pane, 0);
536
537 composite.focus_next();
538 assert_eq!(composite.active_pane, 1);
539
540 composite.focus_next();
541 assert_eq!(composite.active_pane, 0); composite.focus_prev();
544 assert_eq!(composite.active_pane, 1);
545 }
546}