1use super::*;
2use anyhow::Result as AnyhowResult;
3use rust_i18n::t;
4
5enum SearchDirection {
6 Forward,
7 Backward,
8}
9
10impl Editor {
11 pub fn render(&mut self, frame: &mut Frame) {
13 let _span = tracing::info_span!("render").entered();
14 let size = frame.area();
15
16 self.cached_layout.last_frame_width = size.width;
18 self.cached_layout.last_frame_height = size.height;
19
20 self.cached_layout.reset_cell_theme_map();
22
23 let active_split = self.split_manager.active_split();
28 {
29 let _span = tracing::info_span!("pre_sync_ensure_visible").entered();
30 self.pre_sync_ensure_visible(active_split);
31 }
32
33 {
36 let _span = tracing::info_span!("sync_scroll_groups").entered();
37 self.sync_scroll_groups();
38 }
39
40 let mut semantic_ranges: std::collections::HashMap<BufferId, (usize, usize)> =
46 std::collections::HashMap::new();
47 {
48 let _span = tracing::info_span!("compute_semantic_ranges").entered();
49 for (split_id, view_state) in &self.split_view_states {
50 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
51 if let Some(state) = self.buffers.get(&buffer_id) {
52 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
53 let visible_lines =
54 view_state.viewport.visible_line_count().saturating_sub(1);
55 let end_line = start_line.saturating_add(visible_lines);
56 semantic_ranges
57 .entry(buffer_id)
58 .and_modify(|(min_start, max_end)| {
59 *min_start = (*min_start).min(start_line);
60 *max_end = (*max_end).max(end_line);
61 })
62 .or_insert((start_line, end_line));
63 }
64 }
65 }
66 }
67 for (buffer_id, (start_line, end_line)) in semantic_ranges {
68 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
69 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
70 self.maybe_request_folding_ranges_debounced(buffer_id);
71 }
72
73 {
74 let _span = tracing::info_span!("prepare_for_render").entered();
75 for (split_id, view_state) in &self.split_view_states {
76 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
77 if let Some(state) = self.buffers.get_mut(&buffer_id) {
78 let top_byte = view_state.viewport.top_byte;
79 let height = view_state.viewport.height;
80 if let Err(e) = state.prepare_for_render(top_byte, height) {
81 tracing::error!("Failed to prepare buffer for render: {}", e);
82 }
84 }
85 }
86 }
87 }
88
89 let is_search_prompt_active = self.prompt.as_ref().is_some_and(|p| {
92 matches!(
93 p.prompt_type,
94 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
95 )
96 });
97 if is_search_prompt_active {
98 if let Some(ref search_state) = self.search_state {
99 let query = search_state.query.clone();
100 self.update_search_highlights(&query);
101 }
102 }
103
104 let show_search_options = self.prompt.as_ref().is_some_and(|p| {
106 matches!(
107 p.prompt_type,
108 PromptType::Search
109 | PromptType::ReplaceSearch
110 | PromptType::Replace { .. }
111 | PromptType::QueryReplaceSearch
112 | PromptType::QueryReplace { .. }
113 )
114 });
115
116 let has_suggestions = self
118 .prompt
119 .as_ref()
120 .is_some_and(|p| !p.suggestions.is_empty());
121 let has_file_browser = self.prompt.as_ref().is_some_and(|p| {
122 matches!(
123 p.prompt_type,
124 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
125 )
126 }) && self.file_open_state.is_some();
127
128 let constraints = vec![
132 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }), Constraint::Min(0), Constraint::Length(
135 if !self.status_bar_visible || has_suggestions || has_file_browser {
136 0
137 } else {
138 1
139 },
140 ), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(if self.prompt_line_visible || self.prompt.is_some() {
143 1
144 } else {
145 0
146 }), ];
148
149 let main_chunks = Layout::default()
150 .direction(Direction::Vertical)
151 .constraints(constraints)
152 .split(size);
153
154 let menu_bar_area = main_chunks[0];
155 let main_content_area = main_chunks[1];
156 let status_bar_idx = 2;
157 let search_options_idx = 3;
158 let prompt_line_idx = 4;
159
160 let editor_content_area;
163 let file_explorer_should_show = self.file_explorer_visible
164 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
165
166 if file_explorer_should_show {
167 tracing::trace!(
169 "render: file explorer layout active (present={}, sync_in_progress={})",
170 self.file_explorer.is_some(),
171 self.file_explorer_sync_in_progress
172 );
173 let explorer_percent = (self.file_explorer_width_percent * 100.0) as u16;
175 let editor_percent = 100 - explorer_percent;
176 let horizontal_chunks = Layout::default()
177 .direction(Direction::Horizontal)
178 .constraints([
179 Constraint::Percentage(explorer_percent), Constraint::Percentage(editor_percent), ])
182 .split(main_content_area);
183
184 self.cached_layout.file_explorer_area = Some(horizontal_chunks[0]);
185 editor_content_area = horizontal_chunks[1];
186
187 let remote_connection = self.remote_connection_info().map(|s| s.to_string());
189
190 if let Some(ref mut explorer) = self.file_explorer {
192 let is_focused = self.key_context == KeyContext::FileExplorer;
193
194 let mut files_with_unsaved_changes = std::collections::HashSet::new();
196 for (buffer_id, state) in &self.buffers {
197 if state.buffer.is_modified() {
198 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
199 if let Some(file_path) = metadata.file_path() {
200 files_with_unsaved_changes.insert(file_path.clone());
201 }
202 }
203 }
204 }
205
206 let close_button_hovered = matches!(
207 &self.mouse_state.hover_target,
208 Some(HoverTarget::FileExplorerCloseButton)
209 );
210 FileExplorerRenderer::render(
211 explorer,
212 frame,
213 horizontal_chunks[0],
214 is_focused,
215 &files_with_unsaved_changes,
216 &self.file_explorer_decoration_cache,
217 &self.keybindings,
218 self.key_context.clone(),
219 &self.theme,
220 close_button_hovered,
221 remote_connection.as_deref(),
222 );
223 }
224 } else {
227 self.cached_layout.file_explorer_area = None;
229 editor_content_area = main_content_area;
230 }
231
232 if self.plugin_manager.is_active() {
239 let hooks_start = std::time::Instant::now();
240 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
242
243 let mut total_new_lines = 0usize;
244 for (split_id, buffer_id, split_area) in visible_buffers {
245 let viewport_top_byte = self
247 .split_view_states
248 .get(&split_id)
249 .map(|vs| vs.viewport.top_byte)
250 .unwrap_or(0);
251
252 if let Some(state) = self.buffers.get_mut(&buffer_id) {
253 self.plugin_manager.run_hook(
255 "render_start",
256 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
257 );
258
259 let visible_count = split_area.height as usize;
262 let is_binary = state.buffer.is_binary();
263 let line_ending = state.buffer.line_ending();
264 let base_tokens =
265 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
266 &mut state.buffer,
267 viewport_top_byte,
268 self.config.editor.estimated_line_length,
269 visible_count,
270 is_binary,
271 line_ending,
272 );
273 let viewport_start = viewport_top_byte;
274 let viewport_end = base_tokens
275 .last()
276 .and_then(|t| t.source_offset)
277 .unwrap_or(viewport_start);
278 let cursor_positions: Vec<usize> = self
279 .split_view_states
280 .get(&split_id)
281 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
282 .unwrap_or_default();
283 self.plugin_manager.run_hook(
284 "view_transform_request",
285 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
286 buffer_id,
287 split_id: split_id.into(),
288 viewport_start,
289 viewport_end,
290 tokens: base_tokens,
291 cursor_positions,
292 },
293 );
294
295 if let Some(vs) = self.split_view_states.get_mut(&split_id) {
299 vs.view_transform_stale = false;
300 }
301
302 let visible_count = split_area.height as usize;
304 let top_byte = viewport_top_byte;
305
306 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
308
309 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
311 let mut line_number = state.buffer.get_line_number(top_byte);
312 let mut iter = state
313 .buffer
314 .line_iterator(top_byte, self.config.editor.estimated_line_length);
315
316 for _ in 0..visible_count {
317 if let Some((line_start, line_content)) = iter.next_line() {
318 let byte_end = line_start + line_content.len();
319 let byte_range = (line_start, byte_end);
320
321 if !seen_byte_ranges.contains(&byte_range) {
323 new_lines.push(crate::services::plugins::hooks::LineInfo {
324 line_number,
325 byte_start: line_start,
326 byte_end,
327 content: line_content,
328 });
329 seen_byte_ranges.insert(byte_range);
330 }
331 line_number += 1;
332 } else {
333 break;
334 }
335 }
336
337 if !new_lines.is_empty() {
339 total_new_lines += new_lines.len();
340 self.plugin_manager.run_hook(
341 "lines_changed",
342 crate::services::plugins::hooks::HookArgs::LinesChanged {
343 buffer_id,
344 lines: new_lines,
345 },
346 );
347 }
348 }
349 }
350 let hooks_elapsed = hooks_start.elapsed();
351 tracing::trace!(
352 new_lines = total_new_lines,
353 elapsed_ms = hooks_elapsed.as_millis(),
354 elapsed_us = hooks_elapsed.as_micros(),
355 "lines_changed hooks total"
356 );
357
358 let commands = self.plugin_manager.process_commands();
370 if !commands.is_empty() {
371 let cmd_names: Vec<String> =
372 commands.iter().map(|c| c.debug_variant_name()).collect();
373 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
374 }
375 for command in commands {
376 if let Err(e) = self.handle_plugin_command(command) {
377 tracing::error!("Error handling plugin command: {}", e);
378 }
379 }
380
381 self.flush_pending_grammars();
383 }
384
385 let lsp_waiting = !self.pending_completion_requests.is_empty()
387 || self.pending_goto_definition_request.is_some();
388
389 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
396 let hide_cursor = self.menu_state.active_menu.is_some()
397 || self.key_context == KeyContext::FileExplorer
398 || self.terminal_mode
399 || settings_visible
400 || self.keybinding_editor.is_some();
401
402 let hovered_tab = match &self.mouse_state.hover_target {
404 Some(HoverTarget::TabName(buffer_id, split_id)) => Some((*buffer_id, *split_id, false)),
405 Some(HoverTarget::TabCloseButton(buffer_id, split_id)) => {
406 Some((*buffer_id, *split_id, true))
407 }
408 _ => None,
409 };
410
411 let hovered_close_split = match &self.mouse_state.hover_target {
413 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
414 _ => None,
415 };
416
417 let hovered_maximize_split = match &self.mouse_state.hover_target {
419 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
420 _ => None,
421 };
422
423 let is_maximized = self.split_manager.is_maximized();
424
425 let _content_span = tracing::info_span!("render_content").entered();
426 let (
427 split_areas,
428 tab_layouts,
429 close_split_areas,
430 maximize_split_areas,
431 view_line_mappings,
432 horizontal_scrollbar_areas,
433 ) = SplitRenderer::render_content(
434 frame,
435 editor_content_area,
436 &self.split_manager,
437 &mut self.buffers,
438 &self.buffer_metadata,
439 &mut self.event_logs,
440 &self.composite_buffers,
441 &mut self.composite_view_states,
442 &self.theme,
443 self.ansi_background.as_ref(),
444 self.background_fade,
445 lsp_waiting,
446 self.config.editor.large_file_threshold_bytes,
447 self.config.editor.line_wrap,
448 self.config.editor.estimated_line_length,
449 self.config.editor.highlight_context_bytes,
450 Some(&mut self.split_view_states),
451 hide_cursor,
452 hovered_tab,
453 hovered_close_split,
454 hovered_maximize_split,
455 is_maximized,
456 self.config.editor.relative_line_numbers,
457 self.tab_bar_visible,
458 self.config.editor.use_terminal_bg,
459 self.session_mode || !self.software_cursor_only,
460 self.software_cursor_only,
461 self.config.editor.show_vertical_scrollbar,
462 self.config.editor.show_horizontal_scrollbar,
463 self.config.editor.diagnostics_inline_text,
464 self.config.editor.show_tilde,
465 &mut self.cached_layout.cell_theme_map,
466 size.width,
467 );
468
469 drop(_content_span);
470
471 if self.plugin_manager.is_active() {
475 for (split_id, view_state) in &self.split_view_states {
476 let current = (
477 view_state.viewport.top_byte,
478 view_state.viewport.width,
479 view_state.viewport.height,
480 );
481 let (changed, previous) = match self.previous_viewports.get(split_id) {
486 Some(previous) => (*previous != current, Some(*previous)),
487 None => (false, None), };
489 tracing::trace!(
490 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
491 split_id,
492 current,
493 previous,
494 changed
495 );
496 if changed {
497 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
498 let top_line = self.buffers.get(&buffer_id).and_then(|state| {
500 if state.buffer.line_count().is_some() {
501 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
502 } else {
503 None
504 }
505 });
506 tracing::debug!(
507 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
508 split_id,
509 buffer_id,
510 view_state.viewport.top_byte,
511 top_line
512 );
513 self.plugin_manager.run_hook(
514 "viewport_changed",
515 crate::services::plugins::hooks::HookArgs::ViewportChanged {
516 split_id: (*split_id).into(),
517 buffer_id,
518 top_byte: view_state.viewport.top_byte,
519 top_line,
520 width: view_state.viewport.width,
521 height: view_state.viewport.height,
522 },
523 );
524 }
525 }
526 }
527 }
528
529 self.previous_viewports.clear();
531 for (split_id, view_state) in &self.split_view_states {
532 self.previous_viewports.insert(
533 *split_id,
534 (
535 view_state.viewport.top_byte,
536 view_state.viewport.width,
537 view_state.viewport.height,
538 ),
539 );
540 }
541
542 self.render_terminal_splits(frame, &split_areas);
544
545 self.cached_layout.split_areas = split_areas;
546 self.cached_layout.horizontal_scrollbar_areas = horizontal_scrollbar_areas;
547 self.cached_layout.tab_layouts = tab_layouts;
548 self.cached_layout.close_split_areas = close_split_areas;
549 self.cached_layout.maximize_split_areas = maximize_split_areas;
550 self.cached_layout.view_line_mappings = view_line_mappings;
551 self.cached_layout.separator_areas = self
552 .split_manager
553 .get_separators_with_ids(editor_content_area);
554 self.cached_layout.editor_content_area = Some(editor_content_area);
555
556 self.render_hover_highlights(frame);
558
559 self.cached_layout.suggestions_area = None;
561 self.file_browser_layout = None;
562
563 let display_name = self
565 .buffer_metadata
566 .get(&self.active_buffer())
567 .map(|m| m.display_name.clone())
568 .unwrap_or_else(|| "[No Name]".to_string());
569 let status_message = self.status_message.clone();
570 let plugin_status_message = self.plugin_status_message.clone();
571 let prompt = self.prompt.clone();
572 let lsp_status = self.lsp_status.clone();
573 let theme = self.theme.clone();
574 let keybindings_cloned = self.keybindings.clone(); let chord_state_cloned = self.chord_state.clone(); let update_available = self.latest_version().map(|v| v.to_string());
579
580 if self.status_bar_visible && !has_suggestions && !has_file_browser {
582 let (warning_level, general_warning_count) =
584 if self.config.warnings.show_status_indicator {
585 (
586 self.get_effective_warning_level(),
587 self.get_general_warning_count(),
588 )
589 } else {
590 (WarningLevel::None, 0)
591 };
592
593 use crate::view::ui::status_bar::StatusBarHover;
595 let status_bar_hover = match &self.mouse_state.hover_target {
596 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
597 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
598 Some(HoverTarget::StatusBarLineEndingIndicator) => {
599 StatusBarHover::LineEndingIndicator
600 }
601 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
602 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
603 _ => StatusBarHover::None,
604 };
605
606 let remote_connection = self.remote_connection_info().map(|s| s.to_string());
608
609 let session_name = self.session_name().map(|s| s.to_string());
611
612 let active_split = self.split_manager.active_split();
613 let active_buf = self.active_buffer();
614 let default_cursors = crate::model::cursor::Cursors::new();
615 let status_cursors = self
616 .split_view_states
617 .get(&active_split)
618 .map(|vs| &vs.cursors)
619 .unwrap_or(&default_cursors);
620 let is_read_only = self
621 .buffer_metadata
622 .get(&active_buf)
623 .map(|m| m.read_only)
624 .unwrap_or(false);
625 let status_bar_layout = StatusBarRenderer::render_status_bar(
626 frame,
627 main_chunks[status_bar_idx],
628 self.buffers.get_mut(&active_buf).unwrap(),
629 status_cursors,
630 &status_message,
631 &plugin_status_message,
632 &lsp_status,
633 &theme,
634 &display_name,
635 &keybindings_cloned, &chord_state_cloned, update_available.as_deref(), warning_level, general_warning_count, status_bar_hover, remote_connection.as_deref(), session_name.as_deref(), is_read_only, );
645
646 let status_bar_area = main_chunks[status_bar_idx];
648 self.cached_layout.status_bar_area =
649 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
650 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
651 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
652 self.cached_layout.status_bar_line_ending_area =
653 status_bar_layout.line_ending_indicator;
654 self.cached_layout.status_bar_encoding_area = status_bar_layout.encoding_indicator;
655 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
656 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
657 }
658
659 if show_search_options {
661 let confirm_each = self.prompt.as_ref().and_then(|p| {
663 if matches!(
664 p.prompt_type,
665 PromptType::ReplaceSearch
666 | PromptType::Replace { .. }
667 | PromptType::QueryReplaceSearch
668 | PromptType::QueryReplace { .. }
669 ) {
670 Some(self.search_confirm_each)
671 } else {
672 None
673 }
674 });
675
676 use crate::view::ui::status_bar::SearchOptionsHover;
678 let search_options_hover = match &self.mouse_state.hover_target {
679 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
680 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
681 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
682 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
683 _ => SearchOptionsHover::None,
684 };
685
686 let search_options_layout = StatusBarRenderer::render_search_options(
687 frame,
688 main_chunks[search_options_idx],
689 self.search_case_sensitive,
690 self.search_whole_word,
691 self.search_use_regex,
692 confirm_each,
693 &theme,
694 &keybindings_cloned,
695 search_options_hover,
696 );
697 self.cached_layout.search_options_layout = Some(search_options_layout);
698 } else {
699 self.cached_layout.search_options_layout = None;
700 }
701
702 if let Some(prompt) = &prompt {
704 if matches!(
706 prompt.prompt_type,
707 crate::view::prompt::PromptType::OpenFile
708 | crate::view::prompt::PromptType::SwitchProject
709 ) {
710 if let Some(file_open_state) = &self.file_open_state {
711 StatusBarRenderer::render_file_open_prompt(
712 frame,
713 main_chunks[prompt_line_idx],
714 prompt,
715 file_open_state,
716 &theme,
717 );
718 } else {
719 StatusBarRenderer::render_prompt(
720 frame,
721 main_chunks[prompt_line_idx],
722 prompt,
723 &theme,
724 );
725 }
726 } else {
727 StatusBarRenderer::render_prompt(
728 frame,
729 main_chunks[prompt_line_idx],
730 prompt,
731 &theme,
732 );
733 }
734 }
735
736 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
739
740 let theme_clone = self.theme.clone();
743 let hover_target = self.mouse_state.hover_target.clone();
744
745 self.cached_layout.popup_areas.clear();
747
748 let popup_info: Vec<_> = {
750 let active_split = self.split_manager.active_split();
752 let viewport = self
753 .split_view_states
754 .get(&active_split)
755 .map(|vs| vs.viewport.clone());
756
757 let content_rect = self
762 .cached_layout
763 .split_areas
764 .iter()
765 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
766 .map(|(_, _, rect, _, _, _)| *rect);
767
768 let primary_cursor = self
769 .split_view_states
770 .get(&active_split)
771 .map(|vs| *vs.cursors.primary());
772 let state = self.active_state_mut();
773 if state.popups.is_visible() {
774 let primary_cursor =
776 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
777
778 let gutter_width = viewport
780 .as_ref()
781 .map(|vp| vp.gutter_width(&state.buffer) as u16)
782 .unwrap_or(0);
783
784 let cursor_screen_pos = viewport
785 .as_ref()
786 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
787 .unwrap_or((0, 0));
788
789 let word_start_screen_pos = {
793 use crate::primitives::word_navigation::find_completion_word_start;
794 let word_start =
795 find_completion_word_start(&state.buffer, primary_cursor.position);
796 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
797 viewport
798 .as_ref()
799 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
800 .unwrap_or((0, 0))
801 };
802
803 let (base_x, base_y) = content_rect
808 .map(|r| (r.x + gutter_width, r.y))
809 .unwrap_or((gutter_width, 1));
810
811 let cursor_screen_pos =
812 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
813 let word_start_screen_pos = (
814 word_start_screen_pos.0 + base_x,
815 word_start_screen_pos.1 + base_y,
816 );
817
818 state
820 .popups
821 .all()
822 .iter()
823 .enumerate()
824 .map(|(popup_idx, popup)| {
825 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
827 (word_start_screen_pos.0, cursor_screen_pos.1)
828 } else {
829 cursor_screen_pos
830 };
831 let popup_area = popup.calculate_area(size, Some(popup_pos));
832
833 let desc_height = popup.description_height();
836 let inner_area = if popup.bordered {
837 ratatui::layout::Rect {
838 x: popup_area.x + 1,
839 y: popup_area.y + 1 + desc_height,
840 width: popup_area.width.saturating_sub(2),
841 height: popup_area.height.saturating_sub(2 + desc_height),
842 }
843 } else {
844 ratatui::layout::Rect {
845 x: popup_area.x,
846 y: popup_area.y + desc_height,
847 width: popup_area.width,
848 height: popup_area.height.saturating_sub(desc_height),
849 }
850 };
851
852 let num_items = match &popup.content {
853 crate::view::popup::PopupContent::List { items, .. } => items.len(),
854 _ => 0,
855 };
856
857 let total_lines = popup.item_count();
859 let visible_lines = inner_area.height as usize;
860 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
861 {
862 Some(ratatui::layout::Rect {
863 x: inner_area.x + inner_area.width - 1,
864 y: inner_area.y,
865 width: 1,
866 height: inner_area.height,
867 })
868 } else {
869 None
870 };
871
872 (
873 popup_idx,
874 popup_area,
875 inner_area,
876 popup.scroll_offset,
877 num_items,
878 scrollbar_rect,
879 total_lines,
880 )
881 })
882 .collect()
883 } else {
884 Vec::new()
885 }
886 };
887
888 self.cached_layout.popup_areas = popup_info.clone();
890
891 let state = self.active_state_mut();
893 if state.popups.is_visible() {
894 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
895 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
896 popup.render_with_hover(
897 frame,
898 *popup_area,
899 &theme_clone,
900 hover_target.as_ref(),
901 );
902 }
903 }
904 }
905
906 self.update_menu_context();
909
910 let settings_visible = self
913 .settings_state
914 .as_ref()
915 .map(|s| s.visible)
916 .unwrap_or(false);
917 if settings_visible {
918 crate::view::dimming::apply_dimming(frame, size);
920 }
921 if let Some(ref mut settings_state) = self.settings_state {
922 if settings_state.visible {
923 settings_state.update_focus_states();
924 let settings_layout = crate::view::settings::render_settings(
925 frame,
926 size,
927 settings_state,
928 &self.theme,
929 );
930 self.cached_layout.settings_layout = Some(settings_layout);
931 }
932 }
933
934 if let Some(ref wizard) = self.calibration_wizard {
936 crate::view::dimming::apply_dimming(frame, size);
938 crate::view::calibration_wizard::render_calibration_wizard(
939 frame,
940 size,
941 wizard,
942 &self.theme,
943 );
944 }
945
946 if let Some(ref mut kb_editor) = self.keybinding_editor {
948 crate::view::dimming::apply_dimming(frame, size);
949 crate::view::keybinding_editor::render_keybinding_editor(
950 frame,
951 size,
952 kb_editor,
953 &self.theme,
954 );
955 }
956
957 if let Some(ref debug) = self.event_debug {
959 crate::view::dimming::apply_dimming(frame, size);
961 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
962 }
963
964 if self.menu_bar_visible {
965 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
966 frame,
967 menu_bar_area,
968 &self.menus,
969 &self.menu_state,
970 &self.keybindings,
971 &self.theme,
972 self.mouse_state.hover_target.as_ref(),
973 self.config.editor.menu_bar_mnemonics,
974 ));
975 } else {
976 self.cached_layout.menu_layout = None;
977 }
978
979 if let Some(ref menu) = self.tab_context_menu {
981 self.render_tab_context_menu(frame, menu);
982 }
983
984 self.record_non_editor_theme_regions();
986
987 self.render_theme_info_popup(frame);
989
990 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
992 if drag_state.is_dragging() {
993 self.render_tab_drop_zone(frame, drag_state);
994 }
995 }
996
997 if self.gpm_active {
1003 if let Some((col, row)) = self.mouse_cursor_position {
1004 use ratatui::style::Modifier;
1005
1006 if col < size.width && row < size.height {
1008 let buf = frame.buffer_mut();
1010 if let Some(cell) = buf.cell_mut((col, row)) {
1011 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1012 }
1013 }
1014 }
1015 }
1016
1017 if self.keyboard_capture && self.terminal_mode {
1020 let active_split = self.split_manager.active_split();
1022 let active_split_area = self
1023 .cached_layout
1024 .split_areas
1025 .iter()
1026 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1027 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1028
1029 if let Some(terminal_area) = active_split_area {
1030 self.apply_keyboard_capture_dimming(frame, terminal_area);
1031 }
1032 }
1033
1034 crate::view::color_support::convert_buffer_colors(
1036 frame.buffer_mut(),
1037 self.color_capability,
1038 );
1039 }
1040
1041 fn render_quick_open_hints(
1043 frame: &mut Frame,
1044 area: ratatui::layout::Rect,
1045 theme: &crate::view::theme::Theme,
1046 ) {
1047 use ratatui::style::{Modifier, Style};
1048 use ratatui::text::{Line, Span};
1049 use ratatui::widgets::Paragraph;
1050 use rust_i18n::t;
1051
1052 let hints_style = Style::default()
1053 .fg(theme.line_number_fg)
1054 .bg(theme.suggestion_selected_bg)
1055 .add_modifier(Modifier::DIM);
1056 let hints_text = t!("quick_open.mode_hints");
1057 let left_margin = 2;
1059 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1060 let mut spans = Vec::new();
1061 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1062 spans.push(Span::styled(hints_text.to_string(), hints_style));
1063 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1064 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1065
1066 let paragraph = Paragraph::new(Line::from(spans));
1067 frame.render_widget(paragraph, area);
1068 }
1069
1070 fn apply_keyboard_capture_dimming(
1073 &self,
1074 frame: &mut Frame,
1075 terminal_area: ratatui::layout::Rect,
1076 ) {
1077 let size = frame.area();
1078 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1079 }
1080
1081 fn render_prompt_popups(
1084 &mut self,
1085 frame: &mut Frame,
1086 prompt_area: ratatui::layout::Rect,
1087 width: u16,
1088 ) {
1089 let Some(prompt) = &self.prompt else { return };
1090
1091 if matches!(
1092 prompt.prompt_type,
1093 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1094 ) {
1095 let Some(file_open_state) = &self.file_open_state else {
1096 return;
1097 };
1098 let max_height = prompt_area.y.saturating_sub(1).min(20);
1099 let popup_area = ratatui::layout::Rect {
1100 x: 0,
1101 y: prompt_area.y.saturating_sub(max_height),
1102 width,
1103 height: max_height,
1104 };
1105 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1106 frame,
1107 popup_area,
1108 file_open_state,
1109 &self.theme,
1110 &self.mouse_state.hover_target,
1111 Some(&self.keybindings),
1112 );
1113 return;
1114 }
1115
1116 if prompt.suggestions.is_empty() {
1117 return;
1118 }
1119
1120 let suggestion_count = prompt.suggestions.len().min(10);
1121 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1122 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1123 let height = suggestion_count as u16 + 2 + hints_height;
1124
1125 let suggestions_area = ratatui::layout::Rect {
1126 x: 0,
1127 y: prompt_area.y.saturating_sub(height),
1128 width,
1129 height: height - hints_height,
1130 };
1131
1132 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1133
1134 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
1135 frame,
1136 suggestions_area,
1137 prompt,
1138 &self.theme,
1139 self.mouse_state.hover_target.as_ref(),
1140 );
1141
1142 if is_quick_open {
1143 let hints_area = ratatui::layout::Rect {
1144 x: 0,
1145 y: prompt_area.y.saturating_sub(hints_height),
1146 width,
1147 height: hints_height,
1148 };
1149 frame.render_widget(ratatui::widgets::Clear, hints_area);
1150 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1151 }
1152 }
1153
1154 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
1156 use ratatui::style::Style;
1157 use ratatui::text::Span;
1158 use ratatui::widgets::Paragraph;
1159
1160 match &self.mouse_state.hover_target {
1161 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
1162 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
1164 if sid == split_id && dir == direction {
1165 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1166 match dir {
1167 SplitDirection::Horizontal => {
1168 let line_text = "─".repeat(*length as usize);
1169 let paragraph =
1170 Paragraph::new(Span::styled(line_text, hover_style));
1171 frame.render_widget(
1172 paragraph,
1173 ratatui::layout::Rect::new(*x, *y, *length, 1),
1174 );
1175 }
1176 SplitDirection::Vertical => {
1177 for offset in 0..*length {
1178 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1179 frame.render_widget(
1180 paragraph,
1181 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
1182 );
1183 }
1184 }
1185 }
1186 }
1187 }
1188 }
1189 Some(HoverTarget::ScrollbarThumb(split_id)) => {
1190 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1192 &self.cached_layout.split_areas
1193 {
1194 if sid == split_id {
1195 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
1196 for row_offset in *thumb_start..*thumb_end {
1197 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
1198 frame.render_widget(
1199 paragraph,
1200 ratatui::layout::Rect::new(
1201 scrollbar_rect.x,
1202 scrollbar_rect.y + row_offset as u16,
1203 1,
1204 1,
1205 ),
1206 );
1207 }
1208 }
1209 }
1210 }
1211 Some(HoverTarget::ScrollbarTrack(split_id)) => {
1212 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1214 &self.cached_layout.split_areas
1215 {
1216 if sid == split_id {
1217 let track_hover_style =
1218 Style::default().bg(self.theme.scrollbar_track_hover_fg);
1219 let thumb_style = Style::default().bg(self.theme.scrollbar_thumb_fg);
1220 for row_offset in 0..scrollbar_rect.height {
1221 let is_thumb = (row_offset as usize) >= *thumb_start
1222 && (row_offset as usize) < *thumb_end;
1223 let style = if is_thumb {
1224 thumb_style
1225 } else {
1226 track_hover_style
1227 };
1228 let paragraph = Paragraph::new(Span::styled(" ", style));
1229 frame.render_widget(
1230 paragraph,
1231 ratatui::layout::Rect::new(
1232 scrollbar_rect.x,
1233 scrollbar_rect.y + row_offset,
1234 1,
1235 1,
1236 ),
1237 );
1238 }
1239 }
1240 }
1241 }
1242 Some(HoverTarget::FileExplorerBorder) => {
1243 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1245 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1246 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1247 for row_offset in 0..explorer_area.height {
1248 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1249 frame.render_widget(
1250 paragraph,
1251 ratatui::layout::Rect::new(
1252 border_x,
1253 explorer_area.y + row_offset,
1254 1,
1255 1,
1256 ),
1257 );
1258 }
1259 }
1260 }
1261 _ => {}
1263 }
1264 }
1265
1266 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
1268 use ratatui::style::Style;
1269 use ratatui::text::{Line, Span};
1270 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1271
1272 let items = super::types::TabContextMenuItem::all();
1273 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
1278 let screen_height = frame.area().height;
1279
1280 let menu_x = if menu.position.0 + menu_width > screen_width {
1281 screen_width.saturating_sub(menu_width)
1282 } else {
1283 menu.position.0
1284 };
1285
1286 let menu_y = if menu.position.1 + menu_height > screen_height {
1287 screen_height.saturating_sub(menu_height)
1288 } else {
1289 menu.position.1
1290 };
1291
1292 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1293
1294 frame.render_widget(Clear, area);
1296
1297 let mut lines = Vec::new();
1299 for (idx, item) in items.iter().enumerate() {
1300 let is_highlighted = idx == menu.highlighted;
1301
1302 let style = if is_highlighted {
1303 Style::default()
1304 .fg(self.theme.menu_highlight_fg)
1305 .bg(self.theme.menu_highlight_bg)
1306 } else {
1307 Style::default()
1308 .fg(self.theme.menu_dropdown_fg)
1309 .bg(self.theme.menu_dropdown_bg)
1310 };
1311
1312 let label = item.label();
1314 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1316
1317 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1318 }
1319
1320 let block = Block::default()
1321 .borders(Borders::ALL)
1322 .border_style(Style::default().fg(self.theme.menu_border_fg))
1323 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1324
1325 let paragraph = Paragraph::new(lines).block(block);
1326 frame.render_widget(paragraph, area);
1327 }
1328
1329 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
1331 use ratatui::style::Modifier;
1332
1333 let Some(ref drop_zone) = drag_state.drop_zone else {
1334 return;
1335 };
1336
1337 let split_id = drop_zone.split_id();
1338
1339 let split_area = self
1341 .cached_layout
1342 .split_areas
1343 .iter()
1344 .find(|(sid, _, _, _, _, _)| *sid == split_id)
1345 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1346
1347 let Some(content_rect) = split_area else {
1348 return;
1349 };
1350
1351 use super::types::TabDropZone;
1353
1354 let highlight_area = match drop_zone {
1355 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
1356 content_rect
1359 }
1360 TabDropZone::SplitLeft(_) => {
1361 let width = (content_rect.width / 2).max(3);
1363 ratatui::layout::Rect::new(
1364 content_rect.x,
1365 content_rect.y,
1366 width,
1367 content_rect.height,
1368 )
1369 }
1370 TabDropZone::SplitRight(_) => {
1371 let width = (content_rect.width / 2).max(3);
1373 let x = content_rect.x + content_rect.width - width;
1374 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
1375 }
1376 TabDropZone::SplitTop(_) => {
1377 let height = (content_rect.height / 2).max(2);
1379 ratatui::layout::Rect::new(
1380 content_rect.x,
1381 content_rect.y,
1382 content_rect.width,
1383 height,
1384 )
1385 }
1386 TabDropZone::SplitBottom(_) => {
1387 let height = (content_rect.height / 2).max(2);
1389 let y = content_rect.y + content_rect.height - height;
1390 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
1391 }
1392 };
1393
1394 let buf = frame.buffer_mut();
1397 let drop_zone_bg = self.theme.tab_drop_zone_bg;
1398 let drop_zone_border = self.theme.tab_drop_zone_border;
1399
1400 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1402 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1403 if let Some(cell) = buf.cell_mut((x, y)) {
1404 cell.set_bg(drop_zone_bg);
1407
1408 let is_border = x == highlight_area.x
1410 || x == highlight_area.x + highlight_area.width - 1
1411 || y == highlight_area.y
1412 || y == highlight_area.y + highlight_area.height - 1;
1413
1414 if is_border {
1415 cell.set_fg(drop_zone_border);
1416 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
1417 }
1418 }
1419 }
1420 }
1421
1422 match drop_zone {
1424 TabDropZone::SplitLeft(_) => {
1425 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1427 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
1428 cell.set_symbol("▌");
1429 cell.set_fg(drop_zone_border);
1430 }
1431 }
1432 }
1433 TabDropZone::SplitRight(_) => {
1434 let x = highlight_area.x + highlight_area.width - 1;
1436 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1437 if let Some(cell) = buf.cell_mut((x, y)) {
1438 cell.set_symbol("▐");
1439 cell.set_fg(drop_zone_border);
1440 }
1441 }
1442 }
1443 TabDropZone::SplitTop(_) => {
1444 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1446 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
1447 cell.set_symbol("▀");
1448 cell.set_fg(drop_zone_border);
1449 }
1450 }
1451 }
1452 TabDropZone::SplitBottom(_) => {
1453 let y = highlight_area.y + highlight_area.height - 1;
1455 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1456 if let Some(cell) = buf.cell_mut((x, y)) {
1457 cell.set_symbol("▄");
1458 cell.set_fg(drop_zone_border);
1459 }
1460 }
1461 }
1462 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
1463 }
1465 }
1466 }
1467
1468 pub fn add_overlay(
1472 &mut self,
1473 namespace: Option<crate::view::overlay::OverlayNamespace>,
1474 range: Range<usize>,
1475 face: crate::model::event::OverlayFace,
1476 priority: i32,
1477 message: Option<String>,
1478 ) -> crate::view::overlay::OverlayHandle {
1479 let event = Event::AddOverlay {
1480 namespace,
1481 range,
1482 face,
1483 priority,
1484 message,
1485 extend_to_line_end: false,
1486 url: None,
1487 };
1488 self.apply_event_to_active_buffer(&event);
1489 let state = self.active_state();
1491 state
1492 .overlays
1493 .all()
1494 .last()
1495 .map(|o| o.handle.clone())
1496 .unwrap_or_default()
1497 }
1498
1499 pub fn remove_overlay(&mut self, handle: crate::view::overlay::OverlayHandle) {
1501 let event = Event::RemoveOverlay { handle };
1502 self.apply_event_to_active_buffer(&event);
1503 }
1504
1505 pub fn remove_overlays_in_range(&mut self, range: Range<usize>) {
1507 let event = Event::RemoveOverlaysInRange { range };
1508 self.active_event_log_mut().append(event.clone());
1509 self.apply_event_to_active_buffer(&event);
1510 }
1511
1512 pub fn clear_overlays(&mut self) {
1514 let event = Event::ClearOverlays;
1515 self.active_event_log_mut().append(event.clone());
1516 self.apply_event_to_active_buffer(&event);
1517 }
1518
1519 pub fn show_popup(&mut self, popup: crate::model::event::PopupData) {
1523 let event = Event::ShowPopup { popup };
1524 self.active_event_log_mut().append(event.clone());
1525 self.apply_event_to_active_buffer(&event);
1526 }
1527
1528 pub fn hide_popup(&mut self) {
1530 let event = Event::HidePopup;
1531 self.active_event_log_mut().append(event.clone());
1532 self.apply_event_to_active_buffer(&event);
1533
1534 let active = self.active_buffer();
1536 if let Some((wait_id, true)) = self.wait_tracking.remove(&active) {
1537 self.completed_waits.push(wait_id);
1538 }
1539
1540 if let Some(handle) = self.hover_symbol_overlay.take() {
1542 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1543 self.apply_event_to_active_buffer(&remove_overlay_event);
1544 }
1545 self.hover_symbol_range = None;
1546 }
1547
1548 pub(super) fn dismiss_transient_popups(&mut self) {
1551 let is_transient_popup = self
1552 .active_state()
1553 .popups
1554 .top()
1555 .is_some_and(|p| p.transient);
1556
1557 if is_transient_popup {
1558 self.hide_popup();
1559 tracing::trace!("Dismissed transient popup");
1560 }
1561 }
1562
1563 pub(super) fn scroll_popup(&mut self, delta: i32) {
1566 if let Some(popup) = self.active_state_mut().popups.top_mut() {
1567 popup.scroll_by(delta);
1568 tracing::debug!(
1569 "Scrolled popup by {}, new offset: {}",
1570 delta,
1571 popup.scroll_offset
1572 );
1573 }
1574 }
1575
1576 pub(super) fn on_editor_focus_lost(&mut self) {
1584 self.active_state_mut().on_focus_lost();
1586
1587 self.mouse_state.lsp_hover_state = None;
1589 self.mouse_state.lsp_hover_request_sent = false;
1590 self.pending_hover_request = None;
1591
1592 if let Some(handle) = self.hover_symbol_overlay.take() {
1594 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1595 self.apply_event_to_active_buffer(&remove_overlay_event);
1596 }
1597 self.hover_symbol_range = None;
1598 }
1599
1600 pub fn clear_popups(&mut self) {
1602 let event = Event::ClearPopups;
1603 self.active_event_log_mut().append(event.clone());
1604 self.apply_event_to_active_buffer(&event);
1605 }
1606
1607 pub fn show_lsp_confirmation_popup(&mut self, language: &str) {
1614 use crate::model::event::{
1615 PopupContentData, PopupData, PopupKindHint, PopupListItemData, PopupPositionData,
1616 };
1617
1618 self.pending_lsp_confirmation = Some(language.to_string());
1620
1621 let server_info = if let Some(lsp) = &self.lsp {
1623 if let Some(config) = lsp.get_config(language) {
1624 if !config.command.is_empty() {
1625 format!("{} ({})", language, config.command)
1626 } else {
1627 language.to_string()
1628 }
1629 } else {
1630 language.to_string()
1631 }
1632 } else {
1633 language.to_string()
1634 };
1635
1636 let popup = PopupData {
1637 kind: PopupKindHint::List,
1638 title: Some(format!("Start LSP Server: {}?", server_info)),
1639 description: None,
1640 transient: false,
1641 content: PopupContentData::List {
1642 items: vec![
1643 PopupListItemData {
1644 text: "Allow this time".to_string(),
1645 detail: Some("Start the LSP server for this session".to_string()),
1646 icon: None,
1647 data: Some("allow_once".to_string()),
1648 },
1649 PopupListItemData {
1650 text: "Always allow".to_string(),
1651 detail: Some("Always start this LSP server automatically".to_string()),
1652 icon: None,
1653 data: Some("allow_always".to_string()),
1654 },
1655 PopupListItemData {
1656 text: "Don't start".to_string(),
1657 detail: Some("Cancel LSP server startup".to_string()),
1658 icon: None,
1659 data: Some("deny".to_string()),
1660 },
1661 ],
1662 selected: 0,
1663 },
1664 position: PopupPositionData::Centered,
1665 width: 50,
1666 max_height: 8,
1667 bordered: true,
1668 };
1669
1670 self.show_popup(popup);
1671 }
1672
1673 pub fn handle_lsp_confirmation_response(&mut self, action: &str) -> bool {
1681 let Some(language) = self.pending_lsp_confirmation.take() else {
1682 return false;
1683 };
1684
1685 let file_path = self
1687 .buffer_metadata
1688 .get(&self.active_buffer())
1689 .and_then(|meta| meta.file_path().cloned());
1690
1691 match action {
1692 "allow_once" => {
1693 if let Some(lsp) = &mut self.lsp {
1695 lsp.allow_language(&language);
1697 if lsp.force_spawn(&language, file_path.as_deref()).is_some() {
1699 tracing::info!("LSP server for {} started (allowed once)", language);
1700 self.set_status_message(
1701 t!("lsp.server_started", language = language).to_string(),
1702 );
1703 } else {
1704 self.set_status_message(
1705 t!("lsp.failed_to_start", language = language).to_string(),
1706 );
1707 }
1708 }
1709 self.notify_lsp_current_file_opened(&language);
1711 }
1712 "allow_always" => {
1713 if let Some(lsp) = &mut self.lsp {
1715 lsp.allow_language(&language);
1716 if lsp.force_spawn(&language, file_path.as_deref()).is_some() {
1718 tracing::info!("LSP server for {} started (always allowed)", language);
1719 self.set_status_message(
1720 t!("lsp.server_started_auto", language = language).to_string(),
1721 );
1722 } else {
1723 self.set_status_message(
1724 t!("lsp.failed_to_start", language = language).to_string(),
1725 );
1726 }
1727 }
1728 self.notify_lsp_current_file_opened(&language);
1730 }
1731 _ => {
1732 tracing::info!("LSP server for {} startup declined by user", language);
1734 self.set_status_message(
1735 t!("lsp.startup_cancelled", language = language).to_string(),
1736 );
1737 }
1738 }
1739
1740 true
1741 }
1742
1743 fn notify_lsp_current_file_opened(&mut self, language: &str) {
1748 let metadata = match self.buffer_metadata.get(&self.active_buffer()) {
1750 Some(m) => m,
1751 None => {
1752 tracing::debug!(
1753 "notify_lsp_current_file_opened: no metadata for buffer {:?}",
1754 self.active_buffer()
1755 );
1756 return;
1757 }
1758 };
1759
1760 if !metadata.lsp_enabled {
1761 tracing::debug!("notify_lsp_current_file_opened: LSP disabled for this buffer");
1762 return;
1763 }
1764
1765 let file_path = metadata.file_path().cloned();
1767
1768 let uri = match metadata.file_uri() {
1770 Some(u) => u.clone(),
1771 None => {
1772 tracing::debug!(
1773 "notify_lsp_current_file_opened: no URI for buffer (not a file or URI creation failed)"
1774 );
1775 return;
1776 }
1777 };
1778
1779 let active_buffer = self.active_buffer();
1781
1782 let file_language = match self.buffers.get(&active_buffer).map(|s| s.language.clone()) {
1784 Some(l) => l,
1785 None => {
1786 tracing::debug!("notify_lsp_current_file_opened: no buffer state");
1787 return;
1788 }
1789 };
1790
1791 if file_language != language {
1793 tracing::debug!(
1794 "notify_lsp_current_file_opened: file language {} doesn't match server {}",
1795 file_language,
1796 language
1797 );
1798 return;
1799 }
1800 let (text, line_count) = if let Some(state) = self.buffers.get(&active_buffer) {
1801 let text = match state.buffer.to_string() {
1802 Some(t) => t,
1803 None => {
1804 tracing::debug!("notify_lsp_current_file_opened: buffer not fully loaded");
1805 return;
1806 }
1807 };
1808 let line_count = state.buffer.line_count().unwrap_or(1000);
1809 (text, line_count)
1810 } else {
1811 tracing::debug!("notify_lsp_current_file_opened: no buffer state");
1812 return;
1813 };
1814
1815 if let Some(lsp) = &mut self.lsp {
1817 if lsp.force_spawn(language, file_path.as_deref()).is_some() {
1819 tracing::info!("Sending didOpen to LSP servers for: {}", uri.as_str());
1820 let mut any_opened = false;
1821 for sh in lsp.get_handles_mut(language) {
1822 if let Err(e) =
1823 sh.handle
1824 .did_open(uri.clone(), text.clone(), file_language.clone())
1825 {
1826 tracing::warn!("Failed to send didOpen to '{}': {}", sh.name, e);
1827 } else {
1828 any_opened = true;
1829 }
1830 }
1831
1832 if any_opened {
1833 tracing::info!("Successfully sent didOpen to LSP after confirmation");
1834
1835 if let Some(handle) = lsp.get_handle_mut(language) {
1837 let previous_result_id =
1838 self.diagnostic_result_ids.get(uri.as_str()).cloned();
1839 let request_id = self.next_lsp_request_id;
1840 self.next_lsp_request_id += 1;
1841
1842 if let Err(e) =
1843 handle.document_diagnostic(request_id, uri.clone(), previous_result_id)
1844 {
1845 tracing::debug!(
1846 "Failed to request pull diagnostics (server may not support): {}",
1847 e
1848 );
1849 }
1850
1851 if self.config.editor.enable_inlay_hints {
1853 let request_id = self.next_lsp_request_id;
1854 self.next_lsp_request_id += 1;
1855 self.pending_inlay_hints_request = Some(request_id);
1856
1857 let last_line = line_count.saturating_sub(1) as u32;
1858 let last_char = 10000u32;
1859
1860 if let Err(e) = handle.inlay_hints(
1861 request_id,
1862 uri.clone(),
1863 0,
1864 0,
1865 last_line,
1866 last_char,
1867 ) {
1868 tracing::debug!(
1869 "Failed to request inlay hints (server may not support): {}",
1870 e
1871 );
1872 self.pending_inlay_hints_request = None;
1873 }
1874 }
1875 }
1876 }
1877 }
1878 }
1879 }
1880
1881 pub fn has_pending_lsp_confirmation(&self) -> bool {
1883 self.pending_lsp_confirmation.is_some()
1884 }
1885
1886 pub fn popup_select_next(&mut self) {
1888 let event = Event::PopupSelectNext;
1889 self.active_event_log_mut().append(event.clone());
1890 self.apply_event_to_active_buffer(&event);
1891 }
1892
1893 pub fn popup_select_prev(&mut self) {
1895 let event = Event::PopupSelectPrev;
1896 self.active_event_log_mut().append(event.clone());
1897 self.apply_event_to_active_buffer(&event);
1898 }
1899
1900 pub fn popup_page_down(&mut self) {
1902 let event = Event::PopupPageDown;
1903 self.active_event_log_mut().append(event.clone());
1904 self.apply_event_to_active_buffer(&event);
1905 }
1906
1907 pub fn popup_page_up(&mut self) {
1909 let event = Event::PopupPageUp;
1910 self.active_event_log_mut().append(event.clone());
1911 self.apply_event_to_active_buffer(&event);
1912 }
1913
1914 pub(super) fn collect_lsp_changes(&self, event: &Event) -> Vec<TextDocumentContentChangeEvent> {
1920 match event {
1921 Event::Insert { position, text, .. } => {
1922 tracing::trace!(
1923 "collect_lsp_changes: processing Insert at position {}",
1924 position
1925 );
1926 let (line, character) = self
1928 .active_state()
1929 .buffer
1930 .position_to_lsp_position(*position);
1931 let lsp_pos = Position::new(line as u32, character as u32);
1932 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
1933 vec![TextDocumentContentChangeEvent {
1934 range: Some(lsp_range),
1935 range_length: None,
1936 text: text.clone(),
1937 }]
1938 }
1939 Event::Delete { range, .. } => {
1940 tracing::trace!("collect_lsp_changes: processing Delete range {:?}", range);
1941 let (start_line, start_char) = self
1943 .active_state()
1944 .buffer
1945 .position_to_lsp_position(range.start);
1946 let (end_line, end_char) = self
1947 .active_state()
1948 .buffer
1949 .position_to_lsp_position(range.end);
1950 let lsp_range = LspRange::new(
1951 Position::new(start_line as u32, start_char as u32),
1952 Position::new(end_line as u32, end_char as u32),
1953 );
1954 vec![TextDocumentContentChangeEvent {
1955 range: Some(lsp_range),
1956 range_length: None,
1957 text: String::new(),
1958 }]
1959 }
1960 Event::Batch { events, .. } => {
1961 tracing::trace!(
1964 "collect_lsp_changes: processing Batch with {} events",
1965 events.len()
1966 );
1967 let mut all_changes = Vec::new();
1968 for sub_event in events {
1969 all_changes.extend(self.collect_lsp_changes(sub_event));
1970 }
1971 all_changes
1972 }
1973 _ => Vec::new(), }
1975 }
1976
1977 pub(super) fn calculate_event_line_info(&self, event: &Event) -> super::types::EventLineInfo {
1999 match event {
2000 Event::Insert { position, text, .. } => {
2001 let start_line = self.active_state().buffer.get_line_number(*position);
2003
2004 let lines_added = text.matches('\n').count();
2006 let end_line = start_line + lines_added;
2007
2008 super::types::EventLineInfo {
2009 start_line,
2010 end_line,
2011 line_delta: lines_added as i32,
2012 }
2013 }
2014 Event::Delete {
2015 range,
2016 deleted_text,
2017 ..
2018 } => {
2019 let start_line = self.active_state().buffer.get_line_number(range.start);
2021 let end_line = self.active_state().buffer.get_line_number(range.end);
2022
2023 let lines_removed = deleted_text.matches('\n').count();
2025
2026 super::types::EventLineInfo {
2027 start_line,
2028 end_line,
2029 line_delta: -(lines_removed as i32),
2030 }
2031 }
2032 Event::Batch { events, .. } => {
2033 let mut min_line = usize::MAX;
2036 let mut max_line = 0usize;
2037 let mut total_delta = 0i32;
2038
2039 for sub_event in events {
2040 let info = self.calculate_event_line_info(sub_event);
2041 min_line = min_line.min(info.start_line);
2042 max_line = max_line.max(info.end_line);
2043 total_delta += info.line_delta;
2044 }
2045
2046 if min_line == usize::MAX {
2047 min_line = 0;
2048 }
2049
2050 super::types::EventLineInfo {
2051 start_line: min_line,
2052 end_line: max_line,
2053 line_delta: total_delta,
2054 }
2055 }
2056 _ => super::types::EventLineInfo::default(),
2057 }
2058 }
2059
2060 pub(super) fn notify_lsp_save(&mut self) {
2062 let buffer_id = self.active_buffer();
2063 self.notify_lsp_save_buffer(buffer_id);
2064 }
2065
2066 pub(super) fn notify_lsp_save_buffer(&mut self, buffer_id: BufferId) {
2068 let metadata = match self.buffer_metadata.get(&buffer_id) {
2070 Some(m) => m,
2071 None => {
2072 tracing::debug!(
2073 "notify_lsp_save_buffer: no metadata for buffer {:?}",
2074 buffer_id
2075 );
2076 return;
2077 }
2078 };
2079
2080 if !metadata.lsp_enabled {
2081 tracing::debug!(
2082 "notify_lsp_save_buffer: LSP disabled for buffer {:?}",
2083 buffer_id
2084 );
2085 return;
2086 }
2087
2088 let file_path = metadata.file_path().cloned();
2090
2091 let uri = match metadata.file_uri() {
2093 Some(u) => u.clone(),
2094 None => {
2095 tracing::debug!("notify_lsp_save_buffer: no URI for buffer {:?}", buffer_id);
2096 return;
2097 }
2098 };
2099
2100 let language = match self
2103 .buffers
2104 .get(&self.active_buffer())
2105 .map(|s| s.language.clone())
2106 {
2107 Some(l) => l,
2108 None => {
2109 tracing::debug!("notify_lsp_save: no buffer state");
2110 return;
2111 }
2112 };
2113
2114 let full_text = match self.active_state().buffer.to_string() {
2116 Some(t) => t,
2117 None => {
2118 tracing::debug!("notify_lsp_save: buffer not fully loaded");
2119 return;
2120 }
2121 };
2122 tracing::debug!(
2123 "notify_lsp_save: sending didSave to {} (text length: {} bytes)",
2124 uri.as_str(),
2125 full_text.len()
2126 );
2127
2128 if let Some(lsp) = &mut self.lsp {
2130 use crate::services::lsp::manager::LspSpawnResult;
2131 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
2132 tracing::debug!(
2133 "notify_lsp_save: LSP not running for {} (auto_start disabled)",
2134 language
2135 );
2136 return;
2137 }
2138 let mut any_sent = false;
2140 for sh in lsp.get_handles_mut(&language) {
2141 if let Err(e) = sh.handle.did_save(uri.clone(), Some(full_text.clone())) {
2142 tracing::warn!("Failed to send didSave to '{}': {}", sh.name, e);
2143 } else {
2144 any_sent = true;
2145 }
2146 }
2147 if any_sent {
2148 tracing::info!("Successfully sent didSave to LSP");
2149 } else {
2150 tracing::warn!("notify_lsp_save: no LSP handles for {}", language);
2151 }
2152 } else {
2153 tracing::debug!("notify_lsp_save: no LSP manager available");
2154 }
2155 }
2156
2157 pub fn action_to_events(&mut self, action: Action) -> Option<Vec<Event>> {
2160 let auto_indent = self.config.editor.auto_indent;
2161 let estimated_line_length = self.config.editor.estimated_line_length;
2162
2163 let active_split = self.split_manager.active_split();
2165 let viewport_height = self
2166 .split_view_states
2167 .get(&active_split)
2168 .map(|vs| vs.viewport.height)
2169 .unwrap_or(24);
2170
2171 if let Some(events) =
2175 self.handle_visual_line_movement(&action, active_split, estimated_line_length)
2176 {
2177 return Some(events);
2178 }
2179
2180 let buffer_id = self.active_buffer();
2181 let state = self.buffers.get_mut(&buffer_id).unwrap();
2182
2183 let tab_size = state.buffer_settings.tab_size;
2185 let auto_close = state.buffer_settings.auto_close;
2186 let auto_surround = state.buffer_settings.auto_surround;
2187
2188 let cursors = &mut self
2189 .split_view_states
2190 .get_mut(&active_split)
2191 .unwrap()
2192 .cursors;
2193 convert_action_to_events(
2194 state,
2195 cursors,
2196 action,
2197 tab_size,
2198 auto_indent,
2199 auto_close,
2200 auto_surround,
2201 estimated_line_length,
2202 viewport_height,
2203 )
2204 }
2205
2206 fn handle_visual_line_movement(
2209 &mut self,
2210 action: &Action,
2211 split_id: LeafId,
2212 _estimated_line_length: usize,
2213 ) -> Option<Vec<Event>> {
2214 enum VisualAction {
2216 UpDown { direction: i8, is_select: bool },
2217 LineEnd { is_select: bool },
2218 LineStart { is_select: bool },
2219 }
2220
2221 let visual_action = match action {
2224 Action::MoveUp => VisualAction::UpDown {
2225 direction: -1,
2226 is_select: false,
2227 },
2228 Action::MoveDown => VisualAction::UpDown {
2229 direction: 1,
2230 is_select: false,
2231 },
2232 Action::SelectUp => VisualAction::UpDown {
2233 direction: -1,
2234 is_select: true,
2235 },
2236 Action::SelectDown => VisualAction::UpDown {
2237 direction: 1,
2238 is_select: true,
2239 },
2240 Action::MoveLineEnd if self.config.editor.line_wrap => {
2244 VisualAction::LineEnd { is_select: false }
2245 }
2246 Action::SelectLineEnd if self.config.editor.line_wrap => {
2247 VisualAction::LineEnd { is_select: true }
2248 }
2249 Action::MoveLineStart if self.config.editor.line_wrap => {
2250 VisualAction::LineStart { is_select: false }
2251 }
2252 Action::SelectLineStart if self.config.editor.line_wrap => {
2253 VisualAction::LineStart { is_select: true }
2254 }
2255 _ => return None, };
2257
2258 let cursor_data: Vec<_> = {
2260 let active_split = self.split_manager.active_split();
2261 let active_buffer = self.split_manager.active_buffer_id().unwrap();
2262 let cursors = &self.split_view_states.get(&active_split).unwrap().cursors;
2263 let state = self.buffers.get(&active_buffer).unwrap();
2264 cursors
2265 .iter()
2266 .map(|(cursor_id, cursor)| {
2267 let at_line_ending = if cursor.position < state.buffer.len() {
2271 let bytes = state
2272 .buffer
2273 .slice_bytes(cursor.position..cursor.position + 1);
2274 bytes.first() == Some(&b'\n') || bytes.first() == Some(&b'\r')
2275 } else {
2276 true };
2278 let at_line_start = if cursor.position == 0 {
2279 true
2280 } else {
2281 let prev = state
2282 .buffer
2283 .slice_bytes(cursor.position - 1..cursor.position);
2284 prev.first() == Some(&b'\n')
2285 };
2286 (
2287 cursor_id,
2288 cursor.position,
2289 cursor.anchor,
2290 cursor.sticky_column,
2291 cursor.deselect_on_move,
2292 at_line_ending,
2293 at_line_start,
2294 )
2295 })
2296 .collect()
2297 };
2298
2299 let mut events = Vec::new();
2300
2301 for (
2302 cursor_id,
2303 position,
2304 anchor,
2305 sticky_column,
2306 deselect_on_move,
2307 at_line_ending,
2308 at_line_start,
2309 ) in cursor_data
2310 {
2311 let (new_pos, new_sticky) = match &visual_action {
2312 VisualAction::UpDown { direction, .. } => {
2313 let current_visual_col = self
2315 .cached_layout
2316 .byte_to_visual_column(split_id, position)?;
2317
2318 let goal_visual_col = if sticky_column > 0 {
2319 sticky_column
2320 } else {
2321 current_visual_col
2322 };
2323
2324 match self.cached_layout.move_visual_line(
2325 split_id,
2326 position,
2327 goal_visual_col,
2328 *direction,
2329 ) {
2330 Some(result) => result,
2331 None => continue, }
2333 }
2334 VisualAction::LineEnd { .. } => {
2335 let allow_advance = !at_line_ending;
2337 match self
2338 .cached_layout
2339 .visual_line_end(split_id, position, allow_advance)
2340 {
2341 Some(end_pos) => (end_pos, 0),
2342 None => return None,
2343 }
2344 }
2345 VisualAction::LineStart { .. } => {
2346 let allow_advance = !at_line_start;
2348 match self
2349 .cached_layout
2350 .visual_line_start(split_id, position, allow_advance)
2351 {
2352 Some(start_pos) => (start_pos, 0),
2353 None => return None,
2354 }
2355 }
2356 };
2357
2358 let is_select = match &visual_action {
2359 VisualAction::UpDown { is_select, .. } => *is_select,
2360 VisualAction::LineEnd { is_select } => *is_select,
2361 VisualAction::LineStart { is_select } => *is_select,
2362 };
2363
2364 let new_anchor = if is_select {
2365 Some(anchor.unwrap_or(position))
2366 } else if deselect_on_move {
2367 None
2368 } else {
2369 anchor
2370 };
2371
2372 events.push(Event::MoveCursor {
2373 cursor_id,
2374 old_position: position,
2375 new_position: new_pos,
2376 old_anchor: anchor,
2377 new_anchor,
2378 old_sticky_column: sticky_column,
2379 new_sticky_column: new_sticky,
2380 });
2381 }
2382
2383 if events.is_empty() {
2384 None } else {
2386 Some(events)
2387 }
2388 }
2389
2390 pub(super) fn clear_search_highlights(&mut self) {
2394 self.clear_search_overlays();
2395 self.search_state = None;
2397 }
2398
2399 pub(super) fn clear_search_overlays(&mut self) {
2402 let ns = self.search_namespace.clone();
2403 let state = self.active_state_mut();
2404 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2405 }
2406
2407 pub(super) fn update_search_highlights(&mut self, query: &str) {
2410 if query.is_empty() {
2412 self.clear_search_highlights();
2413 return;
2414 }
2415
2416 let search_bg = self.theme.search_match_bg;
2418 let search_fg = self.theme.search_match_fg;
2419 let case_sensitive = self.search_case_sensitive;
2420 let whole_word = self.search_whole_word;
2421 let use_regex = self.search_use_regex;
2422 let ns = self.search_namespace.clone();
2423
2424 let regex_pattern = if use_regex {
2426 if whole_word {
2427 format!(r"\b{}\b", query)
2428 } else {
2429 query.to_string()
2430 }
2431 } else {
2432 let escaped = regex::escape(query);
2433 if whole_word {
2434 format!(r"\b{}\b", escaped)
2435 } else {
2436 escaped
2437 }
2438 };
2439
2440 let regex = regex::RegexBuilder::new(®ex_pattern)
2442 .case_insensitive(!case_sensitive)
2443 .build();
2444
2445 let regex = match regex {
2446 Ok(r) => r,
2447 Err(_) => {
2448 self.clear_search_highlights();
2450 return;
2451 }
2452 };
2453
2454 let active_split = self.split_manager.active_split();
2456 let (top_byte, visible_height) = self
2457 .split_view_states
2458 .get(&active_split)
2459 .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2460 .unwrap_or((0, 20));
2461
2462 let state = self.active_state_mut();
2463
2464 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2466
2467 let visible_start = top_byte;
2469 let mut visible_end = top_byte;
2470
2471 {
2472 let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2473 for _ in 0..visible_height {
2474 if let Some((line_start, line_content)) = line_iter.next_line() {
2475 visible_end = line_start + line_content.len();
2476 } else {
2477 break;
2478 }
2479 }
2480 }
2481
2482 visible_end = visible_end.min(state.buffer.len());
2484
2485 let visible_text = state.get_text_range(visible_start, visible_end);
2487
2488 for mat in regex.find_iter(&visible_text) {
2490 let absolute_pos = visible_start + mat.start();
2491 let match_len = mat.end() - mat.start();
2492
2493 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2495 let overlay = crate::view::overlay::Overlay::with_namespace(
2496 &mut state.marker_list,
2497 absolute_pos..(absolute_pos + match_len),
2498 crate::view::overlay::OverlayFace::Style {
2499 style: search_style,
2500 },
2501 ns.clone(),
2502 )
2503 .with_priority_value(10); state.overlays.add(overlay);
2506 }
2507 }
2508
2509 fn build_search_regex(&self, query: &str) -> Result<regex::Regex, String> {
2511 let regex_pattern = if self.search_use_regex {
2512 if self.search_whole_word {
2513 format!(r"\b{}\b", query)
2514 } else {
2515 query.to_string()
2516 }
2517 } else {
2518 let escaped = regex::escape(query);
2519 if self.search_whole_word {
2520 format!(r"\b{}\b", escaped)
2521 } else {
2522 escaped
2523 }
2524 };
2525
2526 regex::RegexBuilder::new(®ex_pattern)
2527 .case_insensitive(!self.search_case_sensitive)
2528 .build()
2529 .map_err(|e| e.to_string())
2530 }
2531
2532 pub(super) fn perform_search(&mut self, query: &str) {
2541 if query.is_empty() {
2542 self.search_state = None;
2543 self.set_status_message(t!("search.cancelled").to_string());
2544 return;
2545 }
2546
2547 let search_range = self.pending_search_range.take();
2548
2549 let regex = match self.build_search_regex(query) {
2551 Ok(r) => r,
2552 Err(e) => {
2553 self.search_state = None;
2554 self.set_status_message(t!("error.invalid_regex", error = e).to_string());
2555 return;
2556 }
2557 };
2558
2559 let is_large = self.active_state().buffer.is_large_file();
2561 if is_large && search_range.is_none() {
2562 self.start_search_scan(query, regex);
2563 return;
2564 }
2565
2566 let buffer_content = {
2569 let state = self.active_state_mut();
2570 let total_bytes = state.buffer.len();
2571 match state.buffer.get_text_range_mut(0, total_bytes) {
2572 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2573 Err(e) => {
2574 tracing::warn!("Failed to load buffer for search: {}", e);
2575 self.set_status_message(t!("error.buffer_not_loaded").to_string());
2576 return;
2577 }
2578 }
2579 };
2580
2581 let (search_start, search_end) = if let Some(ref range) = search_range {
2582 (range.start, range.end)
2583 } else {
2584 (0, buffer_content.len())
2585 };
2586
2587 let search_slice = &buffer_content[search_start..search_end];
2588
2589 let mut match_ranges: Vec<(usize, usize)> = Vec::new();
2591 let mut capped = false;
2592 for m in regex.find_iter(search_slice) {
2593 if match_ranges.len() >= SearchState::MAX_MATCHES {
2594 capped = true;
2595 break;
2596 }
2597 match_ranges.push((search_start + m.start(), m.end() - m.start()));
2598 }
2599
2600 if match_ranges.is_empty() {
2601 self.search_state = None;
2602 let msg = if search_range.is_some() {
2603 format!("No matches found for '{}' in selection", query)
2604 } else {
2605 format!("No matches found for '{}'", query)
2606 };
2607 self.set_status_message(msg);
2608 return;
2609 }
2610
2611 self.finalize_search(query, match_ranges, capped, search_range);
2612 }
2613
2614 pub(super) fn finalize_search(
2623 &mut self,
2624 query: &str,
2625 match_ranges: Vec<(usize, usize)>,
2626 capped: bool,
2627 search_range: Option<std::ops::Range<usize>>,
2628 ) {
2629 let matches: Vec<usize> = match_ranges.iter().map(|(pos, _)| *pos).collect();
2630 let match_lengths: Vec<usize> = match_ranges.iter().map(|(_, len)| *len).collect();
2631 let is_large = self.active_state().buffer.is_large_file();
2632
2633 let cursor_pos = self.active_cursors().primary().position;
2635 let current_match_index = matches
2636 .iter()
2637 .position(|&pos| pos >= cursor_pos)
2638 .unwrap_or(0);
2639
2640 let match_pos = matches[current_match_index];
2642 {
2643 let active_split = self.split_manager.active_split();
2644 let active_buffer = self.active_buffer();
2645 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2646 view_state.cursors.primary_mut().position = match_pos;
2647 view_state.cursors.primary_mut().anchor = None;
2648 let state = self.buffers.get_mut(&active_buffer).unwrap();
2649 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
2650 }
2651 }
2652
2653 let num_matches = matches.len();
2654
2655 self.search_state = Some(SearchState {
2656 query: query.to_string(),
2657 matches,
2658 match_lengths: match_lengths.clone(),
2659 current_match_index: Some(current_match_index),
2660 wrap_search: search_range.is_none(),
2661 search_range,
2662 capped,
2663 });
2664
2665 if is_large {
2666 self.refresh_search_overlays();
2668 } else {
2669 let search_bg = self.theme.search_match_bg;
2671 let search_fg = self.theme.search_match_fg;
2672 let ns = self.search_namespace.clone();
2673 let state = self.active_state_mut();
2674 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2675
2676 for (&pos, &len) in match_ranges
2677 .iter()
2678 .map(|(p, _)| p)
2679 .zip(match_lengths.iter())
2680 {
2681 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2682 let overlay = crate::view::overlay::Overlay::with_namespace(
2683 &mut state.marker_list,
2684 pos..(pos + len),
2685 crate::view::overlay::OverlayFace::Style {
2686 style: search_style,
2687 },
2688 ns.clone(),
2689 )
2690 .with_priority_value(10);
2691 state.overlays.add(overlay);
2692 }
2693 }
2694
2695 let cap_suffix = if capped { "+" } else { "" };
2696 let msg = if self.search_state.as_ref().unwrap().search_range.is_some() {
2697 format!(
2698 "Found {}{} match{} for '{}' in selection",
2699 num_matches,
2700 cap_suffix,
2701 if num_matches == 1 { "" } else { "es" },
2702 query
2703 )
2704 } else {
2705 format!(
2706 "Found {}{} match{} for '{}'",
2707 num_matches,
2708 cap_suffix,
2709 if num_matches == 1 { "" } else { "es" },
2710 query
2711 )
2712 };
2713 self.set_status_message(msg);
2714 }
2715
2716 pub(super) fn refresh_search_overlays(&mut self) {
2720 let _span = tracing::info_span!("refresh_search_overlays").entered();
2721 let search_bg = self.theme.search_match_bg;
2722 let search_fg = self.theme.search_match_fg;
2723 let ns = self.search_namespace.clone();
2724
2725 let active_split = self.split_manager.active_split();
2727 let (top_byte, visible_height) = self
2728 .split_view_states
2729 .get(&active_split)
2730 .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2731 .unwrap_or((0, 20));
2732
2733 self.search_overlay_top_byte = Some(top_byte);
2736
2737 let state = self.active_state_mut();
2738
2739 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2741
2742 let visible_start = top_byte;
2744 let mut visible_end = top_byte;
2745 {
2746 let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2747 for _ in 0..visible_height {
2748 if let Some((line_start, line_content)) = line_iter.next_line() {
2749 visible_end = line_start + line_content.len();
2750 } else {
2751 break;
2752 }
2753 }
2754 }
2755 visible_end = visible_end.min(state.buffer.len());
2756
2757 let _ = state;
2761
2762 let viewport_matches: Vec<(usize, usize)> = match &self.search_state {
2763 Some(ss) => {
2764 let start_idx = ss.matches.partition_point(|&pos| pos < visible_start);
2765 ss.matches[start_idx..]
2766 .iter()
2767 .zip(ss.match_lengths[start_idx..].iter())
2768 .take_while(|(&pos, _)| pos <= visible_end)
2769 .map(|(&pos, &len)| (pos, len))
2770 .collect()
2771 }
2772 None => return,
2773 };
2774
2775 let state = self.active_state_mut();
2776
2777 for (pos, len) in &viewport_matches {
2778 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2779 let overlay = crate::view::overlay::Overlay::with_namespace(
2780 &mut state.marker_list,
2781 *pos..(*pos + *len),
2782 crate::view::overlay::OverlayFace::Style {
2783 style: search_style,
2784 },
2785 ns.clone(),
2786 )
2787 .with_priority_value(10);
2788 state.overlays.add(overlay);
2789 }
2790 }
2791
2792 pub(super) fn check_search_overlay_refresh(&mut self) -> bool {
2800 if self.search_state.is_none() {
2801 return false;
2802 }
2803 if !self.active_state().buffer.is_large_file() {
2805 return false;
2806 }
2807 let active_split = self.split_manager.active_split();
2808 let current_top = self
2809 .split_view_states
2810 .get(&active_split)
2811 .map(|vs| vs.viewport.top_byte);
2812 if current_top != self.search_overlay_top_byte {
2813 self.refresh_search_overlays();
2814 true
2815 } else {
2816 false
2817 }
2818 }
2819
2820 fn start_search_scan(&mut self, query: &str, regex: regex::Regex) {
2825 let buffer_id = self.active_buffer();
2826 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2827 let leaves = state.buffer.piece_tree_leaves();
2828 let bytes_regex = regex::bytes::RegexBuilder::new(regex.as_str())
2830 .case_insensitive(!self.search_case_sensitive)
2831 .build()
2832 .expect("regex already validated");
2833 let scan = state.buffer.search_scan_init(
2834 bytes_regex,
2835 super::SearchState::MAX_MATCHES,
2836 query.len(),
2837 );
2838 self.search_scan_state = Some(super::SearchScanState {
2839 buffer_id,
2840 leaves,
2841 scan,
2842 query: query.to_string(),
2843 search_range: None,
2844 case_sensitive: self.search_case_sensitive,
2845 whole_word: self.search_whole_word,
2846 use_regex: self.search_use_regex,
2847 });
2848 self.set_status_message(t!("goto.scanning_progress", percent = 0).to_string());
2849 }
2850 }
2851
2852 fn get_search_match_positions(&self) -> Vec<usize> {
2856 let ns = &self.search_namespace;
2857 let state = self.active_state();
2858
2859 let mut positions: Vec<usize> = state
2860 .overlays
2861 .all()
2862 .iter()
2863 .filter(|o| o.namespace.as_ref() == Some(ns))
2864 .filter_map(|o| state.marker_list.get_position(o.start_marker))
2865 .collect();
2866
2867 positions.sort_unstable();
2868 positions.dedup();
2869 positions
2870 }
2871
2872 pub(super) fn find_next(&mut self) {
2879 self.find_match_in_direction(SearchDirection::Forward);
2880 }
2881
2882 pub(super) fn find_previous(&mut self) {
2888 self.find_match_in_direction(SearchDirection::Backward);
2889 }
2890
2891 fn find_match_in_direction(&mut self, direction: SearchDirection) {
2896 let overlay_positions = self.get_search_match_positions();
2897 let is_large = self.active_state().buffer.is_large_file();
2898
2899 if let Some(ref mut search_state) = self.search_state {
2900 let use_overlays =
2903 !is_large && !overlay_positions.is_empty() && search_state.search_range.is_none();
2904 let match_positions: &[usize] = if use_overlays {
2905 &overlay_positions
2906 } else {
2907 &search_state.matches
2908 };
2909
2910 if match_positions.is_empty() {
2911 return;
2912 }
2913
2914 let cursor_pos = {
2915 let active_split = self.split_manager.active_split();
2916 self.split_view_states
2917 .get(&active_split)
2918 .map(|vs| vs.cursors.primary().position)
2919 .unwrap_or(0)
2920 };
2921
2922 let target_index = match direction {
2923 SearchDirection::Forward => {
2924 let idx = match match_positions.binary_search(&(cursor_pos + 1)) {
2926 Ok(i) | Err(i) => {
2927 if i < match_positions.len() {
2928 Some(i)
2929 } else {
2930 None
2931 }
2932 }
2933 };
2934 match idx {
2935 Some(i) => i,
2936 None if search_state.wrap_search => 0,
2937 None => {
2938 self.set_status_message(t!("search.no_matches").to_string());
2939 return;
2940 }
2941 }
2942 }
2943 SearchDirection::Backward => {
2944 let idx = if cursor_pos == 0 {
2946 None
2947 } else {
2948 match match_positions.binary_search(&(cursor_pos - 1)) {
2949 Ok(i) => Some(i),
2950 Err(i) => {
2951 if i > 0 {
2952 Some(i - 1)
2953 } else {
2954 None
2955 }
2956 }
2957 }
2958 };
2959 match idx {
2960 Some(i) => i,
2961 None if search_state.wrap_search => match_positions.len() - 1,
2962 None => {
2963 self.set_status_message(t!("search.no_matches").to_string());
2964 return;
2965 }
2966 }
2967 }
2968 };
2969
2970 search_state.current_match_index = Some(target_index);
2971 let match_pos = match_positions[target_index];
2972 let matches_len = match_positions.len();
2973
2974 {
2975 let active_split = self.split_manager.active_split();
2976 let active_buffer = self.active_buffer();
2977 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2978 view_state.cursors.primary_mut().position = match_pos;
2979 view_state.cursors.primary_mut().anchor = None;
2980 let state = self.buffers.get_mut(&active_buffer).unwrap();
2981 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
2982 }
2983 }
2984
2985 self.set_status_message(
2986 t!(
2987 "search.match_of",
2988 current = target_index + 1,
2989 total = matches_len
2990 )
2991 .to_string(),
2992 );
2993
2994 if is_large {
2995 self.refresh_search_overlays();
2996 }
2997 } else {
2998 let find_key = self
2999 .get_keybinding_for_action("find")
3000 .unwrap_or_else(|| "Ctrl+F".to_string());
3001 self.set_status_message(t!("search.no_active", find_key = find_key).to_string());
3002 }
3003 }
3004
3005 pub(super) fn find_selection_next(&mut self) {
3012 if let Some(ref search_state) = self.search_state {
3015 let cursor_pos = self.active_cursors().primary().position;
3016 if search_state.matches.binary_search(&cursor_pos).is_ok() {
3017 self.find_next();
3018 return;
3019 }
3020 }
3022 self.search_state = None;
3023
3024 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
3026
3027 match search_text {
3028 Some(text) if !text.is_empty() => {
3029 let cursor_before = self.active_cursors().primary().position;
3031
3032 self.perform_search(&text);
3034
3035 if let Some(ref search_state) = self.search_state {
3037 let cursor_after = self.active_cursors().primary().position;
3038
3039 let started_at_match = selection_start
3043 .map(|start| search_state.matches.binary_search(&start).is_ok())
3044 .unwrap_or(false);
3045
3046 let landed_at_start = selection_start
3047 .map(|start| cursor_after == start)
3048 .unwrap_or(false);
3049
3050 if ((started_at_match && landed_at_start) || cursor_before == cursor_after)
3054 && search_state.matches.len() > 1
3055 {
3056 self.find_next();
3057 }
3058 }
3059 }
3060 _ => {
3061 self.set_status_message(t!("search.no_text").to_string());
3062 }
3063 }
3064 }
3065
3066 pub(super) fn find_selection_previous(&mut self) {
3072 if let Some(ref search_state) = self.search_state {
3075 let cursor_pos = self.active_cursors().primary().position;
3076 if search_state.matches.binary_search(&cursor_pos).is_ok() {
3077 self.find_previous();
3078 return;
3079 }
3080 }
3082 self.search_state = None;
3083
3084 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
3086
3087 match search_text {
3088 Some(text) if !text.is_empty() => {
3089 let cursor_before = self.active_cursors().primary().position;
3091
3092 self.perform_search(&text);
3094
3095 if let Some(ref search_state) = self.search_state {
3097 let cursor_after = self.active_cursors().primary().position;
3098
3099 let started_at_match = selection_start
3101 .map(|start| search_state.matches.binary_search(&start).is_ok())
3102 .unwrap_or(false);
3103
3104 let landed_at_start = selection_start
3105 .map(|start| cursor_after == start)
3106 .unwrap_or(false);
3107
3108 if started_at_match && landed_at_start {
3113 self.find_previous();
3115 } else if cursor_before != cursor_after {
3116 self.find_previous();
3119 } else {
3120 self.find_previous();
3122 }
3123 }
3124 }
3125 _ => {
3126 self.set_status_message(t!("search.no_text").to_string());
3127 }
3128 }
3129 }
3130
3131 fn get_selection_or_word_for_search_with_pos(&mut self) -> (Option<String>, Option<usize>) {
3134 use crate::primitives::word_navigation::{find_word_end, find_word_start};
3135
3136 let (selection_range, cursor_pos) = {
3138 let primary = self.active_cursors().primary();
3139 (primary.selection_range(), primary.position)
3140 };
3141
3142 if let Some(range) = selection_range {
3144 let state = self.active_state_mut();
3145 let text = state.get_text_range(range.start, range.end);
3146 if !text.is_empty() {
3147 return (Some(text), Some(range.start));
3148 }
3149 }
3150
3151 let (word_start, word_end) = {
3153 let state = self.active_state();
3154 let word_start = find_word_start(&state.buffer, cursor_pos);
3155 let word_end = find_word_end(&state.buffer, cursor_pos);
3156 (word_start, word_end)
3157 };
3158
3159 if word_start < word_end {
3160 let state = self.active_state_mut();
3161 (
3162 Some(state.get_text_range(word_start, word_end)),
3163 Some(word_start),
3164 )
3165 } else {
3166 (None, None)
3167 }
3168 }
3169
3170 fn build_replace_regex(&self, search: &str) -> Option<regex::bytes::Regex> {
3174 super::regex_replace::build_regex(
3175 search,
3176 self.search_use_regex,
3177 self.search_whole_word,
3178 self.search_case_sensitive,
3179 )
3180 }
3181
3182 fn get_regex_match_len(&mut self, regex: ®ex::bytes::Regex, pos: usize) -> Option<usize> {
3184 let state = self.active_state_mut();
3185 let remaining = state.buffer.len().saturating_sub(pos);
3186 if remaining == 0 {
3187 return None;
3188 }
3189 let bytes = state.buffer.get_text_range_mut(pos, remaining).ok()?;
3190 regex.find(&bytes).map(|m| m.len())
3191 }
3192
3193 fn expand_regex_replacement(
3196 &mut self,
3197 regex: ®ex::bytes::Regex,
3198 pos: usize,
3199 match_len: usize,
3200 replacement: &str,
3201 ) -> String {
3202 let state = self.active_state_mut();
3203 if let Ok(bytes) = state.buffer.get_text_range_mut(pos, match_len) {
3204 return super::regex_replace::expand_replacement(regex, &bytes, replacement);
3205 }
3206 replacement.to_string()
3207 }
3208
3209 pub(super) fn perform_replace(&mut self, search: &str, replacement: &str) {
3214 if search.is_empty() {
3215 self.set_status_message(t!("replace.empty_query").to_string());
3216 return;
3217 }
3218
3219 let compiled_regex = self.build_replace_regex(search);
3220
3221 let matches: Vec<(usize, usize, String)> = if let Some(ref regex) = compiled_regex {
3224 let buffer_bytes = {
3227 let state = self.active_state_mut();
3228 let total_bytes = state.buffer.len();
3229 match state.buffer.get_text_range_mut(0, total_bytes) {
3230 Ok(bytes) => bytes,
3231 Err(e) => {
3232 tracing::warn!("Failed to load buffer for replace: {}", e);
3233 self.set_status_message(t!("error.buffer_not_loaded").to_string());
3234 return;
3235 }
3236 }
3237 };
3238 super::regex_replace::collect_regex_matches(regex, &buffer_bytes, replacement)
3239 .into_iter()
3240 .map(|m| (m.offset, m.len, m.replacement))
3241 .collect()
3242 } else {
3243 let state = self.active_state();
3245 let buffer_len = state.buffer.len();
3246 let mut matches = Vec::new();
3247 let mut current_pos = 0;
3248
3249 while current_pos < buffer_len {
3250 if let Some(offset) = state.buffer.find_next_in_range(
3251 search,
3252 current_pos,
3253 Some(current_pos..buffer_len),
3254 ) {
3255 matches.push((offset, search.len(), replacement.to_string()));
3256 current_pos = offset + search.len();
3257 } else {
3258 break;
3259 }
3260 }
3261 matches
3262 };
3263
3264 let count = matches.len();
3265
3266 if count == 0 {
3267 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3268 return;
3269 }
3270
3271 let cursor_id = self.active_cursors().primary_id();
3273
3274 let mut events = Vec::with_capacity(count * 2);
3277 for (match_pos, match_len, expanded_replacement) in &matches {
3278 let deleted_text = self
3280 .active_state_mut()
3281 .get_text_range(*match_pos, match_pos + match_len);
3282 events.push(Event::Delete {
3284 range: *match_pos..match_pos + match_len,
3285 deleted_text,
3286 cursor_id,
3287 });
3288 events.push(Event::Insert {
3290 position: *match_pos,
3291 text: expanded_replacement.clone(),
3292 cursor_id,
3293 });
3294 }
3295
3296 let description = format!("Replace all '{}' with '{}'", search, replacement);
3298 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3299 self.active_event_log_mut().append(bulk_edit);
3300 }
3301
3302 self.search_state = None;
3304
3305 let ns = self.search_namespace.clone();
3307 let state = self.active_state_mut();
3308 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3309
3310 self.set_status_message(
3312 t!(
3313 "search.replaced",
3314 count = count,
3315 search = search,
3316 replace = replacement
3317 )
3318 .to_string(),
3319 );
3320 }
3321
3322 pub(super) fn start_interactive_replace(&mut self, search: &str, replacement: &str) {
3324 if search.is_empty() {
3325 self.set_status_message(t!("replace.query_empty").to_string());
3326 return;
3327 }
3328
3329 let compiled_regex = self.build_replace_regex(search);
3330
3331 let start_pos = self.active_cursors().primary().position;
3333 let (first_match_pos, first_match_len) = if let Some(ref regex) = compiled_regex {
3334 let state = self.active_state();
3335 let buffer_len = state.buffer.len();
3336 let found = state
3338 .buffer
3339 .find_next_regex_in_range(regex, start_pos, Some(start_pos..buffer_len))
3340 .or_else(|| {
3341 if start_pos > 0 {
3342 state
3343 .buffer
3344 .find_next_regex_in_range(regex, 0, Some(0..start_pos))
3345 } else {
3346 None
3347 }
3348 });
3349 let Some(pos) = found else {
3350 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3351 return;
3352 };
3353 let match_len = self.get_regex_match_len(regex, pos).unwrap_or(search.len());
3355 (pos, match_len)
3356 } else {
3357 let state = self.active_state();
3358 let Some(pos) = state.buffer.find_next(search, start_pos) else {
3359 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3360 return;
3361 };
3362 (pos, search.len())
3363 };
3364
3365 self.interactive_replace_state = Some(InteractiveReplaceState {
3367 search: search.to_string(),
3368 replacement: replacement.to_string(),
3369 current_match_pos: first_match_pos,
3370 current_match_len: first_match_len,
3371 start_pos: first_match_pos,
3372 has_wrapped: false,
3373 replacements_made: 0,
3374 regex: compiled_regex,
3375 });
3376
3377 let active_split = self.split_manager.active_split();
3379 let active_buffer = self.active_buffer();
3380 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
3381 view_state.cursors.primary_mut().position = first_match_pos;
3382 view_state.cursors.primary_mut().anchor = None;
3383 let state = self.buffers.get_mut(&active_buffer).unwrap();
3385 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
3386 }
3387
3388 self.prompt = Some(Prompt::new(
3390 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string(),
3391 PromptType::QueryReplaceConfirm,
3392 ));
3393 }
3394
3395 pub(super) fn handle_interactive_replace_key(&mut self, c: char) -> AnyhowResult<()> {
3397 let state = self.interactive_replace_state.clone();
3398 let Some(mut ir_state) = state else {
3399 return Ok(());
3400 };
3401
3402 match c {
3403 'y' | 'Y' => {
3404 self.replace_current_match(&ir_state)?;
3406 ir_state.replacements_made += 1;
3407
3408 let search_pos = ir_state.current_match_pos + ir_state.replacement.len();
3410 if let Some((next_match, match_len, wrapped)) =
3411 self.find_next_match_for_replace(&ir_state, search_pos)
3412 {
3413 ir_state.current_match_pos = next_match;
3414 ir_state.current_match_len = match_len;
3415 if wrapped {
3416 ir_state.has_wrapped = true;
3417 }
3418 self.interactive_replace_state = Some(ir_state.clone());
3419 self.move_to_current_match(&ir_state);
3420 } else {
3421 self.finish_interactive_replace(ir_state.replacements_made);
3422 }
3423 }
3424 'n' | 'N' => {
3425 let search_pos = ir_state.current_match_pos + ir_state.current_match_len;
3427 if let Some((next_match, match_len, wrapped)) =
3428 self.find_next_match_for_replace(&ir_state, search_pos)
3429 {
3430 ir_state.current_match_pos = next_match;
3431 ir_state.current_match_len = match_len;
3432 if wrapped {
3433 ir_state.has_wrapped = true;
3434 }
3435 self.interactive_replace_state = Some(ir_state.clone());
3436 self.move_to_current_match(&ir_state);
3437 } else {
3438 self.finish_interactive_replace(ir_state.replacements_made);
3439 }
3440 }
3441 'a' | 'A' | '!' => {
3442 let all_matches: Vec<(usize, usize)> = {
3451 let mut matches = Vec::new();
3452 let mut temp_state = ir_state.clone();
3453 temp_state.has_wrapped = false; matches.push((ir_state.current_match_pos, ir_state.current_match_len));
3457 let mut current_pos = ir_state.current_match_pos + ir_state.current_match_len;
3458
3459 while let Some((next_match, match_len, wrapped)) =
3461 self.find_next_match_for_replace(&temp_state, current_pos)
3462 {
3463 matches.push((next_match, match_len));
3464 current_pos = next_match + match_len;
3465 if wrapped {
3466 temp_state.has_wrapped = true;
3467 }
3468 }
3469 matches
3470 };
3471
3472 let total_count = all_matches.len();
3473
3474 if total_count > 0 {
3475 let cursor_id = self.active_cursors().primary_id();
3477
3478 let mut events = Vec::with_capacity(total_count * 2);
3480 for &(match_pos, match_len) in &all_matches {
3481 let deleted_text = self
3482 .active_state_mut()
3483 .get_text_range(match_pos, match_pos + match_len);
3484 let replacement_text = if let Some(ref regex) = ir_state.regex {
3486 self.expand_regex_replacement(
3487 regex,
3488 match_pos,
3489 match_len,
3490 &ir_state.replacement,
3491 )
3492 } else {
3493 ir_state.replacement.clone()
3494 };
3495 events.push(Event::Delete {
3496 range: match_pos..match_pos + match_len,
3497 deleted_text,
3498 cursor_id,
3499 });
3500 events.push(Event::Insert {
3501 position: match_pos,
3502 text: replacement_text,
3503 cursor_id,
3504 });
3505 }
3506
3507 let description = format!(
3509 "Replace all {} occurrences of '{}' with '{}'",
3510 total_count, ir_state.search, ir_state.replacement
3511 );
3512 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3513 self.active_event_log_mut().append(bulk_edit);
3514 }
3515
3516 ir_state.replacements_made += total_count;
3517 }
3518
3519 self.finish_interactive_replace(ir_state.replacements_made);
3520 }
3521 'c' | 'C' | 'q' | 'Q' | '\x1b' => {
3522 self.finish_interactive_replace(ir_state.replacements_made);
3524 }
3525 _ => {
3526 }
3528 }
3529
3530 Ok(())
3531 }
3532
3533 pub(super) fn find_next_match_for_replace(
3536 &mut self,
3537 ir_state: &InteractiveReplaceState,
3538 start_pos: usize,
3539 ) -> Option<(usize, usize, bool)> {
3540 if let Some(ref regex) = ir_state.regex {
3541 let regex = regex.clone();
3543 let state = self.active_state();
3544 let buffer_len = state.buffer.len();
3545
3546 if ir_state.has_wrapped {
3547 let search_range = Some(start_pos..ir_state.start_pos);
3548 if let Some(match_pos) =
3549 state
3550 .buffer
3551 .find_next_regex_in_range(®ex, start_pos, search_range)
3552 {
3553 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3554 return Some((match_pos, match_len, true));
3555 }
3556 None
3557 } else {
3558 let search_range = Some(start_pos..buffer_len);
3559 if let Some(match_pos) =
3560 state
3561 .buffer
3562 .find_next_regex_in_range(®ex, start_pos, search_range)
3563 {
3564 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3565 return Some((match_pos, match_len, false));
3566 }
3567
3568 let wrap_range = Some(0..ir_state.start_pos);
3570 let state = self.active_state();
3571 if let Some(match_pos) =
3572 state.buffer.find_next_regex_in_range(®ex, 0, wrap_range)
3573 {
3574 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3575 return Some((match_pos, match_len, true));
3576 }
3577
3578 None
3579 }
3580 } else {
3581 let search_len = ir_state.search.len();
3583 let state = self.active_state();
3584
3585 if ir_state.has_wrapped {
3586 let search_range = Some(start_pos..ir_state.start_pos);
3587 if let Some(match_pos) =
3588 state
3589 .buffer
3590 .find_next_in_range(&ir_state.search, start_pos, search_range)
3591 {
3592 return Some((match_pos, search_len, true));
3593 }
3594 None
3595 } else {
3596 let buffer_len = state.buffer.len();
3597 let search_range = Some(start_pos..buffer_len);
3598 if let Some(match_pos) =
3599 state
3600 .buffer
3601 .find_next_in_range(&ir_state.search, start_pos, search_range)
3602 {
3603 return Some((match_pos, search_len, false));
3604 }
3605
3606 let wrap_range = Some(0..ir_state.start_pos);
3607 if let Some(match_pos) =
3608 state
3609 .buffer
3610 .find_next_in_range(&ir_state.search, 0, wrap_range)
3611 {
3612 return Some((match_pos, search_len, true));
3613 }
3614
3615 None
3616 }
3617 }
3618 }
3619
3620 pub(super) fn replace_current_match(
3622 &mut self,
3623 ir_state: &InteractiveReplaceState,
3624 ) -> AnyhowResult<()> {
3625 let match_pos = ir_state.current_match_pos;
3626 let match_len = ir_state.current_match_len;
3627 let range = match_pos..(match_pos + match_len);
3628
3629 let replacement_text = if let Some(ref regex) = ir_state.regex {
3631 self.expand_regex_replacement(regex, match_pos, match_len, &ir_state.replacement)
3632 } else {
3633 ir_state.replacement.clone()
3634 };
3635
3636 let deleted_text = self
3638 .active_state_mut()
3639 .get_text_range(range.start, range.end);
3640
3641 let cursor_id = self.active_cursors().primary_id();
3643 let cursor = *self.active_cursors().primary();
3644 let old_position = cursor.position;
3645 let old_anchor = cursor.anchor;
3646 let old_sticky_column = cursor.sticky_column;
3647
3648 let events = vec![
3651 Event::MoveCursor {
3652 cursor_id,
3653 old_position,
3654 new_position: match_pos,
3655 old_anchor,
3656 new_anchor: None,
3657 old_sticky_column,
3658 new_sticky_column: 0,
3659 },
3660 Event::Delete {
3661 range: range.clone(),
3662 deleted_text,
3663 cursor_id,
3664 },
3665 Event::Insert {
3666 position: match_pos,
3667 text: replacement_text,
3668 cursor_id,
3669 },
3670 ];
3671
3672 let batch = Event::Batch {
3674 events,
3675 description: format!(
3676 "Query replace '{}' with '{}'",
3677 ir_state.search, ir_state.replacement
3678 ),
3679 };
3680
3681 self.active_event_log_mut().append(batch.clone());
3683 self.apply_event_to_active_buffer(&batch);
3684
3685 Ok(())
3686 }
3687
3688 pub(super) fn move_to_current_match(&mut self, ir_state: &InteractiveReplaceState) {
3690 let match_pos = ir_state.current_match_pos;
3691 let active_split = self.split_manager.active_split();
3692 let active_buffer = self.active_buffer();
3693 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
3694 view_state.cursors.primary_mut().position = match_pos;
3695 view_state.cursors.primary_mut().anchor = None;
3696 let state = self.buffers.get_mut(&active_buffer).unwrap();
3698 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
3699 }
3700
3701 let msg = if ir_state.has_wrapped {
3703 "[Wrapped] Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
3704 } else {
3705 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
3706 };
3707 if let Some(ref mut prompt) = self.prompt {
3708 if prompt.prompt_type == PromptType::QueryReplaceConfirm {
3709 prompt.message = msg;
3710 prompt.input.clear();
3711 prompt.cursor_pos = 0;
3712 }
3713 }
3714 }
3715
3716 pub(super) fn finish_interactive_replace(&mut self, replacements_made: usize) {
3718 self.interactive_replace_state = None;
3719 self.prompt = None; let ns = self.search_namespace.clone();
3723 let state = self.active_state_mut();
3724 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3725
3726 self.set_status_message(t!("search.replaced_count", count = replacements_made).to_string());
3727 }
3728
3729 pub(super) fn smart_home(&mut self) {
3731 let estimated_line_length = self.config.editor.estimated_line_length;
3732 let cursor = *self.active_cursors().primary();
3733 let cursor_id = self.active_cursors().primary_id();
3734
3735 if self.config.editor.line_wrap {
3737 let split_id = self.split_manager.active_split();
3738 if let Some(new_pos) =
3739 self.smart_home_visual_line(split_id, cursor.position, estimated_line_length)
3740 {
3741 let event = Event::MoveCursor {
3742 cursor_id,
3743 old_position: cursor.position,
3744 new_position: new_pos,
3745 old_anchor: cursor.anchor,
3746 new_anchor: None,
3747 old_sticky_column: cursor.sticky_column,
3748 new_sticky_column: 0,
3749 };
3750 self.active_event_log_mut().append(event.clone());
3751 self.apply_event_to_active_buffer(&event);
3752 return;
3753 }
3754 }
3756
3757 let state = self.active_state_mut();
3758
3759 let mut iter = state
3761 .buffer
3762 .line_iterator(cursor.position, estimated_line_length);
3763 if let Some((line_start, line_content)) = iter.next_line() {
3764 let first_non_ws = line_content
3766 .chars()
3767 .take_while(|c| *c != '\n')
3768 .position(|c| !c.is_whitespace())
3769 .map(|offset| line_start + offset)
3770 .unwrap_or(line_start);
3771
3772 let new_pos = if cursor.position == first_non_ws {
3774 line_start
3775 } else {
3776 first_non_ws
3777 };
3778
3779 let event = Event::MoveCursor {
3780 cursor_id,
3781 old_position: cursor.position,
3782 new_position: new_pos,
3783 old_anchor: cursor.anchor,
3784 new_anchor: None,
3785 old_sticky_column: cursor.sticky_column,
3786 new_sticky_column: 0,
3787 };
3788
3789 self.active_event_log_mut().append(event.clone());
3790 self.apply_event_to_active_buffer(&event);
3791 }
3792 }
3793
3794 fn smart_home_visual_line(
3803 &mut self,
3804 split_id: LeafId,
3805 cursor_pos: usize,
3806 estimated_line_length: usize,
3807 ) -> Option<usize> {
3808 let visual_start = self
3809 .cached_layout
3810 .visual_line_start(split_id, cursor_pos, false)?;
3811
3812 let buffer_id = self.split_manager.active_buffer_id()?;
3814 let state = self.buffers.get_mut(&buffer_id)?;
3815 let mut iter = state
3816 .buffer
3817 .line_iterator(visual_start, estimated_line_length);
3818 let (phys_line_start, content) = iter.next_line()?;
3819
3820 let is_first_visual_row = visual_start == phys_line_start;
3821
3822 if is_first_visual_row {
3823 let visual_end = self
3825 .cached_layout
3826 .visual_line_end(split_id, cursor_pos, false)
3827 .unwrap_or(visual_start);
3828 let visual_len = visual_end.saturating_sub(visual_start);
3829 let first_non_ws = content
3830 .chars()
3831 .take(visual_len)
3832 .take_while(|c| *c != '\n')
3833 .position(|c| !c.is_whitespace())
3834 .map(|offset| visual_start + offset)
3835 .unwrap_or(visual_start);
3836
3837 if cursor_pos == first_non_ws {
3838 Some(visual_start)
3839 } else {
3840 Some(first_non_ws)
3841 }
3842 } else {
3843 if cursor_pos == visual_start {
3845 self.cached_layout
3847 .visual_line_start(split_id, cursor_pos, true)
3848 } else {
3849 Some(visual_start)
3850 }
3851 }
3852 }
3853
3854 pub(super) fn toggle_comment(&mut self) {
3856 let language = &self.active_state().language;
3859 let comment_prefix = self
3860 .config
3861 .languages
3862 .get(language)
3863 .and_then(|lang_config| lang_config.comment_prefix.clone());
3864
3865 let comment_prefix: String = match comment_prefix {
3866 Some(prefix) => {
3867 if prefix.ends_with(' ') {
3869 prefix
3870 } else {
3871 format!("{} ", prefix)
3872 }
3873 }
3874 None => return, };
3876
3877 let estimated_line_length = self.config.editor.estimated_line_length;
3878
3879 let cursor = *self.active_cursors().primary();
3880 let cursor_id = self.active_cursors().primary_id();
3881 let state = self.active_state_mut();
3882
3883 let original_anchor = cursor.anchor;
3885 let original_position = cursor.position;
3886 let had_selection = original_anchor.is_some();
3887
3888 let (start_pos, end_pos) = if let Some(range) = cursor.selection_range() {
3889 (range.start, range.end)
3890 } else {
3891 let iter = state
3892 .buffer
3893 .line_iterator(cursor.position, estimated_line_length);
3894 let line_start = iter.current_position();
3895 (line_start, cursor.position)
3896 };
3897
3898 let buffer_len = state.buffer.len();
3900 let mut line_starts = Vec::new();
3901 let mut iter = state.buffer.line_iterator(start_pos, estimated_line_length);
3902 let mut current_pos = iter.current_position();
3903 line_starts.push(current_pos);
3904
3905 while let Some((_, content)) = iter.next_line() {
3906 current_pos += content.len();
3907 if current_pos >= end_pos || current_pos >= buffer_len {
3908 break;
3909 }
3910 let next_iter = state
3911 .buffer
3912 .line_iterator(current_pos, estimated_line_length);
3913 let next_start = next_iter.current_position();
3914 if next_start != *line_starts.last().unwrap() {
3915 line_starts.push(next_start);
3916 }
3917 iter = state
3918 .buffer
3919 .line_iterator(current_pos, estimated_line_length);
3920 }
3921
3922 let all_commented = line_starts.iter().all(|&line_start| {
3925 let line_bytes = state
3926 .buffer
3927 .slice_bytes(line_start..buffer_len.min(line_start + comment_prefix.len() + 10));
3928 let line_str = String::from_utf8_lossy(&line_bytes);
3929 let trimmed = line_str.trim_start();
3930 trimmed.starts_with(comment_prefix.trim())
3931 });
3932
3933 let mut events = Vec::new();
3934 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
3937
3938 if all_commented {
3939 for &line_start in line_starts.iter().rev() {
3941 let line_bytes = state
3942 .buffer
3943 .slice_bytes(line_start..buffer_len.min(line_start + 100));
3944 let line_str = String::from_utf8_lossy(&line_bytes);
3945
3946 let leading_ws: usize = line_str
3948 .chars()
3949 .take_while(|c| c.is_whitespace() && *c != '\n')
3950 .map(|c| c.len_utf8())
3951 .sum();
3952 let rest = &line_str[leading_ws..];
3953
3954 if rest.starts_with(comment_prefix.trim()) {
3955 let remove_len = if rest.starts_with(&comment_prefix) {
3956 comment_prefix.len()
3957 } else {
3958 comment_prefix.trim().len()
3959 };
3960 let deleted_text = String::from_utf8_lossy(&state.buffer.slice_bytes(
3961 line_start + leading_ws..line_start + leading_ws + remove_len,
3962 ))
3963 .to_string();
3964 events.push(Event::Delete {
3965 range: (line_start + leading_ws)..(line_start + leading_ws + remove_len),
3966 deleted_text,
3967 cursor_id,
3968 });
3969 position_deltas.push((line_start, -(remove_len as isize)));
3970 }
3971 }
3972 } else {
3973 let prefix_len = comment_prefix.len();
3975 for &line_start in line_starts.iter().rev() {
3976 events.push(Event::Insert {
3977 position: line_start,
3978 text: comment_prefix.to_string(),
3979 cursor_id,
3980 });
3981 position_deltas.push((line_start, prefix_len as isize));
3982 }
3983 }
3984
3985 if events.is_empty() {
3986 return;
3987 }
3988
3989 let action_desc = if all_commented {
3990 "Uncomment"
3991 } else {
3992 "Comment"
3993 };
3994
3995 if had_selection {
3997 position_deltas.sort_by_key(|(pos, _)| *pos);
3999
4000 let calc_shift = |original_pos: usize| -> isize {
4002 let mut shift: isize = 0;
4003 for (edit_pos, delta) in &position_deltas {
4004 if *edit_pos < original_pos {
4005 shift += delta;
4006 }
4007 }
4008 shift
4009 };
4010
4011 let anchor_shift = calc_shift(original_anchor.unwrap_or(0));
4012 let position_shift = calc_shift(original_position);
4013
4014 let new_anchor = (original_anchor.unwrap_or(0) as isize + anchor_shift).max(0) as usize;
4015 let new_position = (original_position as isize + position_shift).max(0) as usize;
4016
4017 events.push(Event::MoveCursor {
4018 cursor_id,
4019 old_position: original_position,
4020 new_position,
4021 old_anchor: original_anchor,
4022 new_anchor: Some(new_anchor),
4023 old_sticky_column: 0,
4024 new_sticky_column: 0,
4025 });
4026 }
4027
4028 let description = format!("{} lines", action_desc);
4030 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
4031 self.active_event_log_mut().append(bulk_edit);
4032 }
4033
4034 self.set_status_message(
4035 t!(
4036 "lines.action",
4037 action = action_desc,
4038 count = line_starts.len()
4039 )
4040 .to_string(),
4041 );
4042 }
4043
4044 pub(super) fn goto_matching_bracket(&mut self) {
4046 let cursor = *self.active_cursors().primary();
4047 let cursor_id = self.active_cursors().primary_id();
4048 let state = self.active_state_mut();
4049
4050 let pos = cursor.position;
4051 if pos >= state.buffer.len() {
4052 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4053 return;
4054 }
4055
4056 let bytes = state.buffer.slice_bytes(pos..pos + 1);
4057 if bytes.is_empty() {
4058 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4059 return;
4060 }
4061
4062 let ch = bytes[0] as char;
4063
4064 const BRACKET_PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
4066
4067 let bracket_info = match ch {
4068 '(' => Some(('(', ')', true)),
4069 ')' => Some(('(', ')', false)),
4070 '[' => Some(('[', ']', true)),
4071 ']' => Some(('[', ']', false)),
4072 '{' => Some(('{', '}', true)),
4073 '}' => Some(('{', '}', false)),
4074 '<' => Some(('<', '>', true)),
4075 '>' => Some(('<', '>', false)),
4076 _ => None,
4077 };
4078
4079 use crate::view::bracket_highlight_overlay::MAX_BRACKET_SEARCH_BYTES;
4081
4082 let (opening, closing, search_start, forward) =
4085 if let Some((opening, closing, forward)) = bracket_info {
4086 (opening, closing, pos, forward)
4087 } else {
4088 let mut depths: Vec<i32> = vec![0; BRACKET_PAIRS.len()];
4091 let mut found = None;
4092 let search_limit = pos.saturating_sub(MAX_BRACKET_SEARCH_BYTES);
4093 let mut search_pos = pos.saturating_sub(1);
4094 loop {
4095 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4096 if !b.is_empty() {
4097 let c = b[0] as char;
4098 for (i, &(open, close)) in BRACKET_PAIRS.iter().enumerate() {
4099 if c == close {
4100 depths[i] += 1;
4101 } else if c == open {
4102 if depths[i] > 0 {
4103 depths[i] -= 1;
4104 } else {
4105 found = Some((open, close, search_pos));
4107 break;
4108 }
4109 }
4110 }
4111 if found.is_some() {
4112 break;
4113 }
4114 }
4115 if search_pos <= search_limit {
4116 break;
4117 }
4118 search_pos -= 1;
4119 }
4120
4121 if let Some((opening, closing, bracket_pos)) = found {
4122 (opening, closing, bracket_pos, true)
4124 } else {
4125 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4126 return;
4127 }
4128 };
4129
4130 let buffer_len = state.buffer.len();
4132 let mut depth = 1;
4133 let matching_pos = if forward {
4134 let search_limit = (search_start + 1 + MAX_BRACKET_SEARCH_BYTES).min(buffer_len);
4135 let mut search_pos = search_start + 1;
4136 let mut found = None;
4137 while search_pos < search_limit && depth > 0 {
4138 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4139 if !b.is_empty() {
4140 let c = b[0] as char;
4141 if c == opening {
4142 depth += 1;
4143 } else if c == closing {
4144 depth -= 1;
4145 if depth == 0 {
4146 found = Some(search_pos);
4147 }
4148 }
4149 }
4150 search_pos += 1;
4151 }
4152 found
4153 } else {
4154 let search_limit = search_start.saturating_sub(MAX_BRACKET_SEARCH_BYTES);
4155 let mut search_pos = search_start.saturating_sub(1);
4156 let mut found = None;
4157 loop {
4158 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4159 if !b.is_empty() {
4160 let c = b[0] as char;
4161 if c == closing {
4162 depth += 1;
4163 } else if c == opening {
4164 depth -= 1;
4165 if depth == 0 {
4166 found = Some(search_pos);
4167 break;
4168 }
4169 }
4170 }
4171 if search_pos <= search_limit {
4172 break;
4173 }
4174 search_pos -= 1;
4175 }
4176 found
4177 };
4178
4179 if let Some(new_pos) = matching_pos {
4180 let event = Event::MoveCursor {
4181 cursor_id,
4182 old_position: cursor.position,
4183 new_position: new_pos,
4184 old_anchor: cursor.anchor,
4185 new_anchor: None,
4186 old_sticky_column: cursor.sticky_column,
4187 new_sticky_column: 0,
4188 };
4189 self.active_event_log_mut().append(event.clone());
4190 self.apply_event_to_active_buffer(&event);
4191 } else {
4192 self.set_status_message(t!("diagnostics.bracket_no_match").to_string());
4193 }
4194 }
4195
4196 pub(super) fn jump_to_next_error(&mut self) {
4198 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
4199 let cursor_pos = self.active_cursors().primary().position;
4200 let cursor_id = self.active_cursors().primary_id();
4201 let cursor = *self.active_cursors().primary();
4202 let state = self.active_state_mut();
4203
4204 let mut diagnostic_positions: Vec<usize> = state
4206 .overlays
4207 .all()
4208 .iter()
4209 .filter_map(|overlay| {
4210 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4212 Some(overlay.range(&state.marker_list).start)
4213 } else {
4214 None
4215 }
4216 })
4217 .collect();
4218
4219 if diagnostic_positions.is_empty() {
4220 self.set_status_message(t!("diagnostics.none").to_string());
4221 return;
4222 }
4223
4224 diagnostic_positions.sort_unstable();
4226 diagnostic_positions.dedup();
4227
4228 let next_pos = diagnostic_positions
4230 .iter()
4231 .find(|&&pos| pos > cursor_pos)
4232 .or_else(|| diagnostic_positions.first()) .copied();
4234
4235 if let Some(new_pos) = next_pos {
4236 let event = Event::MoveCursor {
4237 cursor_id,
4238 old_position: cursor.position,
4239 new_position: new_pos,
4240 old_anchor: cursor.anchor,
4241 new_anchor: None,
4242 old_sticky_column: cursor.sticky_column,
4243 new_sticky_column: 0,
4244 };
4245 self.active_event_log_mut().append(event.clone());
4246 self.apply_event_to_active_buffer(&event);
4247
4248 let state = self.active_state();
4250 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
4251 let range = overlay.range(&state.marker_list);
4252 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4253 overlay.message.clone()
4254 } else {
4255 None
4256 }
4257 }) {
4258 self.set_status_message(msg);
4259 }
4260 }
4261 }
4262
4263 pub(super) fn jump_to_previous_error(&mut self) {
4265 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
4266 let cursor_pos = self.active_cursors().primary().position;
4267 let cursor_id = self.active_cursors().primary_id();
4268 let cursor = *self.active_cursors().primary();
4269 let state = self.active_state_mut();
4270
4271 let mut diagnostic_positions: Vec<usize> = state
4273 .overlays
4274 .all()
4275 .iter()
4276 .filter_map(|overlay| {
4277 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4279 Some(overlay.range(&state.marker_list).start)
4280 } else {
4281 None
4282 }
4283 })
4284 .collect();
4285
4286 if diagnostic_positions.is_empty() {
4287 self.set_status_message(t!("diagnostics.none").to_string());
4288 return;
4289 }
4290
4291 diagnostic_positions.sort_unstable();
4293 diagnostic_positions.dedup();
4294
4295 let prev_pos = diagnostic_positions
4297 .iter()
4298 .rev()
4299 .find(|&&pos| pos < cursor_pos)
4300 .or_else(|| diagnostic_positions.last()) .copied();
4302
4303 if let Some(new_pos) = prev_pos {
4304 let event = Event::MoveCursor {
4305 cursor_id,
4306 old_position: cursor.position,
4307 new_position: new_pos,
4308 old_anchor: cursor.anchor,
4309 new_anchor: None,
4310 old_sticky_column: cursor.sticky_column,
4311 new_sticky_column: 0,
4312 };
4313 self.active_event_log_mut().append(event.clone());
4314 self.apply_event_to_active_buffer(&event);
4315
4316 let state = self.active_state();
4318 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
4319 let range = overlay.range(&state.marker_list);
4320 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4321 overlay.message.clone()
4322 } else {
4323 None
4324 }
4325 }) {
4326 self.set_status_message(msg);
4327 }
4328 }
4329 }
4330
4331 pub(super) fn toggle_macro_recording(&mut self, key: char) {
4333 if let Some(state) = &self.macro_recording {
4334 if state.key == key {
4335 self.stop_macro_recording();
4337 } else {
4338 self.stop_macro_recording();
4340 self.start_macro_recording(key);
4341 }
4342 } else {
4343 self.start_macro_recording(key);
4345 }
4346 }
4347
4348 pub(super) fn start_macro_recording(&mut self, key: char) {
4350 self.macro_recording = Some(MacroRecordingState {
4351 key,
4352 actions: Vec::new(),
4353 });
4354
4355 let stop_hint = self.build_macro_stop_hint(key);
4357 self.set_status_message(
4358 t!(
4359 "macro.recording_with_hint",
4360 key = key,
4361 stop_hint = stop_hint
4362 )
4363 .to_string(),
4364 );
4365 }
4366
4367 fn build_macro_stop_hint(&self, _key: char) -> String {
4369 let mut hints = Vec::new();
4370
4371 if let Some(stop_key) = self.get_keybinding_for_action("stop_macro_recording") {
4373 hints.push(stop_key);
4374 }
4375
4376 let palette_key = self
4378 .get_keybinding_for_action("command_palette")
4379 .unwrap_or_else(|| "Ctrl+P".to_string());
4380
4381 if hints.is_empty() {
4382 format!("{} → Stop Recording Macro", palette_key)
4384 } else {
4385 format!("{} or {} → Stop Recording", hints.join("/"), palette_key)
4387 }
4388 }
4389
4390 pub(super) fn stop_macro_recording(&mut self) {
4392 if let Some(state) = self.macro_recording.take() {
4393 let action_count = state.actions.len();
4394 let key = state.key;
4395 self.macros.insert(key, state.actions);
4396 self.last_macro_register = Some(key);
4397
4398 let play_hint = self.build_macro_play_hint();
4400 self.set_status_message(
4401 t!(
4402 "macro.saved",
4403 key = key,
4404 count = action_count,
4405 play_hint = play_hint
4406 )
4407 .to_string(),
4408 );
4409 } else {
4410 self.set_status_message(t!("macro.not_recording").to_string());
4411 }
4412 }
4413
4414 fn build_macro_play_hint(&self) -> String {
4416 if let Some(play_key) = self.get_keybinding_for_action("play_last_macro") {
4418 return format!("{} → Play Last Macro", play_key);
4419 }
4420
4421 let palette_key = self
4423 .get_keybinding_for_action("command_palette")
4424 .unwrap_or_else(|| "Ctrl+P".to_string());
4425
4426 format!("{} → Play Macro", palette_key)
4427 }
4428
4429 pub fn recompute_layout(&mut self, width: u16, height: u16) {
4434 let size = ratatui::layout::Rect::new(0, 0, width, height);
4435
4436 let active_split = self.split_manager.active_split();
4438 self.pre_sync_ensure_visible(active_split);
4439 self.sync_scroll_groups();
4440
4441 let constraints = vec![
4444 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
4445 Constraint::Min(0),
4446 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(if self.prompt_line_visible { 1 } else { 0 }), ];
4450 let main_chunks = Layout::default()
4451 .direction(Direction::Vertical)
4452 .constraints(constraints)
4453 .split(size);
4454 let main_content_area = main_chunks[1];
4455
4456 let file_explorer_should_show = self.file_explorer_visible
4458 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
4459 let editor_content_area = if file_explorer_should_show {
4460 let explorer_percent = (self.file_explorer_width_percent * 100.0) as u16;
4461 let editor_percent = 100 - explorer_percent;
4462 let horizontal_chunks = Layout::default()
4463 .direction(Direction::Horizontal)
4464 .constraints([
4465 Constraint::Percentage(explorer_percent),
4466 Constraint::Percentage(editor_percent),
4467 ])
4468 .split(main_content_area);
4469 horizontal_chunks[1]
4470 } else {
4471 main_content_area
4472 };
4473
4474 let view_line_mappings = SplitRenderer::compute_content_layout(
4476 editor_content_area,
4477 &self.split_manager,
4478 &mut self.buffers,
4479 &mut self.split_view_states,
4480 &self.theme,
4481 false, self.config.editor.estimated_line_length,
4483 self.config.editor.highlight_context_bytes,
4484 self.config.editor.relative_line_numbers,
4485 self.config.editor.use_terminal_bg,
4486 self.session_mode || !self.software_cursor_only,
4487 self.software_cursor_only,
4488 self.tab_bar_visible,
4489 self.config.editor.show_vertical_scrollbar,
4490 self.config.editor.show_horizontal_scrollbar,
4491 self.config.editor.diagnostics_inline_text,
4492 self.config.editor.show_tilde,
4493 );
4494
4495 self.cached_layout.view_line_mappings = view_line_mappings;
4496 }
4497
4498 pub(super) fn play_macro(&mut self, key: char) {
4505 if self.macro_playing {
4507 return;
4508 }
4509
4510 if let Some(actions) = self.macros.get(&key).cloned() {
4511 if actions.is_empty() {
4512 self.set_status_message(t!("macro.empty", key = key).to_string());
4513 return;
4514 }
4515
4516 self.macro_playing = true;
4517 let action_count = actions.len();
4518 let width = self.cached_layout.last_frame_width;
4519 let height = self.cached_layout.last_frame_height;
4520 for action in actions {
4521 if let Err(e) = self.handle_action(action) {
4522 tracing::warn!("Macro action failed: {}", e);
4523 }
4524 self.recompute_layout(width, height);
4525 }
4526 self.macro_playing = false;
4527
4528 self.set_status_message(
4529 t!("macro.played", key = key, count = action_count).to_string(),
4530 );
4531 } else {
4532 self.set_status_message(t!("macro.not_found", key = key).to_string());
4533 }
4534 }
4535
4536 pub(super) fn record_macro_action(&mut self, action: &Action) {
4538 if self.macro_playing {
4540 return;
4541 }
4542 if let Some(state) = &mut self.macro_recording {
4543 match action {
4545 Action::StartMacroRecording
4546 | Action::StopMacroRecording
4547 | Action::PlayMacro(_)
4548 | Action::ToggleMacroRecording(_)
4549 | Action::ShowMacro(_)
4550 | Action::ListMacros
4551 | Action::PromptRecordMacro
4552 | Action::PromptPlayMacro
4553 | Action::PlayLastMacro => {}
4554 Action::PromptConfirm => {
4557 if let Some(prompt) = &self.prompt {
4558 let text = prompt.get_text().to_string();
4559 state.actions.push(Action::PromptConfirmWithText(text));
4560 } else {
4561 state.actions.push(action.clone());
4562 }
4563 }
4564 _ => {
4565 state.actions.push(action.clone());
4566 }
4567 }
4568 }
4569 }
4570
4571 pub(super) fn show_macro_in_buffer(&mut self, key: char) {
4573 let (json, actions_len) = match self.macros.get(&key) {
4575 Some(actions) => {
4576 let json = match serde_json::to_string_pretty(actions) {
4577 Ok(json) => json,
4578 Err(e) => {
4579 self.set_status_message(
4580 t!("macro.serialize_failed", error = e.to_string()).to_string(),
4581 );
4582 return;
4583 }
4584 };
4585 (json, actions.len())
4586 }
4587 None => {
4588 self.set_status_message(t!("macro.not_found", key = key).to_string());
4589 return;
4590 }
4591 };
4592
4593 let content = format!(
4595 "// Macro '{}' ({} actions)\n// This buffer can be saved as a .json file for persistence\n\n{}",
4596 key,
4597 actions_len,
4598 json
4599 );
4600
4601 let buffer_id = BufferId(self.next_buffer_id);
4603 self.next_buffer_id += 1;
4604
4605 let mut state = EditorState::new(
4606 self.terminal_width,
4607 self.terminal_height,
4608 self.config.editor.large_file_threshold_bytes as usize,
4609 std::sync::Arc::clone(&self.filesystem),
4610 );
4611 state
4612 .margins
4613 .configure_for_line_numbers(self.config.editor.line_numbers);
4614
4615 self.buffers.insert(buffer_id, state);
4616 self.event_logs.insert(buffer_id, EventLog::new());
4617
4618 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4620 state.buffer = crate::model::buffer::Buffer::from_str(
4621 &content,
4622 self.config.editor.large_file_threshold_bytes as usize,
4623 std::sync::Arc::clone(&self.filesystem),
4624 );
4625 }
4626
4627 let metadata = BufferMetadata {
4629 kind: BufferKind::Virtual {
4630 mode: "macro-view".to_string(),
4631 },
4632 display_name: format!("*Macro {}*", key),
4633 lsp_enabled: false,
4634 lsp_disabled_reason: Some("Virtual macro buffer".to_string()),
4635 read_only: false, binary: false,
4637 lsp_opened_with: std::collections::HashSet::new(),
4638 hidden_from_tabs: false,
4639 recovery_id: None,
4640 };
4641 self.buffer_metadata.insert(buffer_id, metadata);
4642
4643 self.set_active_buffer(buffer_id);
4645 self.set_status_message(
4646 t!("macro.shown_buffer", key = key, count = actions_len).to_string(),
4647 );
4648 }
4649
4650 pub(super) fn list_macros_in_buffer(&mut self) {
4652 if self.macros.is_empty() {
4653 self.set_status_message(t!("macro.none_recorded").to_string());
4654 return;
4655 }
4656
4657 let mut content =
4659 String::from("// Recorded Macros\n// Use ShowMacro(key) to see details\n\n");
4660
4661 let mut keys: Vec<char> = self.macros.keys().copied().collect();
4662 keys.sort();
4663
4664 for key in keys {
4665 if let Some(actions) = self.macros.get(&key) {
4666 content.push_str(&format!("Macro '{}': {} actions\n", key, actions.len()));
4667
4668 for (i, action) in actions.iter().enumerate() {
4670 content.push_str(&format!(" {}. {:?}\n", i + 1, action));
4671 }
4672 content.push('\n');
4673 }
4674 }
4675
4676 let buffer_id = BufferId(self.next_buffer_id);
4678 self.next_buffer_id += 1;
4679
4680 let mut state = EditorState::new(
4681 self.terminal_width,
4682 self.terminal_height,
4683 self.config.editor.large_file_threshold_bytes as usize,
4684 std::sync::Arc::clone(&self.filesystem),
4685 );
4686 state
4687 .margins
4688 .configure_for_line_numbers(self.config.editor.line_numbers);
4689
4690 self.buffers.insert(buffer_id, state);
4691 self.event_logs.insert(buffer_id, EventLog::new());
4692
4693 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4695 state.buffer = crate::model::buffer::Buffer::from_str(
4696 &content,
4697 self.config.editor.large_file_threshold_bytes as usize,
4698 std::sync::Arc::clone(&self.filesystem),
4699 );
4700 }
4701
4702 let metadata = BufferMetadata {
4704 kind: BufferKind::Virtual {
4705 mode: "macro-list".to_string(),
4706 },
4707 display_name: "*Macros*".to_string(),
4708 lsp_enabled: false,
4709 lsp_disabled_reason: Some("Virtual macro list buffer".to_string()),
4710 read_only: true,
4711 binary: false,
4712 lsp_opened_with: std::collections::HashSet::new(),
4713 hidden_from_tabs: false,
4714 recovery_id: None,
4715 };
4716 self.buffer_metadata.insert(buffer_id, metadata);
4717
4718 self.set_active_buffer(buffer_id);
4720 self.set_status_message(t!("macro.showing", count = self.macros.len()).to_string());
4721 }
4722
4723 pub(super) fn set_bookmark(&mut self, key: char) {
4725 let buffer_id = self.active_buffer();
4726 let position = self.active_cursors().primary().position;
4727 self.bookmarks.insert(
4728 key,
4729 Bookmark {
4730 buffer_id,
4731 position,
4732 },
4733 );
4734 self.set_status_message(t!("bookmark.set", key = key).to_string());
4735 }
4736
4737 pub(super) fn jump_to_bookmark(&mut self, key: char) {
4739 if let Some(bookmark) = self.bookmarks.get(&key).cloned() {
4740 if bookmark.buffer_id != self.active_buffer() {
4742 if self.buffers.contains_key(&bookmark.buffer_id) {
4743 self.set_active_buffer(bookmark.buffer_id);
4744 } else {
4745 self.set_status_message(t!("bookmark.buffer_gone", key = key).to_string());
4746 self.bookmarks.remove(&key);
4747 return;
4748 }
4749 }
4750
4751 let cursor = *self.active_cursors().primary();
4753 let cursor_id = self.active_cursors().primary_id();
4754 let state = self.active_state_mut();
4755 let new_pos = bookmark.position.min(state.buffer.len());
4756
4757 let event = Event::MoveCursor {
4758 cursor_id,
4759 old_position: cursor.position,
4760 new_position: new_pos,
4761 old_anchor: cursor.anchor,
4762 new_anchor: None,
4763 old_sticky_column: cursor.sticky_column,
4764 new_sticky_column: 0,
4765 };
4766
4767 self.active_event_log_mut().append(event.clone());
4768 self.apply_event_to_active_buffer(&event);
4769 self.set_status_message(t!("bookmark.jumped", key = key).to_string());
4770 } else {
4771 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
4772 }
4773 }
4774
4775 pub(super) fn clear_bookmark(&mut self, key: char) {
4777 if self.bookmarks.remove(&key).is_some() {
4778 self.set_status_message(t!("bookmark.cleared", key = key).to_string());
4779 } else {
4780 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
4781 }
4782 }
4783
4784 pub(super) fn list_bookmarks(&mut self) {
4786 if self.bookmarks.is_empty() {
4787 self.set_status_message(t!("bookmark.none_set").to_string());
4788 return;
4789 }
4790
4791 let mut bookmark_list: Vec<_> = self.bookmarks.iter().collect();
4792 bookmark_list.sort_by_key(|(k, _)| *k);
4793
4794 let list_str: String = bookmark_list
4795 .iter()
4796 .map(|(k, bm)| {
4797 let buffer_name = self
4798 .buffer_metadata
4799 .get(&bm.buffer_id)
4800 .map(|m| m.display_name.as_str())
4801 .unwrap_or("unknown");
4802 format!("'{}': {} @ {}", k, buffer_name, bm.position)
4803 })
4804 .collect::<Vec<_>>()
4805 .join(", ");
4806
4807 self.set_status_message(t!("bookmark.list", list = list_str).to_string());
4808 }
4809
4810 pub fn clear_search_history(&mut self) {
4813 if let Some(history) = self.prompt_histories.get_mut("search") {
4814 history.clear();
4815 }
4816 }
4817
4818 pub fn save_histories(&self) {
4821 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.data_dir) {
4823 tracing::warn!("Failed to create data directory: {}", e);
4824 return;
4825 }
4826
4827 for (key, history) in &self.prompt_histories {
4829 let path = self.dir_context.prompt_history_path(key);
4830 if let Err(e) = history.save_to_file(&path) {
4831 tracing::warn!("Failed to save {} history: {}", key, e);
4832 } else {
4833 tracing::debug!("Saved {} history to {:?}", key, path);
4834 }
4835 }
4836 }
4837
4838 pub(super) fn ensure_active_tab_visible(
4842 &mut self,
4843 split_id: LeafId,
4844 active_buffer: BufferId,
4845 available_width: u16,
4846 ) {
4847 tracing::debug!(
4848 "ensure_active_tab_visible called: split={:?}, buffer={:?}, width={}",
4849 split_id,
4850 active_buffer,
4851 available_width
4852 );
4853 let Some(view_state) = self.split_view_states.get_mut(&split_id) else {
4854 tracing::debug!(" -> no view_state for split");
4855 return;
4856 };
4857
4858 let split_buffers = view_state.open_buffers.clone();
4859
4860 let (tab_widths, rendered_buffer_ids) = crate::view::ui::tabs::calculate_tab_widths(
4862 &split_buffers,
4863 &self.buffers,
4864 &self.buffer_metadata,
4865 &self.composite_buffers,
4866 );
4867
4868 let total_tabs_width: usize = tab_widths.iter().sum();
4869 let max_visible_width = available_width as usize;
4870
4871 let active_tab_index = rendered_buffer_ids
4874 .iter()
4875 .position(|id| *id == active_buffer);
4876
4877 let active_width_index = active_tab_index.map(|buf_idx| {
4881 if buf_idx == 0 {
4882 0
4883 } else {
4884 buf_idx * 2
4889 }
4890 });
4891
4892 let old_offset = view_state.tab_scroll_offset;
4894 let new_scroll_offset = if let Some(idx) = active_width_index {
4895 crate::view::ui::tabs::scroll_to_show_tab(
4896 &tab_widths,
4897 idx,
4898 view_state.tab_scroll_offset,
4899 max_visible_width,
4900 )
4901 } else {
4902 view_state
4903 .tab_scroll_offset
4904 .min(total_tabs_width.saturating_sub(max_visible_width))
4905 };
4906
4907 tracing::debug!(
4908 " -> offset: {} -> {} (idx={:?}, max_width={}, total={})",
4909 old_offset,
4910 new_scroll_offset,
4911 active_width_index,
4912 max_visible_width,
4913 total_tabs_width
4914 );
4915 view_state.tab_scroll_offset = new_scroll_offset;
4916 }
4917
4918 fn sync_scroll_groups(&mut self) {
4924 let active_split = self.split_manager.active_split();
4925 let group_count = self.scroll_sync_manager.groups().len();
4926
4927 if group_count > 0 {
4928 tracing::debug!(
4929 "sync_scroll_groups: active_split={:?}, {} groups",
4930 active_split,
4931 group_count
4932 );
4933 }
4934
4935 let sync_info: Vec<_> = self
4938 .scroll_sync_manager
4939 .groups()
4940 .iter()
4941 .filter_map(|group| {
4942 tracing::debug!(
4943 "sync_scroll_groups: checking group {}, left={:?}, right={:?}",
4944 group.id,
4945 group.left_split,
4946 group.right_split
4947 );
4948
4949 if !group.contains_split(active_split.into()) {
4950 tracing::debug!(
4951 "sync_scroll_groups: active split {:?} not in group",
4952 active_split
4953 );
4954 return None;
4955 }
4956
4957 let active_top_byte = self
4959 .split_view_states
4960 .get(&active_split)?
4961 .viewport
4962 .top_byte;
4963
4964 let active_buffer_id = self.split_manager.buffer_for_split(active_split)?;
4966 let buffer_state = self.buffers.get(&active_buffer_id)?;
4967 let buffer_len = buffer_state.buffer.len();
4968 let active_line = buffer_state.buffer.get_line_number(active_top_byte);
4969
4970 tracing::debug!(
4971 "sync_scroll_groups: active_split={:?}, buffer_id={:?}, top_byte={}, buffer_len={}, active_line={}",
4972 active_split,
4973 active_buffer_id,
4974 active_top_byte,
4975 buffer_len,
4976 active_line
4977 );
4978
4979 let (other_split, other_line) = if group.is_left_split(active_split.into()) {
4981 (group.right_split, group.left_to_right_line(active_line))
4983 } else {
4984 (group.left_split, group.right_to_left_line(active_line))
4986 };
4987
4988 tracing::debug!(
4989 "sync_scroll_groups: syncing other_split={:?} to line {}",
4990 other_split,
4991 other_line
4992 );
4993
4994 Some((other_split, other_line))
4995 })
4996 .collect();
4997
4998 for (other_split, target_line) in sync_info {
5000 let other_leaf = LeafId(other_split);
5001 if let Some(buffer_id) = self.split_manager.buffer_for_split(other_leaf) {
5002 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5003 let buffer = &mut state.buffer;
5004 if let Some(view_state) = self.split_view_states.get_mut(&other_leaf) {
5005 view_state.viewport.scroll_to(buffer, target_line);
5006 }
5007 }
5008 }
5009 }
5010
5011 let active_buffer_id = if self.same_buffer_scroll_sync {
5021 self.split_manager.buffer_for_split(active_split)
5022 } else {
5023 None
5024 };
5025 if let Some(active_buf_id) = active_buffer_id {
5026 let active_top_byte = self
5027 .split_view_states
5028 .get(&active_split)
5029 .map(|vs| vs.viewport.top_byte);
5030 let active_viewport_height = self
5031 .split_view_states
5032 .get(&active_split)
5033 .map(|vs| vs.viewport.visible_line_count())
5034 .unwrap_or(0);
5035
5036 if let Some(top_byte) = active_top_byte {
5037 let other_splits: Vec<_> = self
5039 .split_view_states
5040 .keys()
5041 .filter(|&&s| {
5042 s != active_split
5043 && self.split_manager.buffer_for_split(s) == Some(active_buf_id)
5044 && !self.scroll_sync_manager.is_split_synced(s.into())
5045 })
5046 .copied()
5047 .collect();
5048
5049 if !other_splits.is_empty() {
5050 let at_bottom = if let Some(state) = self.buffers.get_mut(&active_buf_id) {
5053 let mut iter = state.buffer.line_iterator(top_byte, 80);
5054 let mut lines_remaining = 0;
5055 while iter.next_line().is_some() {
5056 lines_remaining += 1;
5057 if lines_remaining > active_viewport_height {
5058 break;
5059 }
5060 }
5061 lines_remaining <= active_viewport_height
5062 } else {
5063 false
5064 };
5065
5066 for other_split in other_splits {
5067 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
5068 view_state.viewport.top_byte = top_byte;
5069 view_state.viewport.sync_scroll_to_end = at_bottom;
5072 }
5073 }
5074 }
5075 }
5076 }
5077 }
5078
5079 fn pre_sync_ensure_visible(&mut self, active_split: LeafId) {
5088 let group_info = self
5090 .scroll_sync_manager
5091 .find_group_for_split(active_split.into())
5092 .map(|g| (g.left_split, g.right_split));
5093
5094 if let Some((left_split, right_split)) = group_info {
5095 if let Some(buffer_id) = self.split_manager.buffer_for_split(active_split) {
5097 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5098 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
5099 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
5101
5102 tracing::debug!(
5103 "pre_sync_ensure_visible: updated active split {:?} viewport, top_byte={}",
5104 active_split,
5105 view_state.viewport.top_byte
5106 );
5107 }
5108 }
5109 }
5110
5111 let active_sid: SplitId = active_split.into();
5113 let other_split: SplitId = if active_sid == left_split {
5114 right_split
5115 } else {
5116 left_split
5117 };
5118
5119 if let Some(view_state) = self.split_view_states.get_mut(&LeafId(other_split)) {
5120 view_state.viewport.set_skip_ensure_visible();
5121 tracing::debug!(
5122 "pre_sync_ensure_visible: marked other split {:?} to skip ensure_visible",
5123 other_split
5124 );
5125 }
5126 }
5127
5128 if !self.same_buffer_scroll_sync {
5131 } else if let Some(active_buf_id) = self.split_manager.buffer_for_split(active_split) {
5133 let other_same_buffer_splits: Vec<_> = self
5134 .split_view_states
5135 .keys()
5136 .filter(|&&s| {
5137 s != active_split
5138 && self.split_manager.buffer_for_split(s) == Some(active_buf_id)
5139 && !self.scroll_sync_manager.is_split_synced(s.into())
5140 })
5141 .copied()
5142 .collect();
5143
5144 for other_split in other_same_buffer_splits {
5145 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
5146 view_state.viewport.set_skip_ensure_visible();
5147 }
5148 }
5149 }
5150 }
5151}