1use crate::app::types::BufferMetadata;
21use crate::app::Editor;
22use crate::model::composite_buffer::{CompositeBuffer, CompositeLayout, LineAlignment, SourcePane};
23use crate::model::event::{BufferId, LeafId};
24use crate::view::composite_view::CompositeViewState;
25use anyhow::Result as AnyhowResult;
26use unicode_segmentation::UnicodeSegmentation;
27
28struct CursorLineInfo {
30 content: String,
31 length: usize,
32 pane_width: usize,
33}
34
35#[derive(Clone, Copy)]
37enum CursorMovement {
38 Up,
39 Down,
40 Left,
41 Right,
42 LineStart,
43 LineEnd,
44 WordLeft,
45 WordRight,
46 WordEnd, }
48
49fn find_word_boundary_left(line: &str, from_column: usize) -> usize {
51 let graphemes: Vec<&str> = line.graphemes(true).collect();
52 let mut pos = from_column;
53 while pos > 0
55 && graphemes
56 .get(pos.saturating_sub(1))
57 .is_some_and(|g| g.chars().all(|c| c.is_whitespace()))
58 {
59 pos -= 1;
60 }
61 while pos > 0
63 && graphemes
64 .get(pos.saturating_sub(1))
65 .is_some_and(|g| !g.chars().all(|c| c.is_whitespace()))
66 {
67 pos -= 1;
68 }
69 pos
70}
71
72fn find_word_boundary_right(line: &str, from_column: usize, line_length: usize) -> usize {
74 let graphemes: Vec<&str> = line.graphemes(true).collect();
75 let mut pos = from_column;
76 while pos < graphemes.len() && !graphemes[pos].chars().all(|c| c.is_whitespace()) {
78 pos += 1;
79 }
80 while pos < graphemes.len() && graphemes[pos].chars().all(|c| c.is_whitespace()) {
82 pos += 1;
83 }
84 pos.min(line_length)
85}
86
87fn find_word_end_right(line: &str, from_column: usize, line_length: usize) -> usize {
89 let graphemes: Vec<&str> = line.graphemes(true).collect();
90 let mut pos = from_column;
91
92 while pos < graphemes.len() && graphemes[pos].chars().all(|c| c.is_whitespace()) {
94 pos += 1;
95 }
96
97 if pos < graphemes.len()
99 && !graphemes[pos]
100 .chars()
101 .any(|c| c.is_alphanumeric() || c == '_')
102 && !graphemes[pos].chars().all(|c| c.is_whitespace())
103 {
104 while pos < graphemes.len()
105 && !graphemes[pos]
106 .chars()
107 .any(|c| c.is_alphanumeric() || c == '_')
108 && !graphemes[pos].chars().all(|c| c.is_whitespace())
109 {
110 pos += 1;
111 }
112 } else {
113 while pos < graphemes.len()
115 && graphemes[pos]
116 .chars()
117 .any(|c| c.is_alphanumeric() || c == '_')
118 {
119 pos += 1;
120 }
121 }
122
123 pos.min(line_length)
124}
125
126impl Editor {
127 pub fn flush_layout(&mut self) {
140 use crate::view::composite_view::CompositeViewState;
141
142 let visible = self
143 .split_manager
144 .get_visible_buffers(ratatui::layout::Rect {
145 x: 0,
146 y: 0,
147 width: self.terminal_width,
148 height: self.terminal_height,
149 });
150
151 for (split_id, buffer_id, _area) in &visible {
152 if let Some(composite) = self.composite_buffers.get(buffer_id) {
154 let pane_count = composite.pane_count();
155 self.composite_view_states
156 .entry((*split_id, *buffer_id))
157 .or_insert_with(|| CompositeViewState::new(*buffer_id, pane_count));
158 }
159 }
160 }
161
162 pub fn is_composite_buffer(&self, buffer_id: BufferId) -> bool {
168 self.composite_buffers.contains_key(&buffer_id)
169 }
170
171 pub fn get_composite(&self, buffer_id: BufferId) -> Option<&CompositeBuffer> {
173 self.composite_buffers.get(&buffer_id)
174 }
175
176 pub fn get_composite_mut(&mut self, buffer_id: BufferId) -> Option<&mut CompositeBuffer> {
178 self.composite_buffers.get_mut(&buffer_id)
179 }
180
181 pub fn get_composite_view_state(
183 &mut self,
184 split_id: LeafId,
185 buffer_id: BufferId,
186 ) -> Option<&mut CompositeViewState> {
187 if !self.composite_buffers.contains_key(&buffer_id) {
188 return None;
189 }
190
191 let pane_count = self.composite_buffers.get(&buffer_id)?.pane_count();
192
193 Some(
194 self.composite_view_states
195 .entry((split_id, buffer_id))
196 .or_insert_with(|| CompositeViewState::new(buffer_id, pane_count)),
197 )
198 }
199
200 pub fn create_composite_buffer(
211 &mut self,
212 name: String,
213 mode: String,
214 layout: CompositeLayout,
215 sources: Vec<SourcePane>,
216 ) -> BufferId {
217 let buffer_id = BufferId(self.next_buffer_id);
218 self.next_buffer_id += 1;
219
220 let composite =
221 CompositeBuffer::new(buffer_id, name.clone(), mode.clone(), layout, sources);
222 self.composite_buffers.insert(buffer_id, composite);
223
224 let mut metadata = BufferMetadata::virtual_buffer(name.clone(), mode.clone(), true);
228 metadata.hidden_from_tabs = false;
229 self.buffer_metadata.insert(buffer_id, metadata);
230
231 let mut state = crate::state::EditorState::new(
234 80,
235 24,
236 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
237 std::sync::Arc::clone(&self.filesystem),
238 );
239 state.is_composite_buffer = true;
240 state.editing_disabled = true;
241 state.mode = mode;
242 self.buffers.insert(buffer_id, state);
243
244 self.event_logs
246 .insert(buffer_id, crate::model::event::EventLog::new());
247
248 let split_id = self.split_manager.active_split();
250 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
251 view_state.add_buffer(buffer_id);
252 }
253
254 buffer_id
255 }
256
257 pub fn set_composite_alignment(&mut self, buffer_id: BufferId, alignment: LineAlignment) {
262 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
263 composite.set_alignment(alignment);
264 }
265 }
266
267 pub fn close_composite_buffer(&mut self, buffer_id: BufferId) {
269 self.composite_buffers.remove(&buffer_id);
270 self.buffer_metadata.remove(&buffer_id);
271
272 self.composite_view_states
274 .retain(|(_, bid), _| *bid != buffer_id);
275 }
276
277 pub fn composite_focus_next(&mut self, split_id: LeafId, buffer_id: BufferId) {
279 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
280 composite.focus_next();
281 }
282 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
284 view_state.focus_next_pane();
285 }
286 }
287
288 pub fn composite_focus_prev(&mut self, split_id: LeafId, buffer_id: BufferId) {
290 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
291 composite.focus_prev();
292 }
293 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
295 view_state.focus_prev_pane();
296 }
297 }
298
299 pub fn composite_next_hunk_active(&mut self, buffer_id: BufferId) -> bool {
301 let split_id = self.split_manager.active_split();
302 self.composite_next_hunk(split_id, buffer_id)
303 }
304
305 pub fn composite_prev_hunk_active(&mut self, buffer_id: BufferId) -> bool {
307 let split_id = self.split_manager.active_split();
308 self.composite_prev_hunk(split_id, buffer_id)
309 }
310
311 pub fn composite_next_hunk(&mut self, split_id: LeafId, buffer_id: BufferId) -> bool {
314 let viewport_height = self.get_composite_viewport_height(split_id);
315 if let (Some(composite), Some(view_state)) = (
316 self.composite_buffers.get(&buffer_id),
317 self.composite_view_states.get_mut(&(split_id, buffer_id)),
318 ) {
319 if let Some(next_row) = composite.alignment.next_hunk_row(view_state.cursor_row) {
322 view_state.cursor_row = next_row;
323 let context_above = viewport_height / 3;
325 view_state.scroll_row = next_row.saturating_sub(context_above);
326 return true;
327 }
328 }
329 false
330 }
331
332 pub fn composite_prev_hunk(&mut self, split_id: LeafId, buffer_id: BufferId) -> bool {
335 let viewport_height = self.get_composite_viewport_height(split_id);
336 if let (Some(composite), Some(view_state)) = (
337 self.composite_buffers.get(&buffer_id),
338 self.composite_view_states.get_mut(&(split_id, buffer_id)),
339 ) {
340 if let Some(prev_row) = composite.alignment.prev_hunk_row(view_state.cursor_row) {
341 view_state.cursor_row = prev_row;
342 let context_above = viewport_height / 3;
343 view_state.scroll_row = prev_row.saturating_sub(context_above);
344 return true;
345 }
346 }
347 false
348 }
349
350 pub fn composite_scroll(&mut self, split_id: LeafId, buffer_id: BufferId, delta: isize) {
352 if let (Some(composite), Some(view_state)) = (
353 self.composite_buffers.get(&buffer_id),
354 self.composite_view_states.get_mut(&(split_id, buffer_id)),
355 ) {
356 let max_row = composite.row_count().saturating_sub(1);
357 view_state.scroll(delta, max_row);
358 }
359 }
360
361 pub fn composite_scroll_to(&mut self, split_id: LeafId, buffer_id: BufferId, row: usize) {
363 if let (Some(composite), Some(view_state)) = (
364 self.composite_buffers.get(&buffer_id),
365 self.composite_view_states.get_mut(&(split_id, buffer_id)),
366 ) {
367 let max_row = composite.row_count().saturating_sub(1);
368 view_state.set_scroll_row(row, max_row);
369 }
370 }
371
372 fn get_composite_viewport_height(&self, split_id: LeafId) -> usize {
379 const COMPOSITE_HEADER_HEIGHT: u16 = 1;
380 const DEFAULT_VIEWPORT_HEIGHT: usize = 24;
381
382 self.split_view_states
383 .get(&split_id)
384 .map(|vs| vs.viewport.height.saturating_sub(COMPOSITE_HEADER_HEIGHT) as usize)
385 .unwrap_or(DEFAULT_VIEWPORT_HEIGHT)
386 }
387
388 fn get_cursor_line_info(&self, split_id: LeafId, buffer_id: BufferId) -> CursorLineInfo {
390 let composite = self.composite_buffers.get(&buffer_id);
391 let view_state = self.composite_view_states.get(&(split_id, buffer_id));
392
393 if let (Some(composite), Some(view_state)) = (composite, view_state) {
394 let pane_line = composite
395 .alignment
396 .get_row(view_state.cursor_row)
397 .and_then(|row| row.get_pane_line(view_state.focused_pane));
398
399 tracing::debug!(
400 "get_cursor_line_info: cursor_row={}, focused_pane={}, pane_line={:?}",
401 view_state.cursor_row,
402 view_state.focused_pane,
403 pane_line
404 );
405
406 let line_bytes = pane_line.and_then(|line_ref| {
407 let source = composite.sources.get(view_state.focused_pane)?;
408 self.buffers
409 .get(&source.buffer_id)?
410 .buffer
411 .get_line(line_ref.line)
412 });
413
414 let content = line_bytes
415 .as_ref()
416 .map(|b| {
417 let s = String::from_utf8_lossy(b).to_string();
418 s.trim_end_matches('\n').trim_end_matches('\r').to_string()
420 })
421 .unwrap_or_default();
422 let length = content.graphemes(true).count();
423 let pane_width = view_state
424 .pane_widths
425 .get(view_state.focused_pane)
426 .copied()
427 .unwrap_or(40) as usize;
428
429 CursorLineInfo {
430 content,
431 length,
432 pane_width,
433 }
434 } else {
435 CursorLineInfo {
436 content: String::new(),
437 length: 0,
438 pane_width: 40,
439 }
440 }
441 }
442
443 fn apply_cursor_movement(
445 &mut self,
446 split_id: LeafId,
447 buffer_id: BufferId,
448 movement: CursorMovement,
449 line_info: &CursorLineInfo,
450 viewport_height: usize,
451 ) {
452 let max_row = self
453 .composite_buffers
454 .get(&buffer_id)
455 .map(|c| c.row_count().saturating_sub(1))
456 .unwrap_or(0);
457
458 let is_vertical = matches!(movement, CursorMovement::Up | CursorMovement::Down);
459 let mut wrapped_to_new_line = false;
460
461 let composite = self.composite_buffers.get(&buffer_id);
463
464 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
465 match movement {
466 CursorMovement::Down => {
467 view_state.move_cursor_down(max_row, viewport_height);
468 }
469 CursorMovement::Up => {
470 view_state.move_cursor_up(viewport_height);
471 }
472 CursorMovement::Left => {
473 if view_state.cursor_column > 0 {
474 view_state.move_cursor_left();
475 } else if view_state.cursor_row > 0 {
476 if let Some(composite) = composite {
478 let focused_pane = view_state.focused_pane;
479 let mut target_row = view_state.cursor_row - 1;
480 while target_row > 0 {
481 if let Some(row) = composite.alignment.get_row(target_row) {
482 if row.get_pane_line(focused_pane).is_some() {
483 break;
484 }
485 }
486 target_row -= 1;
487 }
488 if let Some(row) = composite.alignment.get_row(target_row) {
490 if row.get_pane_line(focused_pane).is_some() {
491 view_state.cursor_row = target_row;
492 if view_state.cursor_row < view_state.scroll_row {
493 view_state.scroll_row = view_state.cursor_row;
494 }
495 wrapped_to_new_line = true;
496 }
497 }
498 }
499 }
500 }
501 CursorMovement::Right => {
502 if view_state.cursor_column < line_info.length {
503 view_state.move_cursor_right(line_info.length, line_info.pane_width);
504 } else if view_state.cursor_row < max_row {
505 if let Some(composite) = composite {
507 let focused_pane = view_state.focused_pane;
508 let mut target_row = view_state.cursor_row + 1;
509 while target_row < max_row {
510 if let Some(row) = composite.alignment.get_row(target_row) {
511 if row.get_pane_line(focused_pane).is_some() {
512 break;
513 }
514 }
515 target_row += 1;
516 }
517 if let Some(row) = composite.alignment.get_row(target_row) {
519 if row.get_pane_line(focused_pane).is_some() {
520 view_state.cursor_row = target_row;
521 view_state.cursor_column = 0;
522 view_state.sticky_column = 0;
523 if view_state.cursor_row
524 >= view_state.scroll_row + viewport_height
525 {
526 view_state.scroll_row = view_state
527 .cursor_row
528 .saturating_sub(viewport_height - 1);
529 }
530 for viewport in &mut view_state.pane_viewports {
532 viewport.left_column = 0;
533 }
534 }
535 }
536 }
537 }
538 }
539 CursorMovement::LineStart => {
540 view_state.move_cursor_to_line_start();
541 }
542 CursorMovement::LineEnd => {
543 view_state.move_cursor_to_line_end(line_info.length, line_info.pane_width);
544 }
545 CursorMovement::WordLeft => {
546 let new_col =
547 find_word_boundary_left(&line_info.content, view_state.cursor_column);
548 if new_col < view_state.cursor_column {
549 view_state.cursor_column = new_col;
550 view_state.sticky_column = new_col;
551 let current_left = view_state
553 .pane_viewports
554 .get(view_state.focused_pane)
555 .map(|v| v.left_column)
556 .unwrap_or(0);
557 if view_state.cursor_column < current_left {
558 for viewport in &mut view_state.pane_viewports {
559 viewport.left_column = view_state.cursor_column;
560 }
561 }
562 } else if view_state.cursor_row > 0 {
563 if let Some(composite) = composite {
565 let focused_pane = view_state.focused_pane;
566 let mut target_row = view_state.cursor_row - 1;
567 while target_row > 0 {
568 if let Some(row) = composite.alignment.get_row(target_row) {
569 if row.get_pane_line(focused_pane).is_some() {
570 break;
571 }
572 }
573 target_row -= 1;
574 }
575 if let Some(row) = composite.alignment.get_row(target_row) {
577 if row.get_pane_line(focused_pane).is_some() {
578 view_state.cursor_row = target_row;
579 if view_state.cursor_row < view_state.scroll_row {
580 view_state.scroll_row = view_state.cursor_row;
581 }
582 wrapped_to_new_line = true;
583 }
584 }
585 }
586 }
587 }
588 CursorMovement::WordRight => {
589 let new_col = find_word_boundary_right(
590 &line_info.content,
591 view_state.cursor_column,
592 line_info.length,
593 );
594 if new_col > view_state.cursor_column {
595 view_state.cursor_column = new_col;
596 view_state.sticky_column = new_col;
597 let visible_width = line_info.pane_width.saturating_sub(4);
599 let current_left = view_state
600 .pane_viewports
601 .get(view_state.focused_pane)
602 .map(|v| v.left_column)
603 .unwrap_or(0);
604 if visible_width > 0
605 && view_state.cursor_column >= current_left + visible_width
606 {
607 let new_left = view_state
608 .cursor_column
609 .saturating_sub(visible_width.saturating_sub(1));
610 for viewport in &mut view_state.pane_viewports {
611 viewport.left_column = new_left;
612 }
613 }
614 } else if view_state.cursor_row < max_row {
615 if let Some(composite) = composite {
617 let focused_pane = view_state.focused_pane;
618 let mut target_row = view_state.cursor_row + 1;
619 while target_row < max_row {
620 if let Some(row) = composite.alignment.get_row(target_row) {
621 if row.get_pane_line(focused_pane).is_some() {
622 break;
623 }
624 }
625 target_row += 1;
626 }
627 if let Some(row) = composite.alignment.get_row(target_row) {
629 if row.get_pane_line(focused_pane).is_some() {
630 view_state.cursor_row = target_row;
631 view_state.cursor_column = 0;
632 view_state.sticky_column = 0;
633 if view_state.cursor_row
634 >= view_state.scroll_row + viewport_height
635 {
636 view_state.scroll_row = view_state
637 .cursor_row
638 .saturating_sub(viewport_height - 1);
639 }
640 for viewport in &mut view_state.pane_viewports {
642 viewport.left_column = 0;
643 }
644 }
645 }
646 }
647 }
648 }
649 CursorMovement::WordEnd => {
650 let new_col = find_word_end_right(
651 &line_info.content,
652 view_state.cursor_column,
653 line_info.length,
654 );
655 if new_col > view_state.cursor_column {
656 view_state.cursor_column = new_col;
657 view_state.sticky_column = new_col;
658 let visible_width = line_info.pane_width.saturating_sub(4);
660 let current_left = view_state
661 .pane_viewports
662 .get(view_state.focused_pane)
663 .map(|v| v.left_column)
664 .unwrap_or(0);
665 if visible_width > 0
666 && view_state.cursor_column >= current_left + visible_width
667 {
668 let new_left = view_state
669 .cursor_column
670 .saturating_sub(visible_width.saturating_sub(1));
671 for viewport in &mut view_state.pane_viewports {
672 viewport.left_column = new_left;
673 }
674 }
675 } else if view_state.cursor_row < max_row {
676 if let Some(composite) = composite {
678 let focused_pane = view_state.focused_pane;
679 let mut target_row = view_state.cursor_row + 1;
680 while target_row < max_row {
681 if let Some(row) = composite.alignment.get_row(target_row) {
682 if row.get_pane_line(focused_pane).is_some() {
683 break;
684 }
685 }
686 target_row += 1;
687 }
688 if let Some(row) = composite.alignment.get_row(target_row) {
690 if row.get_pane_line(focused_pane).is_some() {
691 view_state.cursor_row = target_row;
692 view_state.cursor_column = 0;
693 view_state.sticky_column = 0;
694 if view_state.cursor_row
695 >= view_state.scroll_row + viewport_height
696 {
697 view_state.scroll_row = view_state
698 .cursor_row
699 .saturating_sub(viewport_height - 1);
700 }
701 for viewport in &mut view_state.pane_viewports {
703 viewport.left_column = 0;
704 }
705 }
706 }
707 }
708 }
709 }
710 }
711 }
712
713 if is_vertical || wrapped_to_new_line {
715 let new_line_info = self.get_cursor_line_info(split_id, buffer_id);
716 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
717 if wrapped_to_new_line
718 && matches!(movement, CursorMovement::Left | CursorMovement::WordLeft)
719 {
720 tracing::debug!(
722 "Wrap left to row {}, setting column to line length {}",
723 view_state.cursor_row,
724 new_line_info.length
725 );
726 view_state.cursor_column = new_line_info.length;
727 view_state.sticky_column = new_line_info.length;
728 let visible_width = new_line_info.pane_width.saturating_sub(4);
730 if visible_width > 0 && view_state.cursor_column >= visible_width {
731 let new_left = view_state
732 .cursor_column
733 .saturating_sub(visible_width.saturating_sub(1));
734 for viewport in &mut view_state.pane_viewports {
735 viewport.left_column = new_left;
736 }
737 }
738 } else {
739 view_state.clamp_cursor_to_line(new_line_info.length);
740 }
741 }
742 }
743 }
744
745 fn sync_editor_cursor_from_composite(&mut self, split_id: LeafId, buffer_id: BufferId) {
747 let (cursor_row, cursor_column, focused_pane) = self
748 .composite_view_states
749 .get(&(split_id, buffer_id))
750 .map(|vs| (vs.cursor_row, vs.cursor_column, vs.focused_pane))
751 .unwrap_or((0, 0, 0));
752
753 let display_line = self
757 .composite_buffers
758 .get(&buffer_id)
759 .and_then(|composite| composite.alignment.get_row(cursor_row))
760 .and_then(|row| row.get_pane_line(focused_pane))
761 .map(|line_ref| line_ref.line)
762 .unwrap_or(cursor_row); if let Some(state) = self.buffers.get_mut(&buffer_id) {
765 state.primary_cursor_line_number =
766 crate::model::buffer::LineNumber::Absolute(display_line);
767 }
768 let active_split = self.split_manager.active_split();
770 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
771 view_state.cursors.primary_mut().position = cursor_column;
772 }
773 }
774
775 fn handle_cursor_movement_action(
777 &mut self,
778 split_id: LeafId,
779 buffer_id: BufferId,
780 movement: CursorMovement,
781 extend_selection: bool,
782 ) -> Option<bool> {
783 let viewport_height = self.get_composite_viewport_height(split_id);
784
785 let line_info = self.get_cursor_line_info(split_id, buffer_id);
786
787 if extend_selection {
788 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
790 if !view_state.visual_mode {
791 view_state.start_visual_selection();
792 }
793 }
794 } else {
795 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
797 if view_state.visual_mode {
798 view_state.clear_selection();
799 }
800 }
801 }
802
803 self.apply_cursor_movement(split_id, buffer_id, movement, &line_info, viewport_height);
804 self.sync_editor_cursor_from_composite(split_id, buffer_id);
805
806 Some(true)
807 }
808
809 pub fn handle_composite_action(
815 &mut self,
816 buffer_id: BufferId,
817 action: &crate::input::keybindings::Action,
818 ) -> Option<bool> {
819 use crate::input::keybindings::Action;
820
821 let split_id = self.split_manager.active_split();
822
823 let _composite = self.composite_buffers.get(&buffer_id)?;
825 let _view_state = self.composite_view_states.get(&(split_id, buffer_id))?;
826
827 match action {
828 Action::InsertTab => {
830 self.composite_focus_next(split_id, buffer_id);
831 Some(true)
832 }
833
834 Action::Copy => {
836 self.handle_composite_copy(split_id, buffer_id);
837 Some(true)
838 }
839
840 Action::MoveDown => {
842 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Down, false)
843 }
844 Action::MoveUp => {
845 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Up, false)
846 }
847 Action::MoveLeft => {
848 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Left, false)
849 }
850 Action::MoveRight => self.handle_cursor_movement_action(
851 split_id,
852 buffer_id,
853 CursorMovement::Right,
854 false,
855 ),
856 Action::MoveLineStart | Action::SmartHome => self.handle_cursor_movement_action(
857 split_id,
858 buffer_id,
859 CursorMovement::LineStart,
860 false,
861 ),
862 Action::MoveLineEnd => self.handle_cursor_movement_action(
863 split_id,
864 buffer_id,
865 CursorMovement::LineEnd,
866 false,
867 ),
868 Action::MoveWordLeft => self.handle_cursor_movement_action(
869 split_id,
870 buffer_id,
871 CursorMovement::WordLeft,
872 false,
873 ),
874 Action::MoveWordRight => self.handle_cursor_movement_action(
875 split_id,
876 buffer_id,
877 CursorMovement::WordRight,
878 false,
879 ),
880 Action::MoveWordEnd | Action::ViMoveWordEnd => self.handle_cursor_movement_action(
881 split_id,
882 buffer_id,
883 CursorMovement::WordEnd,
884 false,
885 ),
886 Action::MoveLeftInLine => {
887 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Left, false)
888 }
889 Action::MoveRightInLine => self.handle_cursor_movement_action(
890 split_id,
891 buffer_id,
892 CursorMovement::Right,
893 false,
894 ),
895
896 Action::SelectDown => {
898 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Down, true)
899 }
900 Action::SelectUp => {
901 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Up, true)
902 }
903 Action::SelectLeft => {
904 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Left, true)
905 }
906 Action::SelectRight => {
907 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Right, true)
908 }
909 Action::SelectLineStart => self.handle_cursor_movement_action(
910 split_id,
911 buffer_id,
912 CursorMovement::LineStart,
913 true,
914 ),
915 Action::SelectLineEnd => self.handle_cursor_movement_action(
916 split_id,
917 buffer_id,
918 CursorMovement::LineEnd,
919 true,
920 ),
921 Action::SelectWordLeft => self.handle_cursor_movement_action(
922 split_id,
923 buffer_id,
924 CursorMovement::WordLeft,
925 true,
926 ),
927 Action::SelectWordRight => self.handle_cursor_movement_action(
928 split_id,
929 buffer_id,
930 CursorMovement::WordRight,
931 true,
932 ),
933 Action::SelectWordEnd | Action::ViSelectWordEnd => self.handle_cursor_movement_action(
934 split_id,
935 buffer_id,
936 CursorMovement::WordEnd,
937 true,
938 ),
939
940 Action::MovePageDown | Action::MovePageUp => {
942 let viewport_height = self.get_composite_viewport_height(split_id);
943
944 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id))
945 {
946 if matches!(action, Action::MovePageDown) {
947 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
948 let max_row = composite.row_count().saturating_sub(1);
949 view_state.page_down(viewport_height, max_row);
950 view_state.cursor_row = view_state.scroll_row;
951 }
952 } else {
953 view_state.page_up(viewport_height);
954 view_state.cursor_row = view_state.scroll_row;
955 }
956 }
957 self.sync_editor_cursor_from_composite(split_id, buffer_id);
958 Some(true)
959 }
960
961 Action::MoveDocumentStart | Action::MoveDocumentEnd => {
963 let viewport_height = self.get_composite_viewport_height(split_id);
964
965 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id))
966 {
967 if matches!(action, Action::MoveDocumentStart) {
968 view_state.move_cursor_to_top();
969 } else if let Some(composite) = self.composite_buffers.get(&buffer_id) {
970 let max_row = composite.row_count().saturating_sub(1);
971 view_state.move_cursor_to_bottom(max_row, viewport_height);
972 }
973 }
974 self.sync_editor_cursor_from_composite(split_id, buffer_id);
975 Some(true)
976 }
977
978 Action::ScrollDown | Action::ScrollUp => {
980 let delta = if matches!(action, Action::ScrollDown) {
981 1
982 } else {
983 -1
984 };
985 self.composite_scroll(split_id, buffer_id, delta);
986 Some(true)
987 }
988
989 _ => None,
991 }
992 }
993
994 fn handle_composite_copy(&mut self, split_id: LeafId, buffer_id: BufferId) {
996 let text = {
997 let composite = match self.composite_buffers.get(&buffer_id) {
998 Some(c) => c,
999 None => return,
1000 };
1001 let view_state = match self.composite_view_states.get(&(split_id, buffer_id)) {
1002 Some(vs) => vs,
1003 None => return,
1004 };
1005
1006 let (start_row, end_row) = match view_state.selection_row_range() {
1007 Some(range) => range,
1008 None => return,
1009 };
1010
1011 let source = match composite.sources.get(view_state.focused_pane) {
1012 Some(s) => s,
1013 None => return,
1014 };
1015
1016 let source_state = match self.buffers.get(&source.buffer_id) {
1017 Some(s) => s,
1018 None => return,
1019 };
1020
1021 let mut text = String::new();
1023 for row in start_row..=end_row {
1024 if let Some(aligned_row) = composite.alignment.rows.get(row) {
1025 if let Some(line_ref) = aligned_row.get_pane_line(view_state.focused_pane) {
1026 if let Some(line_bytes) = source_state.buffer.get_line(line_ref.line) {
1027 if !text.is_empty() {
1028 text.push('\n');
1029 }
1030 let line_str = String::from_utf8_lossy(&line_bytes);
1032 let line_trimmed = line_str.trim_end_matches(&['\n', '\r'][..]);
1033 text.push_str(line_trimmed);
1034 }
1035 }
1036 }
1037 }
1038 text
1039 };
1040
1041 if !text.is_empty() {
1042 self.clipboard.copy(text);
1043 }
1044
1045 }
1047
1048 pub(crate) fn handle_create_composite_buffer(
1054 &mut self,
1055 name: String,
1056 mode: String,
1057 layout_config: fresh_core::api::CompositeLayoutConfig,
1058 source_configs: Vec<fresh_core::api::CompositeSourceConfig>,
1059 hunks: Option<Vec<fresh_core::api::CompositeHunk>>,
1060 initial_focus_hunk: Option<usize>,
1061 _request_id: Option<u64>,
1062 ) {
1063 use crate::model::composite_buffer::{
1064 CompositeLayout, DiffHunk, GutterStyle, LineAlignment, PaneStyle, SourcePane,
1065 };
1066
1067 let layout = match layout_config.layout_type.as_str() {
1069 "stacked" => CompositeLayout::Stacked {
1070 spacing: layout_config.spacing.unwrap_or(1),
1071 },
1072 "unified" => CompositeLayout::Unified,
1073 _ => CompositeLayout::SideBySide {
1074 ratios: layout_config.ratios.unwrap_or_else(|| vec![0.5, 0.5]),
1075 show_separator: layout_config.show_separator,
1076 },
1077 };
1078
1079 let sources: Vec<SourcePane> = source_configs
1081 .into_iter()
1082 .map(|src| {
1083 let mut pane = SourcePane::new(BufferId(src.buffer_id), src.label, src.editable);
1084 if let Some(style_config) = src.style {
1085 let gutter_style = match style_config.gutter_style.as_deref() {
1086 Some("diff-markers") => GutterStyle::DiffMarkers,
1087 Some("both") => GutterStyle::Both,
1088 Some("none") => GutterStyle::None,
1089 _ => GutterStyle::LineNumbers,
1090 };
1091 let to_tuple = |arr: [u8; 3]| (arr[0], arr[1], arr[2]);
1093 pane.style = PaneStyle {
1094 add_bg: style_config.add_bg.map(to_tuple),
1095 remove_bg: style_config.remove_bg.map(to_tuple),
1096 modify_bg: style_config.modify_bg.map(to_tuple),
1097 gutter_style,
1098 };
1099 }
1100 pane
1101 })
1102 .collect();
1103
1104 let buffer_id = self.create_composite_buffer(name.clone(), mode.clone(), layout, sources);
1106
1107 if let Some(hunk_configs) = hunks {
1109 let diff_hunks: Vec<DiffHunk> = hunk_configs
1110 .into_iter()
1111 .map(|h| DiffHunk::new(h.old_start, h.old_count, h.new_start, h.new_count))
1112 .collect();
1113
1114 let old_line_count = self
1116 .buffers
1117 .get(&self.composite_buffers.get(&buffer_id).unwrap().sources[0].buffer_id)
1118 .and_then(|s| s.buffer.line_count())
1119 .unwrap_or(0);
1120 let new_line_count = self
1121 .buffers
1122 .get(&self.composite_buffers.get(&buffer_id).unwrap().sources[1].buffer_id)
1123 .and_then(|s| s.buffer.line_count())
1124 .unwrap_or(0);
1125
1126 let alignment = LineAlignment::from_hunks(&diff_hunks, old_line_count, new_line_count);
1127 self.set_composite_alignment(buffer_id, alignment);
1128 }
1129
1130 if initial_focus_hunk.is_some() {
1132 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
1133 composite.initial_focus_hunk = initial_focus_hunk;
1134 }
1135 }
1136
1137 tracing::info!(
1138 "Created composite buffer '{}' with mode '{}' (id={:?})",
1139 name,
1140 mode,
1141 buffer_id
1142 );
1143
1144 if let Some(req_id) = _request_id {
1146 let result = buffer_id.0.to_string();
1148 self.plugin_manager
1149 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), result);
1150 tracing::info!(
1151 "CreateCompositeBuffer: resolve_callback sent for request_id={}",
1152 req_id
1153 );
1154 }
1155 }
1156
1157 pub(crate) fn handle_update_composite_alignment(
1159 &mut self,
1160 buffer_id: BufferId,
1161 hunk_configs: Vec<fresh_core::api::CompositeHunk>,
1162 ) {
1163 use crate::model::composite_buffer::{DiffHunk, LineAlignment};
1164
1165 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1166 let diff_hunks: Vec<DiffHunk> = hunk_configs
1167 .into_iter()
1168 .map(|h| DiffHunk::new(h.old_start, h.old_count, h.new_start, h.new_count))
1169 .collect();
1170
1171 let old_line_count = self
1173 .buffers
1174 .get(&composite.sources[0].buffer_id)
1175 .and_then(|s| s.buffer.line_count())
1176 .unwrap_or(0);
1177 let new_line_count = self
1178 .buffers
1179 .get(&composite.sources[1].buffer_id)
1180 .and_then(|s| s.buffer.line_count())
1181 .unwrap_or(0);
1182
1183 let alignment = LineAlignment::from_hunks(&diff_hunks, old_line_count, new_line_count);
1184 self.set_composite_alignment(buffer_id, alignment);
1185 }
1186 }
1187
1188 pub(crate) fn handle_composite_click(
1190 &mut self,
1191 col: u16,
1192 row: u16,
1193 split_id: LeafId,
1194 buffer_id: BufferId,
1195 content_rect: ratatui::layout::Rect,
1196 ) -> AnyhowResult<()> {
1197 let pane_idx =
1199 if let Some(view_state) = self.composite_view_states.get(&(split_id, buffer_id)) {
1200 let mut x = content_rect.x;
1201 let mut found_pane = 0;
1202 for (i, &width) in view_state.pane_widths.iter().enumerate() {
1203 if col >= x && col < x + width {
1204 found_pane = i;
1205 break;
1206 }
1207 x += width + 1; }
1209 found_pane
1210 } else {
1211 0
1212 };
1213
1214 let content_row = row.saturating_sub(content_rect.y).saturating_sub(1) as usize;
1217
1218 let (pane_start_x, left_column) =
1220 if let Some(view_state) = self.composite_view_states.get(&(split_id, buffer_id)) {
1221 let mut x = content_rect.x;
1222 for (i, &width) in view_state.pane_widths.iter().enumerate() {
1223 if i == pane_idx {
1224 break;
1225 }
1226 x += width + 1;
1227 }
1228 let left_col = view_state
1229 .pane_viewports
1230 .get(pane_idx)
1231 .map(|vp| vp.left_column)
1232 .unwrap_or(0);
1233 (x, left_col)
1234 } else {
1235 (content_rect.x, 0)
1236 };
1237 let gutter_width = 4; let visual_col = col
1239 .saturating_sub(pane_start_x)
1240 .saturating_sub(gutter_width) as usize;
1241 let click_col = left_column + visual_col;
1243
1244 let display_row =
1246 if let Some(view_state) = self.composite_view_states.get(&(split_id, buffer_id)) {
1247 view_state.scroll_row + content_row
1248 } else {
1249 content_row
1250 };
1251
1252 let line_length = if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1253 composite
1254 .alignment
1255 .get_row(display_row)
1256 .and_then(|row| row.get_pane_line(pane_idx))
1257 .and_then(|line_ref| {
1258 let source = composite.sources.get(pane_idx)?;
1259 self.buffers
1260 .get(&source.buffer_id)?
1261 .buffer
1262 .get_line(line_ref.line)
1263 })
1264 .map(|bytes| {
1265 let s = String::from_utf8_lossy(&bytes);
1266 let trimmed = s.trim_end_matches('\n').trim_end_matches('\r');
1268 trimmed.graphemes(true).count()
1269 })
1270 .unwrap_or(0)
1271 } else {
1272 0
1273 };
1274
1275 let clamped_col = click_col.min(line_length);
1277
1278 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
1280 composite.active_pane = pane_idx;
1281 }
1282
1283 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
1285 view_state.focused_pane = pane_idx;
1286 view_state.cursor_row = display_row;
1287 view_state.cursor_column = clamped_col;
1288 view_state.sticky_column = clamped_col;
1289
1290 view_state.clear_selection();
1292 }
1293
1294 self.mouse_state.dragging_text_selection = false; self.mouse_state.drag_selection_split = Some(split_id);
1297
1298 self.sync_editor_cursor_from_composite(split_id, buffer_id);
1300
1301 Ok(())
1302 }
1303}