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 is_composite_buffer(&self, buffer_id: BufferId) -> bool {
133 self.composite_buffers.contains_key(&buffer_id)
134 }
135
136 pub fn get_composite(&self, buffer_id: BufferId) -> Option<&CompositeBuffer> {
138 self.composite_buffers.get(&buffer_id)
139 }
140
141 pub fn get_composite_mut(&mut self, buffer_id: BufferId) -> Option<&mut CompositeBuffer> {
143 self.composite_buffers.get_mut(&buffer_id)
144 }
145
146 pub fn get_composite_view_state(
148 &mut self,
149 split_id: LeafId,
150 buffer_id: BufferId,
151 ) -> Option<&mut CompositeViewState> {
152 if !self.composite_buffers.contains_key(&buffer_id) {
153 return None;
154 }
155
156 let pane_count = self.composite_buffers.get(&buffer_id)?.pane_count();
157
158 Some(
159 self.composite_view_states
160 .entry((split_id, buffer_id))
161 .or_insert_with(|| CompositeViewState::new(buffer_id, pane_count)),
162 )
163 }
164
165 pub fn create_composite_buffer(
176 &mut self,
177 name: String,
178 mode: String,
179 layout: CompositeLayout,
180 sources: Vec<SourcePane>,
181 ) -> BufferId {
182 let buffer_id = BufferId(self.next_buffer_id);
183 self.next_buffer_id += 1;
184
185 let composite =
186 CompositeBuffer::new(buffer_id, name.clone(), mode.clone(), layout, sources);
187 self.composite_buffers.insert(buffer_id, composite);
188
189 let mut metadata = BufferMetadata::virtual_buffer(name.clone(), mode.clone(), true);
193 metadata.hidden_from_tabs = false;
194 self.buffer_metadata.insert(buffer_id, metadata);
195
196 let mut state = crate::state::EditorState::new(
199 80,
200 24,
201 crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
202 std::sync::Arc::clone(&self.filesystem),
203 );
204 state.is_composite_buffer = true;
205 state.editing_disabled = true;
206 state.mode = mode;
207 self.buffers.insert(buffer_id, state);
208
209 self.event_logs
211 .insert(buffer_id, crate::model::event::EventLog::new());
212
213 let split_id = self.split_manager.active_split();
215 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
216 view_state.add_buffer(buffer_id);
217 }
218
219 buffer_id
220 }
221
222 pub fn set_composite_alignment(&mut self, buffer_id: BufferId, alignment: LineAlignment) {
227 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
228 composite.set_alignment(alignment);
229 }
230 }
231
232 pub fn close_composite_buffer(&mut self, buffer_id: BufferId) {
234 self.composite_buffers.remove(&buffer_id);
235 self.buffer_metadata.remove(&buffer_id);
236
237 self.composite_view_states
239 .retain(|(_, bid), _| *bid != buffer_id);
240 }
241
242 pub fn composite_focus_next(&mut self, split_id: LeafId, buffer_id: BufferId) {
244 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
245 composite.focus_next();
246 }
247 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
249 view_state.focus_next_pane();
250 }
251 }
252
253 pub fn composite_focus_prev(&mut self, split_id: LeafId, buffer_id: BufferId) {
255 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
256 composite.focus_prev();
257 }
258 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
260 view_state.focus_prev_pane();
261 }
262 }
263
264 pub fn composite_next_hunk(&mut self, split_id: LeafId, buffer_id: BufferId) -> bool {
266 if let (Some(composite), Some(view_state)) = (
267 self.composite_buffers.get(&buffer_id),
268 self.composite_view_states.get_mut(&(split_id, buffer_id)),
269 ) {
270 if let Some(next_row) = composite.alignment.next_hunk_row(view_state.scroll_row) {
271 view_state.scroll_row = next_row;
272 return true;
273 }
274 }
275 false
276 }
277
278 pub fn composite_prev_hunk(&mut self, split_id: LeafId, buffer_id: BufferId) -> bool {
280 if let (Some(composite), Some(view_state)) = (
281 self.composite_buffers.get(&buffer_id),
282 self.composite_view_states.get_mut(&(split_id, buffer_id)),
283 ) {
284 if let Some(prev_row) = composite.alignment.prev_hunk_row(view_state.scroll_row) {
285 view_state.scroll_row = prev_row;
286 return true;
287 }
288 }
289 false
290 }
291
292 pub fn composite_scroll(&mut self, split_id: LeafId, buffer_id: BufferId, delta: isize) {
294 if let (Some(composite), Some(view_state)) = (
295 self.composite_buffers.get(&buffer_id),
296 self.composite_view_states.get_mut(&(split_id, buffer_id)),
297 ) {
298 let max_row = composite.row_count().saturating_sub(1);
299 view_state.scroll(delta, max_row);
300 }
301 }
302
303 pub fn composite_scroll_to(&mut self, split_id: LeafId, buffer_id: BufferId, row: usize) {
305 if let (Some(composite), Some(view_state)) = (
306 self.composite_buffers.get(&buffer_id),
307 self.composite_view_states.get_mut(&(split_id, buffer_id)),
308 ) {
309 let max_row = composite.row_count().saturating_sub(1);
310 view_state.set_scroll_row(row, max_row);
311 }
312 }
313
314 fn get_composite_viewport_height(&self, split_id: LeafId) -> usize {
321 const COMPOSITE_HEADER_HEIGHT: u16 = 1;
322 const DEFAULT_VIEWPORT_HEIGHT: usize = 24;
323
324 self.split_view_states
325 .get(&split_id)
326 .map(|vs| vs.viewport.height.saturating_sub(COMPOSITE_HEADER_HEIGHT) as usize)
327 .unwrap_or(DEFAULT_VIEWPORT_HEIGHT)
328 }
329
330 fn get_cursor_line_info(&self, split_id: LeafId, buffer_id: BufferId) -> CursorLineInfo {
332 let composite = self.composite_buffers.get(&buffer_id);
333 let view_state = self.composite_view_states.get(&(split_id, buffer_id));
334
335 if let (Some(composite), Some(view_state)) = (composite, view_state) {
336 let pane_line = composite
337 .alignment
338 .get_row(view_state.cursor_row)
339 .and_then(|row| row.get_pane_line(view_state.focused_pane));
340
341 tracing::debug!(
342 "get_cursor_line_info: cursor_row={}, focused_pane={}, pane_line={:?}",
343 view_state.cursor_row,
344 view_state.focused_pane,
345 pane_line
346 );
347
348 let line_bytes = pane_line.and_then(|line_ref| {
349 let source = composite.sources.get(view_state.focused_pane)?;
350 self.buffers
351 .get(&source.buffer_id)?
352 .buffer
353 .get_line(line_ref.line)
354 });
355
356 let content = line_bytes
357 .as_ref()
358 .map(|b| {
359 let s = String::from_utf8_lossy(b).to_string();
360 s.trim_end_matches('\n').trim_end_matches('\r').to_string()
362 })
363 .unwrap_or_default();
364 let length = content.graphemes(true).count();
365 let pane_width = view_state
366 .pane_widths
367 .get(view_state.focused_pane)
368 .copied()
369 .unwrap_or(40) as usize;
370
371 CursorLineInfo {
372 content,
373 length,
374 pane_width,
375 }
376 } else {
377 CursorLineInfo {
378 content: String::new(),
379 length: 0,
380 pane_width: 40,
381 }
382 }
383 }
384
385 fn apply_cursor_movement(
387 &mut self,
388 split_id: LeafId,
389 buffer_id: BufferId,
390 movement: CursorMovement,
391 line_info: &CursorLineInfo,
392 viewport_height: usize,
393 ) {
394 let max_row = self
395 .composite_buffers
396 .get(&buffer_id)
397 .map(|c| c.row_count().saturating_sub(1))
398 .unwrap_or(0);
399
400 let is_vertical = matches!(movement, CursorMovement::Up | CursorMovement::Down);
401 let mut wrapped_to_new_line = false;
402
403 let composite = self.composite_buffers.get(&buffer_id);
405
406 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
407 match movement {
408 CursorMovement::Down => {
409 view_state.move_cursor_down(max_row, viewport_height);
410 }
411 CursorMovement::Up => {
412 view_state.move_cursor_up(viewport_height);
413 }
414 CursorMovement::Left => {
415 if view_state.cursor_column > 0 {
416 view_state.move_cursor_left();
417 } else if view_state.cursor_row > 0 {
418 if let Some(composite) = composite {
420 let focused_pane = view_state.focused_pane;
421 let mut target_row = view_state.cursor_row - 1;
422 while target_row > 0 {
423 if let Some(row) = composite.alignment.get_row(target_row) {
424 if row.get_pane_line(focused_pane).is_some() {
425 break;
426 }
427 }
428 target_row -= 1;
429 }
430 if let Some(row) = composite.alignment.get_row(target_row) {
432 if row.get_pane_line(focused_pane).is_some() {
433 view_state.cursor_row = target_row;
434 if view_state.cursor_row < view_state.scroll_row {
435 view_state.scroll_row = view_state.cursor_row;
436 }
437 wrapped_to_new_line = true;
438 }
439 }
440 }
441 }
442 }
443 CursorMovement::Right => {
444 if view_state.cursor_column < line_info.length {
445 view_state.move_cursor_right(line_info.length, line_info.pane_width);
446 } else if view_state.cursor_row < max_row {
447 if let Some(composite) = composite {
449 let focused_pane = view_state.focused_pane;
450 let mut target_row = view_state.cursor_row + 1;
451 while target_row < max_row {
452 if let Some(row) = composite.alignment.get_row(target_row) {
453 if row.get_pane_line(focused_pane).is_some() {
454 break;
455 }
456 }
457 target_row += 1;
458 }
459 if let Some(row) = composite.alignment.get_row(target_row) {
461 if row.get_pane_line(focused_pane).is_some() {
462 view_state.cursor_row = target_row;
463 view_state.cursor_column = 0;
464 view_state.sticky_column = 0;
465 if view_state.cursor_row
466 >= view_state.scroll_row + viewport_height
467 {
468 view_state.scroll_row = view_state
469 .cursor_row
470 .saturating_sub(viewport_height - 1);
471 }
472 for viewport in &mut view_state.pane_viewports {
474 viewport.left_column = 0;
475 }
476 }
477 }
478 }
479 }
480 }
481 CursorMovement::LineStart => {
482 view_state.move_cursor_to_line_start();
483 }
484 CursorMovement::LineEnd => {
485 view_state.move_cursor_to_line_end(line_info.length, line_info.pane_width);
486 }
487 CursorMovement::WordLeft => {
488 let new_col =
489 find_word_boundary_left(&line_info.content, view_state.cursor_column);
490 if new_col < view_state.cursor_column {
491 view_state.cursor_column = new_col;
492 view_state.sticky_column = new_col;
493 let current_left = view_state
495 .pane_viewports
496 .get(view_state.focused_pane)
497 .map(|v| v.left_column)
498 .unwrap_or(0);
499 if view_state.cursor_column < current_left {
500 for viewport in &mut view_state.pane_viewports {
501 viewport.left_column = view_state.cursor_column;
502 }
503 }
504 } else if view_state.cursor_row > 0 {
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 > 0 {
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 if view_state.cursor_row < view_state.scroll_row {
522 view_state.scroll_row = view_state.cursor_row;
523 }
524 wrapped_to_new_line = true;
525 }
526 }
527 }
528 }
529 }
530 CursorMovement::WordRight => {
531 let new_col = find_word_boundary_right(
532 &line_info.content,
533 view_state.cursor_column,
534 line_info.length,
535 );
536 if new_col > view_state.cursor_column {
537 view_state.cursor_column = new_col;
538 view_state.sticky_column = new_col;
539 let visible_width = line_info.pane_width.saturating_sub(4);
541 let current_left = view_state
542 .pane_viewports
543 .get(view_state.focused_pane)
544 .map(|v| v.left_column)
545 .unwrap_or(0);
546 if visible_width > 0
547 && view_state.cursor_column >= current_left + visible_width
548 {
549 let new_left = view_state
550 .cursor_column
551 .saturating_sub(visible_width.saturating_sub(1));
552 for viewport in &mut view_state.pane_viewports {
553 viewport.left_column = new_left;
554 }
555 }
556 } else if view_state.cursor_row < max_row {
557 if let Some(composite) = composite {
559 let focused_pane = view_state.focused_pane;
560 let mut target_row = view_state.cursor_row + 1;
561 while target_row < max_row {
562 if let Some(row) = composite.alignment.get_row(target_row) {
563 if row.get_pane_line(focused_pane).is_some() {
564 break;
565 }
566 }
567 target_row += 1;
568 }
569 if let Some(row) = composite.alignment.get_row(target_row) {
571 if row.get_pane_line(focused_pane).is_some() {
572 view_state.cursor_row = target_row;
573 view_state.cursor_column = 0;
574 view_state.sticky_column = 0;
575 if view_state.cursor_row
576 >= view_state.scroll_row + viewport_height
577 {
578 view_state.scroll_row = view_state
579 .cursor_row
580 .saturating_sub(viewport_height - 1);
581 }
582 for viewport in &mut view_state.pane_viewports {
584 viewport.left_column = 0;
585 }
586 }
587 }
588 }
589 }
590 }
591 CursorMovement::WordEnd => {
592 let new_col = find_word_end_right(
593 &line_info.content,
594 view_state.cursor_column,
595 line_info.length,
596 );
597 if new_col > view_state.cursor_column {
598 view_state.cursor_column = new_col;
599 view_state.sticky_column = new_col;
600 let visible_width = line_info.pane_width.saturating_sub(4);
602 let current_left = view_state
603 .pane_viewports
604 .get(view_state.focused_pane)
605 .map(|v| v.left_column)
606 .unwrap_or(0);
607 if visible_width > 0
608 && view_state.cursor_column >= current_left + visible_width
609 {
610 let new_left = view_state
611 .cursor_column
612 .saturating_sub(visible_width.saturating_sub(1));
613 for viewport in &mut view_state.pane_viewports {
614 viewport.left_column = new_left;
615 }
616 }
617 } else if view_state.cursor_row < max_row {
618 if let Some(composite) = composite {
620 let focused_pane = view_state.focused_pane;
621 let mut target_row = view_state.cursor_row + 1;
622 while target_row < max_row {
623 if let Some(row) = composite.alignment.get_row(target_row) {
624 if row.get_pane_line(focused_pane).is_some() {
625 break;
626 }
627 }
628 target_row += 1;
629 }
630 if let Some(row) = composite.alignment.get_row(target_row) {
632 if row.get_pane_line(focused_pane).is_some() {
633 view_state.cursor_row = target_row;
634 view_state.cursor_column = 0;
635 view_state.sticky_column = 0;
636 if view_state.cursor_row
637 >= view_state.scroll_row + viewport_height
638 {
639 view_state.scroll_row = view_state
640 .cursor_row
641 .saturating_sub(viewport_height - 1);
642 }
643 for viewport in &mut view_state.pane_viewports {
645 viewport.left_column = 0;
646 }
647 }
648 }
649 }
650 }
651 }
652 }
653 }
654
655 if is_vertical || wrapped_to_new_line {
657 let new_line_info = self.get_cursor_line_info(split_id, buffer_id);
658 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
659 if wrapped_to_new_line
660 && matches!(movement, CursorMovement::Left | CursorMovement::WordLeft)
661 {
662 tracing::debug!(
664 "Wrap left to row {}, setting column to line length {}",
665 view_state.cursor_row,
666 new_line_info.length
667 );
668 view_state.cursor_column = new_line_info.length;
669 view_state.sticky_column = new_line_info.length;
670 let visible_width = new_line_info.pane_width.saturating_sub(4);
672 if visible_width > 0 && view_state.cursor_column >= visible_width {
673 let new_left = view_state
674 .cursor_column
675 .saturating_sub(visible_width.saturating_sub(1));
676 for viewport in &mut view_state.pane_viewports {
677 viewport.left_column = new_left;
678 }
679 }
680 } else {
681 view_state.clamp_cursor_to_line(new_line_info.length);
682 }
683 }
684 }
685 }
686
687 fn sync_editor_cursor_from_composite(&mut self, split_id: LeafId, buffer_id: BufferId) {
689 let (cursor_row, cursor_column, focused_pane) = self
690 .composite_view_states
691 .get(&(split_id, buffer_id))
692 .map(|vs| (vs.cursor_row, vs.cursor_column, vs.focused_pane))
693 .unwrap_or((0, 0, 0));
694
695 let display_line = self
699 .composite_buffers
700 .get(&buffer_id)
701 .and_then(|composite| composite.alignment.get_row(cursor_row))
702 .and_then(|row| row.get_pane_line(focused_pane))
703 .map(|line_ref| line_ref.line)
704 .unwrap_or(cursor_row); if let Some(state) = self.buffers.get_mut(&buffer_id) {
707 state.primary_cursor_line_number =
708 crate::model::buffer::LineNumber::Absolute(display_line);
709 }
710 let active_split = self.split_manager.active_split();
712 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
713 view_state.cursors.primary_mut().position = cursor_column;
714 }
715 }
716
717 fn handle_cursor_movement_action(
719 &mut self,
720 split_id: LeafId,
721 buffer_id: BufferId,
722 movement: CursorMovement,
723 extend_selection: bool,
724 ) -> Option<bool> {
725 let viewport_height = self.get_composite_viewport_height(split_id);
726
727 let line_info = self.get_cursor_line_info(split_id, buffer_id);
728
729 if extend_selection {
730 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
732 if !view_state.visual_mode {
733 view_state.start_visual_selection();
734 }
735 }
736 } else {
737 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
739 if view_state.visual_mode {
740 view_state.clear_selection();
741 }
742 }
743 }
744
745 self.apply_cursor_movement(split_id, buffer_id, movement, &line_info, viewport_height);
746 self.sync_editor_cursor_from_composite(split_id, buffer_id);
747
748 Some(true)
749 }
750
751 pub fn handle_composite_action(
757 &mut self,
758 buffer_id: BufferId,
759 action: &crate::input::keybindings::Action,
760 ) -> Option<bool> {
761 use crate::input::keybindings::Action;
762
763 let split_id = self.split_manager.active_split();
764
765 let _composite = self.composite_buffers.get(&buffer_id)?;
767 let _view_state = self.composite_view_states.get(&(split_id, buffer_id))?;
768
769 match action {
770 Action::InsertTab => {
772 self.composite_focus_next(split_id, buffer_id);
773 Some(true)
774 }
775
776 Action::Copy => {
778 self.handle_composite_copy(split_id, buffer_id);
779 Some(true)
780 }
781
782 Action::MoveDown => {
784 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Down, false)
785 }
786 Action::MoveUp => {
787 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Up, false)
788 }
789 Action::MoveLeft => {
790 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Left, false)
791 }
792 Action::MoveRight => self.handle_cursor_movement_action(
793 split_id,
794 buffer_id,
795 CursorMovement::Right,
796 false,
797 ),
798 Action::MoveLineStart | Action::SmartHome => self.handle_cursor_movement_action(
799 split_id,
800 buffer_id,
801 CursorMovement::LineStart,
802 false,
803 ),
804 Action::MoveLineEnd => self.handle_cursor_movement_action(
805 split_id,
806 buffer_id,
807 CursorMovement::LineEnd,
808 false,
809 ),
810 Action::MoveWordLeft => self.handle_cursor_movement_action(
811 split_id,
812 buffer_id,
813 CursorMovement::WordLeft,
814 false,
815 ),
816 Action::MoveWordRight => self.handle_cursor_movement_action(
817 split_id,
818 buffer_id,
819 CursorMovement::WordRight,
820 false,
821 ),
822 Action::MoveWordEnd | Action::ViMoveWordEnd => self.handle_cursor_movement_action(
823 split_id,
824 buffer_id,
825 CursorMovement::WordEnd,
826 false,
827 ),
828 Action::MoveLeftInLine => {
829 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Left, false)
830 }
831 Action::MoveRightInLine => self.handle_cursor_movement_action(
832 split_id,
833 buffer_id,
834 CursorMovement::Right,
835 false,
836 ),
837
838 Action::SelectDown => {
840 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Down, true)
841 }
842 Action::SelectUp => {
843 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Up, true)
844 }
845 Action::SelectLeft => {
846 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Left, true)
847 }
848 Action::SelectRight => {
849 self.handle_cursor_movement_action(split_id, buffer_id, CursorMovement::Right, true)
850 }
851 Action::SelectLineStart => self.handle_cursor_movement_action(
852 split_id,
853 buffer_id,
854 CursorMovement::LineStart,
855 true,
856 ),
857 Action::SelectLineEnd => self.handle_cursor_movement_action(
858 split_id,
859 buffer_id,
860 CursorMovement::LineEnd,
861 true,
862 ),
863 Action::SelectWordLeft => self.handle_cursor_movement_action(
864 split_id,
865 buffer_id,
866 CursorMovement::WordLeft,
867 true,
868 ),
869 Action::SelectWordRight => self.handle_cursor_movement_action(
870 split_id,
871 buffer_id,
872 CursorMovement::WordRight,
873 true,
874 ),
875 Action::SelectWordEnd | Action::ViSelectWordEnd => self.handle_cursor_movement_action(
876 split_id,
877 buffer_id,
878 CursorMovement::WordEnd,
879 true,
880 ),
881
882 Action::MovePageDown | Action::MovePageUp => {
884 let viewport_height = self.get_composite_viewport_height(split_id);
885
886 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id))
887 {
888 if matches!(action, Action::MovePageDown) {
889 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
890 let max_row = composite.row_count().saturating_sub(1);
891 view_state.page_down(viewport_height, max_row);
892 view_state.cursor_row = view_state.scroll_row;
893 }
894 } else {
895 view_state.page_up(viewport_height);
896 view_state.cursor_row = view_state.scroll_row;
897 }
898 }
899 self.sync_editor_cursor_from_composite(split_id, buffer_id);
900 Some(true)
901 }
902
903 Action::MoveDocumentStart | Action::MoveDocumentEnd => {
905 let viewport_height = self.get_composite_viewport_height(split_id);
906
907 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id))
908 {
909 if matches!(action, Action::MoveDocumentStart) {
910 view_state.move_cursor_to_top();
911 } else if let Some(composite) = self.composite_buffers.get(&buffer_id) {
912 let max_row = composite.row_count().saturating_sub(1);
913 view_state.move_cursor_to_bottom(max_row, viewport_height);
914 }
915 }
916 self.sync_editor_cursor_from_composite(split_id, buffer_id);
917 Some(true)
918 }
919
920 Action::ScrollDown | Action::ScrollUp => {
922 let delta = if matches!(action, Action::ScrollDown) {
923 1
924 } else {
925 -1
926 };
927 self.composite_scroll(split_id, buffer_id, delta);
928 Some(true)
929 }
930
931 _ => None,
933 }
934 }
935
936 fn handle_composite_copy(&mut self, split_id: LeafId, buffer_id: BufferId) {
938 let text = {
939 let composite = match self.composite_buffers.get(&buffer_id) {
940 Some(c) => c,
941 None => return,
942 };
943 let view_state = match self.composite_view_states.get(&(split_id, buffer_id)) {
944 Some(vs) => vs,
945 None => return,
946 };
947
948 let (start_row, end_row) = match view_state.selection_row_range() {
949 Some(range) => range,
950 None => return,
951 };
952
953 let source = match composite.sources.get(view_state.focused_pane) {
954 Some(s) => s,
955 None => return,
956 };
957
958 let source_state = match self.buffers.get(&source.buffer_id) {
959 Some(s) => s,
960 None => return,
961 };
962
963 let mut text = String::new();
965 for row in start_row..=end_row {
966 if let Some(aligned_row) = composite.alignment.rows.get(row) {
967 if let Some(line_ref) = aligned_row.get_pane_line(view_state.focused_pane) {
968 if let Some(line_bytes) = source_state.buffer.get_line(line_ref.line) {
969 if !text.is_empty() {
970 text.push('\n');
971 }
972 let line_str = String::from_utf8_lossy(&line_bytes);
974 let line_trimmed = line_str.trim_end_matches(&['\n', '\r'][..]);
975 text.push_str(line_trimmed);
976 }
977 }
978 }
979 }
980 text
981 };
982
983 if !text.is_empty() {
984 self.clipboard.copy(text);
985 }
986
987 }
989
990 pub(crate) fn handle_create_composite_buffer(
996 &mut self,
997 name: String,
998 mode: String,
999 layout_config: fresh_core::api::CompositeLayoutConfig,
1000 source_configs: Vec<fresh_core::api::CompositeSourceConfig>,
1001 hunks: Option<Vec<fresh_core::api::CompositeHunk>>,
1002 _request_id: Option<u64>,
1003 ) {
1004 use crate::model::composite_buffer::{
1005 CompositeLayout, DiffHunk, GutterStyle, LineAlignment, PaneStyle, SourcePane,
1006 };
1007
1008 let layout = match layout_config.layout_type.as_str() {
1010 "stacked" => CompositeLayout::Stacked {
1011 spacing: layout_config.spacing.unwrap_or(1),
1012 },
1013 "unified" => CompositeLayout::Unified,
1014 _ => CompositeLayout::SideBySide {
1015 ratios: layout_config.ratios.unwrap_or_else(|| vec![0.5, 0.5]),
1016 show_separator: layout_config.show_separator,
1017 },
1018 };
1019
1020 let sources: Vec<SourcePane> = source_configs
1022 .into_iter()
1023 .map(|src| {
1024 let mut pane = SourcePane::new(BufferId(src.buffer_id), src.label, src.editable);
1025 if let Some(style_config) = src.style {
1026 let gutter_style = match style_config.gutter_style.as_deref() {
1027 Some("diff-markers") => GutterStyle::DiffMarkers,
1028 Some("both") => GutterStyle::Both,
1029 Some("none") => GutterStyle::None,
1030 _ => GutterStyle::LineNumbers,
1031 };
1032 let to_tuple = |arr: [u8; 3]| (arr[0], arr[1], arr[2]);
1034 pane.style = PaneStyle {
1035 add_bg: style_config.add_bg.map(to_tuple),
1036 remove_bg: style_config.remove_bg.map(to_tuple),
1037 modify_bg: style_config.modify_bg.map(to_tuple),
1038 gutter_style,
1039 };
1040 }
1041 pane
1042 })
1043 .collect();
1044
1045 let buffer_id = self.create_composite_buffer(name.clone(), mode.clone(), layout, sources);
1047
1048 if let Some(hunk_configs) = hunks {
1050 let diff_hunks: Vec<DiffHunk> = hunk_configs
1051 .into_iter()
1052 .map(|h| DiffHunk::new(h.old_start, h.old_count, h.new_start, h.new_count))
1053 .collect();
1054
1055 let old_line_count = self
1057 .buffers
1058 .get(&self.composite_buffers.get(&buffer_id).unwrap().sources[0].buffer_id)
1059 .and_then(|s| s.buffer.line_count())
1060 .unwrap_or(0);
1061 let new_line_count = self
1062 .buffers
1063 .get(&self.composite_buffers.get(&buffer_id).unwrap().sources[1].buffer_id)
1064 .and_then(|s| s.buffer.line_count())
1065 .unwrap_or(0);
1066
1067 let alignment = LineAlignment::from_hunks(&diff_hunks, old_line_count, new_line_count);
1068 self.set_composite_alignment(buffer_id, alignment);
1069 }
1070
1071 tracing::info!(
1072 "Created composite buffer '{}' with mode '{}' (id={:?})",
1073 name,
1074 mode,
1075 buffer_id
1076 );
1077
1078 if let Some(req_id) = _request_id {
1080 let result = buffer_id.0.to_string();
1082 self.plugin_manager
1083 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), result);
1084 tracing::info!(
1085 "CreateCompositeBuffer: resolve_callback sent for request_id={}",
1086 req_id
1087 );
1088 }
1089 }
1090
1091 pub(crate) fn handle_update_composite_alignment(
1093 &mut self,
1094 buffer_id: BufferId,
1095 hunk_configs: Vec<fresh_core::api::CompositeHunk>,
1096 ) {
1097 use crate::model::composite_buffer::{DiffHunk, LineAlignment};
1098
1099 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1100 let diff_hunks: Vec<DiffHunk> = hunk_configs
1101 .into_iter()
1102 .map(|h| DiffHunk::new(h.old_start, h.old_count, h.new_start, h.new_count))
1103 .collect();
1104
1105 let old_line_count = self
1107 .buffers
1108 .get(&composite.sources[0].buffer_id)
1109 .and_then(|s| s.buffer.line_count())
1110 .unwrap_or(0);
1111 let new_line_count = self
1112 .buffers
1113 .get(&composite.sources[1].buffer_id)
1114 .and_then(|s| s.buffer.line_count())
1115 .unwrap_or(0);
1116
1117 let alignment = LineAlignment::from_hunks(&diff_hunks, old_line_count, new_line_count);
1118 self.set_composite_alignment(buffer_id, alignment);
1119 }
1120 }
1121
1122 pub(crate) fn handle_composite_click(
1124 &mut self,
1125 col: u16,
1126 row: u16,
1127 split_id: LeafId,
1128 buffer_id: BufferId,
1129 content_rect: ratatui::layout::Rect,
1130 ) -> AnyhowResult<()> {
1131 let pane_idx =
1133 if let Some(view_state) = self.composite_view_states.get(&(split_id, buffer_id)) {
1134 let mut x = content_rect.x;
1135 let mut found_pane = 0;
1136 for (i, &width) in view_state.pane_widths.iter().enumerate() {
1137 if col >= x && col < x + width {
1138 found_pane = i;
1139 break;
1140 }
1141 x += width + 1; }
1143 found_pane
1144 } else {
1145 0
1146 };
1147
1148 let content_row = row.saturating_sub(content_rect.y).saturating_sub(1) as usize;
1151
1152 let (pane_start_x, left_column) =
1154 if let Some(view_state) = self.composite_view_states.get(&(split_id, buffer_id)) {
1155 let mut x = content_rect.x;
1156 for (i, &width) in view_state.pane_widths.iter().enumerate() {
1157 if i == pane_idx {
1158 break;
1159 }
1160 x += width + 1;
1161 }
1162 let left_col = view_state
1163 .pane_viewports
1164 .get(pane_idx)
1165 .map(|vp| vp.left_column)
1166 .unwrap_or(0);
1167 (x, left_col)
1168 } else {
1169 (content_rect.x, 0)
1170 };
1171 let gutter_width = 4; let visual_col = col
1173 .saturating_sub(pane_start_x)
1174 .saturating_sub(gutter_width) as usize;
1175 let click_col = left_column + visual_col;
1177
1178 let display_row =
1180 if let Some(view_state) = self.composite_view_states.get(&(split_id, buffer_id)) {
1181 view_state.scroll_row + content_row
1182 } else {
1183 content_row
1184 };
1185
1186 let line_length = if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1187 composite
1188 .alignment
1189 .get_row(display_row)
1190 .and_then(|row| row.get_pane_line(pane_idx))
1191 .and_then(|line_ref| {
1192 let source = composite.sources.get(pane_idx)?;
1193 self.buffers
1194 .get(&source.buffer_id)?
1195 .buffer
1196 .get_line(line_ref.line)
1197 })
1198 .map(|bytes| {
1199 let s = String::from_utf8_lossy(&bytes);
1200 let trimmed = s.trim_end_matches('\n').trim_end_matches('\r');
1202 trimmed.graphemes(true).count()
1203 })
1204 .unwrap_or(0)
1205 } else {
1206 0
1207 };
1208
1209 let clamped_col = click_col.min(line_length);
1211
1212 if let Some(composite) = self.composite_buffers.get_mut(&buffer_id) {
1214 composite.active_pane = pane_idx;
1215 }
1216
1217 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
1219 view_state.focused_pane = pane_idx;
1220 view_state.cursor_row = display_row;
1221 view_state.cursor_column = clamped_col;
1222 view_state.sticky_column = clamped_col;
1223
1224 view_state.clear_selection();
1226 }
1227
1228 self.mouse_state.dragging_text_selection = false; self.mouse_state.drag_selection_split = Some(split_id);
1231
1232 self.sync_editor_cursor_from_composite(split_id, buffer_id);
1234
1235 Ok(())
1236 }
1237}