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(|conn| {
189 if self.filesystem.is_remote_connected() {
190 conn.to_string()
191 } else {
192 format!("{} (Disconnected)", conn)
193 }
194 });
195
196 if let Some(ref mut explorer) = self.file_explorer {
198 let is_focused = self.key_context == KeyContext::FileExplorer;
199
200 let mut files_with_unsaved_changes = std::collections::HashSet::new();
202 for (buffer_id, state) in &self.buffers {
203 if state.buffer.is_modified() {
204 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
205 if let Some(file_path) = metadata.file_path() {
206 files_with_unsaved_changes.insert(file_path.clone());
207 }
208 }
209 }
210 }
211
212 let close_button_hovered = matches!(
213 &self.mouse_state.hover_target,
214 Some(HoverTarget::FileExplorerCloseButton)
215 );
216 let keybindings = self.keybindings.read().unwrap();
217 FileExplorerRenderer::render(
218 explorer,
219 frame,
220 horizontal_chunks[0],
221 is_focused,
222 &files_with_unsaved_changes,
223 &self.file_explorer_decoration_cache,
224 &keybindings,
225 self.key_context.clone(),
226 &self.theme,
227 close_button_hovered,
228 remote_connection.as_deref(),
229 );
230 }
231 } else {
234 self.cached_layout.file_explorer_area = None;
236 editor_content_area = main_content_area;
237 }
238
239 if self.plugin_manager.is_active() {
246 let hooks_start = std::time::Instant::now();
247 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
249
250 let mut total_new_lines = 0usize;
251 for (split_id, buffer_id, split_area) in visible_buffers {
252 let viewport_top_byte = self
254 .split_view_states
255 .get(&split_id)
256 .map(|vs| vs.viewport.top_byte)
257 .unwrap_or(0);
258
259 if let Some(state) = self.buffers.get_mut(&buffer_id) {
260 self.plugin_manager.run_hook(
262 "render_start",
263 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
264 );
265
266 let visible_count = split_area.height as usize;
269 let is_binary = state.buffer.is_binary();
270 let line_ending = state.buffer.line_ending();
271 let base_tokens =
272 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
273 &mut state.buffer,
274 viewport_top_byte,
275 self.config.editor.estimated_line_length,
276 visible_count,
277 is_binary,
278 line_ending,
279 );
280 let viewport_start = viewport_top_byte;
281 let viewport_end = base_tokens
282 .last()
283 .and_then(|t| t.source_offset)
284 .unwrap_or(viewport_start);
285 let cursor_positions: Vec<usize> = self
286 .split_view_states
287 .get(&split_id)
288 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
289 .unwrap_or_default();
290 self.plugin_manager.run_hook(
291 "view_transform_request",
292 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
293 buffer_id,
294 split_id: split_id.into(),
295 viewport_start,
296 viewport_end,
297 tokens: base_tokens,
298 cursor_positions,
299 },
300 );
301
302 if let Some(vs) = self.split_view_states.get_mut(&split_id) {
306 vs.view_transform_stale = false;
307 }
308
309 let visible_count = split_area.height as usize;
311 let top_byte = viewport_top_byte;
312
313 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
315
316 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
318 let mut line_number = state.buffer.get_line_number(top_byte);
319 let mut iter = state
320 .buffer
321 .line_iterator(top_byte, self.config.editor.estimated_line_length);
322
323 for _ in 0..visible_count {
324 if let Some((line_start, line_content)) = iter.next_line() {
325 let byte_end = line_start + line_content.len();
326 let byte_range = (line_start, byte_end);
327
328 if !seen_byte_ranges.contains(&byte_range) {
330 new_lines.push(crate::services::plugins::hooks::LineInfo {
331 line_number,
332 byte_start: line_start,
333 byte_end,
334 content: line_content,
335 });
336 seen_byte_ranges.insert(byte_range);
337 }
338 line_number += 1;
339 } else {
340 break;
341 }
342 }
343
344 if !new_lines.is_empty() {
346 total_new_lines += new_lines.len();
347 self.plugin_manager.run_hook(
348 "lines_changed",
349 crate::services::plugins::hooks::HookArgs::LinesChanged {
350 buffer_id,
351 lines: new_lines,
352 },
353 );
354 }
355 }
356 }
357 let hooks_elapsed = hooks_start.elapsed();
358 tracing::trace!(
359 new_lines = total_new_lines,
360 elapsed_ms = hooks_elapsed.as_millis(),
361 elapsed_us = hooks_elapsed.as_micros(),
362 "lines_changed hooks total"
363 );
364
365 let commands = self.plugin_manager.process_commands();
377 if !commands.is_empty() {
378 let cmd_names: Vec<String> =
379 commands.iter().map(|c| c.debug_variant_name()).collect();
380 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
381 }
382 for command in commands {
383 if let Err(e) = self.handle_plugin_command(command) {
384 tracing::error!("Error handling plugin command: {}", e);
385 }
386 }
387
388 self.flush_pending_grammars();
390 }
391
392 let lsp_waiting = !self.pending_completion_requests.is_empty()
394 || self.pending_goto_definition_request.is_some();
395
396 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
403 let hide_cursor = self.menu_state.active_menu.is_some()
404 || self.key_context == KeyContext::FileExplorer
405 || self.terminal_mode
406 || settings_visible
407 || self.keybinding_editor.is_some();
408
409 let hovered_tab = match &self.mouse_state.hover_target {
411 Some(HoverTarget::TabName(target, split_id)) => Some((*target, *split_id, false)),
412 Some(HoverTarget::TabCloseButton(target, split_id)) => Some((*target, *split_id, true)),
413 _ => None,
414 };
415
416 let hovered_close_split = match &self.mouse_state.hover_target {
418 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
419 _ => None,
420 };
421
422 let hovered_maximize_split = match &self.mouse_state.hover_target {
424 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
425 _ => None,
426 };
427
428 let is_maximized = self.split_manager.is_maximized();
429
430 let _content_span = tracing::info_span!("render_content").entered();
431 let (
432 split_areas,
433 tab_layouts,
434 close_split_areas,
435 maximize_split_areas,
436 view_line_mappings,
437 horizontal_scrollbar_areas,
438 grouped_separator_areas,
439 ) = SplitRenderer::render_content(
440 frame,
441 editor_content_area,
442 &self.split_manager,
443 &mut self.buffers,
444 &self.buffer_metadata,
445 &mut self.event_logs,
446 &mut self.composite_buffers,
447 &mut self.composite_view_states,
448 &self.theme,
449 self.ansi_background.as_ref(),
450 self.background_fade,
451 lsp_waiting,
452 self.config.editor.large_file_threshold_bytes,
453 self.config.editor.line_wrap,
454 self.config.editor.estimated_line_length,
455 self.config.editor.highlight_context_bytes,
456 Some(&mut self.split_view_states),
457 &self.grouped_subtrees,
458 hide_cursor,
459 hovered_tab,
460 hovered_close_split,
461 hovered_maximize_split,
462 is_maximized,
463 self.config.editor.relative_line_numbers,
464 self.tab_bar_visible,
465 self.config.editor.use_terminal_bg,
466 self.session_mode || !self.software_cursor_only,
467 self.software_cursor_only,
468 self.config.editor.show_vertical_scrollbar,
469 self.config.editor.show_horizontal_scrollbar,
470 self.config.editor.diagnostics_inline_text,
471 self.config.editor.show_tilde,
472 &mut self.cached_layout.cell_theme_map,
473 size.width,
474 );
475
476 drop(_content_span);
477
478 if self.plugin_manager.is_active() {
482 for (split_id, view_state) in &self.split_view_states {
483 let current = (
484 view_state.viewport.top_byte,
485 view_state.viewport.width,
486 view_state.viewport.height,
487 );
488 let (changed, previous) = match self.previous_viewports.get(split_id) {
493 Some(previous) => (*previous != current, Some(*previous)),
494 None => (false, None), };
496 tracing::trace!(
497 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
498 split_id,
499 current,
500 previous,
501 changed
502 );
503 if changed {
504 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
505 let top_line = self.buffers.get(&buffer_id).and_then(|state| {
507 if state.buffer.line_count().is_some() {
508 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
509 } else {
510 None
511 }
512 });
513 tracing::debug!(
514 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
515 split_id,
516 buffer_id,
517 view_state.viewport.top_byte,
518 top_line
519 );
520 self.plugin_manager.run_hook(
521 "viewport_changed",
522 crate::services::plugins::hooks::HookArgs::ViewportChanged {
523 split_id: (*split_id).into(),
524 buffer_id,
525 top_byte: view_state.viewport.top_byte,
526 top_line,
527 width: view_state.viewport.width,
528 height: view_state.viewport.height,
529 },
530 );
531 }
532 }
533 }
534 }
535
536 self.previous_viewports.clear();
538 for (split_id, view_state) in &self.split_view_states {
539 self.previous_viewports.insert(
540 *split_id,
541 (
542 view_state.viewport.top_byte,
543 view_state.viewport.width,
544 view_state.viewport.height,
545 ),
546 );
547 }
548
549 self.render_terminal_splits(frame, &split_areas);
551
552 self.cached_layout.split_areas = split_areas;
553 self.cached_layout.horizontal_scrollbar_areas = horizontal_scrollbar_areas;
554 self.cached_layout.tab_layouts = tab_layouts;
555 self.cached_layout.close_split_areas = close_split_areas;
556 self.cached_layout.maximize_split_areas = maximize_split_areas;
557 self.cached_layout.view_line_mappings = view_line_mappings;
558 let mut separator_areas = self
559 .split_manager
560 .get_separators_with_ids(editor_content_area);
561 separator_areas.extend(grouped_separator_areas);
566 self.cached_layout.separator_areas = separator_areas;
567 self.cached_layout.editor_content_area = Some(editor_content_area);
568
569 self.render_hover_highlights(frame);
571
572 self.cached_layout.suggestions_area = None;
574 self.file_browser_layout = None;
575
576 let display_name = self
578 .buffer_metadata
579 .get(&self.active_buffer())
580 .map(|m| m.display_name.clone())
581 .unwrap_or_else(|| "[No Name]".to_string());
582 let status_message = self.status_message.clone();
583 let plugin_status_message = self.plugin_status_message.clone();
584 let prompt = self.prompt.clone();
585 let current_language = self
590 .buffers
591 .get(&self.active_buffer())
592 .map(|s| s.language.clone())
593 .unwrap_or_default();
594 let lsp_status = {
595 use crate::services::async_bridge::LspServerStatus;
596 let has_running_servers = self.lsp_server_statuses.iter().any(|((lang, _), status)| {
597 lang == ¤t_language && !matches!(status, LspServerStatus::Shutdown)
598 });
599 if has_running_servers {
600 "LSP".to_string()
601 } else {
602 let has_auto_start_config = self
607 .config
608 .lsp
609 .get(¤t_language)
610 .map(|cfg| {
611 cfg.as_slice()
612 .iter()
613 .any(|c| c.enabled && c.auto_start && !c.command.is_empty())
614 })
615 .unwrap_or(false);
616 if has_auto_start_config {
617 "LSP off".to_string()
618 } else {
619 String::new()
620 }
621 }
622 };
623 let theme = self.theme.clone();
624 let keybindings_cloned = self.keybindings.read().unwrap().clone(); let chord_state_cloned = self.chord_state.clone(); let update_available = self.latest_version().map(|v| v.to_string());
629
630 if self.status_bar_visible && !has_suggestions && !has_file_browser {
632 let (warning_level, general_warning_count) =
635 if self.config.warnings.show_status_indicator {
636 let lsp_level = {
637 use crate::services::async_bridge::LspServerStatus;
638 let mut level = WarningLevel::None;
639 for ((lang, _), status) in &self.lsp_server_statuses {
640 if lang == ¤t_language {
641 match status {
642 LspServerStatus::Error => {
643 level = WarningLevel::Error;
644 break;
645 }
646 LspServerStatus::Starting | LspServerStatus::Initializing => {
647 if level != WarningLevel::Error {
648 level = WarningLevel::Warning;
649 }
650 }
651 _ => {}
652 }
653 }
654 }
655 level
656 };
657 (lsp_level, self.get_general_warning_count())
658 } else {
659 (WarningLevel::None, 0)
660 };
661
662 use crate::view::ui::status_bar::StatusBarHover;
664 let status_bar_hover = match &self.mouse_state.hover_target {
665 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
666 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
667 Some(HoverTarget::StatusBarLineEndingIndicator) => {
668 StatusBarHover::LineEndingIndicator
669 }
670 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
671 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
672 _ => StatusBarHover::None,
673 };
674
675 let remote_connection = self.remote_connection_info().map(|conn| {
677 if self.filesystem.is_remote_connected() {
678 conn.to_string()
679 } else {
680 format!("{} (Disconnected)", conn)
681 }
682 });
683
684 let session_name = self.session_name().map(|s| s.to_string());
686
687 let active_split = self.effective_active_split();
688 let active_buf = self.active_buffer();
689 let default_cursors = crate::model::cursor::Cursors::new();
690 let status_cursors = self
691 .split_view_states
692 .get(&active_split)
693 .map(|vs| &vs.cursors)
694 .unwrap_or(&default_cursors);
695 let is_read_only = self
696 .buffer_metadata
697 .get(&active_buf)
698 .map(|m| m.read_only)
699 .unwrap_or(false);
700 let mut status_ctx = crate::view::ui::status_bar::StatusBarContext {
701 state: self.buffers.get_mut(&active_buf).unwrap(),
702 cursors: status_cursors,
703 status_message: &status_message,
704 plugin_status_message: &plugin_status_message,
705 lsp_status: &lsp_status,
706 theme: &theme,
707 display_name: &display_name,
708 keybindings: &keybindings_cloned,
709 chord_state: &chord_state_cloned,
710 update_available: update_available.as_deref(),
711 warning_level,
712 general_warning_count,
713 hover: status_bar_hover,
714 remote_connection: remote_connection.as_deref(),
715 session_name: session_name.as_deref(),
716 read_only: is_read_only,
717 };
718 let status_bar_layout = StatusBarRenderer::render_status_bar(
719 frame,
720 main_chunks[status_bar_idx],
721 &mut status_ctx,
722 &self.config.editor.status_bar,
723 );
724
725 let status_bar_area = main_chunks[status_bar_idx];
727 self.cached_layout.status_bar_area =
728 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
729 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
730 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
731 self.cached_layout.status_bar_line_ending_area =
732 status_bar_layout.line_ending_indicator;
733 self.cached_layout.status_bar_encoding_area = status_bar_layout.encoding_indicator;
734 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
735 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
736 }
737
738 if show_search_options {
740 let confirm_each = self.prompt.as_ref().and_then(|p| {
742 if matches!(
743 p.prompt_type,
744 PromptType::ReplaceSearch
745 | PromptType::Replace { .. }
746 | PromptType::QueryReplaceSearch
747 | PromptType::QueryReplace { .. }
748 ) {
749 Some(self.search_confirm_each)
750 } else {
751 None
752 }
753 });
754
755 use crate::view::ui::status_bar::SearchOptionsHover;
757 let search_options_hover = match &self.mouse_state.hover_target {
758 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
759 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
760 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
761 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
762 _ => SearchOptionsHover::None,
763 };
764
765 let search_options_layout = StatusBarRenderer::render_search_options(
766 frame,
767 main_chunks[search_options_idx],
768 self.search_case_sensitive,
769 self.search_whole_word,
770 self.search_use_regex,
771 confirm_each,
772 &theme,
773 &keybindings_cloned,
774 search_options_hover,
775 );
776 self.cached_layout.search_options_layout = Some(search_options_layout);
777 } else {
778 self.cached_layout.search_options_layout = None;
779 }
780
781 if let Some(prompt) = &prompt {
783 if matches!(
785 prompt.prompt_type,
786 crate::view::prompt::PromptType::OpenFile
787 | crate::view::prompt::PromptType::SwitchProject
788 ) {
789 if let Some(file_open_state) = &self.file_open_state {
790 StatusBarRenderer::render_file_open_prompt(
791 frame,
792 main_chunks[prompt_line_idx],
793 prompt,
794 file_open_state,
795 &theme,
796 );
797 } else {
798 StatusBarRenderer::render_prompt(
799 frame,
800 main_chunks[prompt_line_idx],
801 prompt,
802 &theme,
803 );
804 }
805 } else {
806 StatusBarRenderer::render_prompt(
807 frame,
808 main_chunks[prompt_line_idx],
809 prompt,
810 &theme,
811 );
812 }
813 }
814
815 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
818
819 let theme_clone = self.theme.clone();
822 let hover_target = self.mouse_state.hover_target.clone();
823
824 self.cached_layout.popup_areas.clear();
826
827 let popup_info: Vec<_> = {
829 let active_split = self.split_manager.active_split();
831 let viewport = self
832 .split_view_states
833 .get(&active_split)
834 .map(|vs| vs.viewport.clone());
835
836 let content_rect = self
841 .cached_layout
842 .split_areas
843 .iter()
844 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
845 .map(|(_, _, rect, _, _, _)| *rect);
846
847 let primary_cursor = self
848 .split_view_states
849 .get(&active_split)
850 .map(|vs| *vs.cursors.primary());
851 let state = self.active_state_mut();
852 if state.popups.is_visible() {
853 let primary_cursor =
855 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
856
857 let gutter_width = viewport
859 .as_ref()
860 .map(|vp| vp.gutter_width(&state.buffer) as u16)
861 .unwrap_or(0);
862
863 let cursor_screen_pos = viewport
864 .as_ref()
865 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
866 .unwrap_or((0, 0));
867
868 let word_start_screen_pos = {
872 use crate::primitives::word_navigation::find_completion_word_start;
873 let word_start =
874 find_completion_word_start(&state.buffer, primary_cursor.position);
875 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
876 viewport
877 .as_ref()
878 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
879 .unwrap_or((0, 0))
880 };
881
882 let (base_x, base_y) = content_rect
887 .map(|r| (r.x + gutter_width, r.y))
888 .unwrap_or((gutter_width, 1));
889
890 let cursor_screen_pos =
891 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
892 let word_start_screen_pos = (
893 word_start_screen_pos.0 + base_x,
894 word_start_screen_pos.1 + base_y,
895 );
896
897 state
899 .popups
900 .all()
901 .iter()
902 .enumerate()
903 .map(|(popup_idx, popup)| {
904 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
906 (word_start_screen_pos.0, cursor_screen_pos.1)
907 } else {
908 cursor_screen_pos
909 };
910 let popup_area = popup.calculate_area(size, Some(popup_pos));
911
912 let desc_height = popup.description_height();
915 let inner_area = if popup.bordered {
916 ratatui::layout::Rect {
917 x: popup_area.x + 1,
918 y: popup_area.y + 1 + desc_height,
919 width: popup_area.width.saturating_sub(2),
920 height: popup_area.height.saturating_sub(2 + desc_height),
921 }
922 } else {
923 ratatui::layout::Rect {
924 x: popup_area.x,
925 y: popup_area.y + desc_height,
926 width: popup_area.width,
927 height: popup_area.height.saturating_sub(desc_height),
928 }
929 };
930
931 let num_items = match &popup.content {
932 crate::view::popup::PopupContent::List { items, .. } => items.len(),
933 _ => 0,
934 };
935
936 let total_lines = popup.item_count();
938 let visible_lines = inner_area.height as usize;
939 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
940 {
941 Some(ratatui::layout::Rect {
942 x: inner_area.x + inner_area.width - 1,
943 y: inner_area.y,
944 width: 1,
945 height: inner_area.height,
946 })
947 } else {
948 None
949 };
950
951 (
952 popup_idx,
953 popup_area,
954 inner_area,
955 popup.scroll_offset,
956 num_items,
957 scrollbar_rect,
958 total_lines,
959 )
960 })
961 .collect()
962 } else {
963 Vec::new()
964 }
965 };
966
967 self.cached_layout.popup_areas = popup_info.clone();
969
970 let state = self.active_state_mut();
972 if state.popups.is_visible() {
973 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
974 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
975 popup.render_with_hover(
976 frame,
977 *popup_area,
978 &theme_clone,
979 hover_target.as_ref(),
980 );
981 }
982 }
983 }
984
985 self.update_menu_context();
988
989 let settings_visible = self
992 .settings_state
993 .as_ref()
994 .map(|s| s.visible)
995 .unwrap_or(false);
996 if settings_visible {
997 crate::view::dimming::apply_dimming(frame, size);
999 }
1000 if let Some(ref mut settings_state) = self.settings_state {
1001 if settings_state.visible {
1002 settings_state.update_focus_states();
1003 let settings_layout = crate::view::settings::render_settings(
1004 frame,
1005 size,
1006 settings_state,
1007 &self.theme,
1008 );
1009 self.cached_layout.settings_layout = Some(settings_layout);
1010 }
1011 }
1012
1013 if let Some(ref wizard) = self.calibration_wizard {
1015 crate::view::dimming::apply_dimming(frame, size);
1017 crate::view::calibration_wizard::render_calibration_wizard(
1018 frame,
1019 size,
1020 wizard,
1021 &self.theme,
1022 );
1023 }
1024
1025 if let Some(ref mut kb_editor) = self.keybinding_editor {
1027 crate::view::dimming::apply_dimming(frame, size);
1028 crate::view::keybinding_editor::render_keybinding_editor(
1029 frame,
1030 size,
1031 kb_editor,
1032 &self.theme,
1033 );
1034 }
1035
1036 if let Some(ref debug) = self.event_debug {
1038 crate::view::dimming::apply_dimming(frame, size);
1040 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
1041 }
1042
1043 if self.menu_bar_visible {
1044 let keybindings = self.keybindings.read().unwrap();
1045 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
1046 frame,
1047 menu_bar_area,
1048 &self.menus,
1049 &self.menu_state,
1050 &keybindings,
1051 &self.theme,
1052 self.mouse_state.hover_target.as_ref(),
1053 self.config.editor.menu_bar_mnemonics,
1054 ));
1055 } else {
1056 self.cached_layout.menu_layout = None;
1057 }
1058
1059 if let Some(ref menu) = self.tab_context_menu {
1061 self.render_tab_context_menu(frame, menu);
1062 }
1063
1064 self.record_non_editor_theme_regions();
1066
1067 self.render_theme_info_popup(frame);
1069
1070 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
1072 if drag_state.is_dragging() {
1073 self.render_tab_drop_zone(frame, drag_state);
1074 }
1075 }
1076
1077 if self.gpm_active {
1083 if let Some((col, row)) = self.mouse_cursor_position {
1084 use ratatui::style::Modifier;
1085
1086 if col < size.width && row < size.height {
1088 let buf = frame.buffer_mut();
1090 if let Some(cell) = buf.cell_mut((col, row)) {
1091 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1092 }
1093 }
1094 }
1095 }
1096
1097 if self.keyboard_capture && self.terminal_mode {
1100 let active_split = self.split_manager.active_split();
1102 let active_split_area = self
1103 .cached_layout
1104 .split_areas
1105 .iter()
1106 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1107 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1108
1109 if let Some(terminal_area) = active_split_area {
1110 self.apply_keyboard_capture_dimming(frame, terminal_area);
1111 }
1112 }
1113
1114 crate::view::color_support::convert_buffer_colors(
1116 frame.buffer_mut(),
1117 self.color_capability,
1118 );
1119 }
1120
1121 fn render_quick_open_hints(
1123 frame: &mut Frame,
1124 area: ratatui::layout::Rect,
1125 theme: &crate::view::theme::Theme,
1126 ) {
1127 use ratatui::style::{Modifier, Style};
1128 use ratatui::text::{Line, Span};
1129 use ratatui::widgets::Paragraph;
1130 use rust_i18n::t;
1131
1132 let hints_style = Style::default()
1133 .fg(theme.line_number_fg)
1134 .bg(theme.suggestion_selected_bg)
1135 .add_modifier(Modifier::DIM);
1136 let hints_text = t!("quick_open.mode_hints");
1137 let left_margin = 2;
1139 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1140 let mut spans = Vec::new();
1141 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1142 spans.push(Span::styled(hints_text.to_string(), hints_style));
1143 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1144 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1145
1146 let paragraph = Paragraph::new(Line::from(spans));
1147 frame.render_widget(paragraph, area);
1148 }
1149
1150 fn apply_keyboard_capture_dimming(
1153 &self,
1154 frame: &mut Frame,
1155 terminal_area: ratatui::layout::Rect,
1156 ) {
1157 let size = frame.area();
1158 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1159 }
1160
1161 fn render_prompt_popups(
1164 &mut self,
1165 frame: &mut Frame,
1166 prompt_area: ratatui::layout::Rect,
1167 width: u16,
1168 ) {
1169 let Some(prompt) = &self.prompt else { return };
1170
1171 if matches!(
1172 prompt.prompt_type,
1173 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1174 ) {
1175 let Some(file_open_state) = &self.file_open_state else {
1176 return;
1177 };
1178 let max_height = prompt_area.y.saturating_sub(1).min(20);
1179 let popup_area = ratatui::layout::Rect {
1180 x: 0,
1181 y: prompt_area.y.saturating_sub(max_height),
1182 width,
1183 height: max_height,
1184 };
1185 let keybindings = self.keybindings.read().unwrap();
1186 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1187 frame,
1188 popup_area,
1189 file_open_state,
1190 &self.theme,
1191 &self.mouse_state.hover_target,
1192 Some(&*keybindings),
1193 );
1194 return;
1195 }
1196
1197 if prompt.suggestions.is_empty() {
1198 return;
1199 }
1200
1201 let suggestion_count = prompt.suggestions.len().min(10);
1202 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1203 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1204 let height = suggestion_count as u16 + 2 + hints_height;
1205
1206 let suggestions_area = ratatui::layout::Rect {
1207 x: 0,
1208 y: prompt_area.y.saturating_sub(height),
1209 width,
1210 height: height - hints_height,
1211 };
1212
1213 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1214
1215 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
1216 frame,
1217 suggestions_area,
1218 prompt,
1219 &self.theme,
1220 self.mouse_state.hover_target.as_ref(),
1221 );
1222
1223 if is_quick_open {
1224 let hints_area = ratatui::layout::Rect {
1225 x: 0,
1226 y: prompt_area.y.saturating_sub(hints_height),
1227 width,
1228 height: hints_height,
1229 };
1230 frame.render_widget(ratatui::widgets::Clear, hints_area);
1231 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1232 }
1233 }
1234
1235 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
1237 use ratatui::style::Style;
1238 use ratatui::text::Span;
1239 use ratatui::widgets::Paragraph;
1240
1241 match &self.mouse_state.hover_target {
1242 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
1243 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
1245 if sid == split_id && dir == direction {
1246 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1247 match dir {
1248 SplitDirection::Horizontal => {
1249 let line_text = "─".repeat(*length as usize);
1250 let paragraph =
1251 Paragraph::new(Span::styled(line_text, hover_style));
1252 frame.render_widget(
1253 paragraph,
1254 ratatui::layout::Rect::new(*x, *y, *length, 1),
1255 );
1256 }
1257 SplitDirection::Vertical => {
1258 for offset in 0..*length {
1259 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1260 frame.render_widget(
1261 paragraph,
1262 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
1263 );
1264 }
1265 }
1266 }
1267 }
1268 }
1269 }
1270 Some(HoverTarget::ScrollbarThumb(split_id)) => {
1271 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1273 &self.cached_layout.split_areas
1274 {
1275 if sid == split_id {
1276 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
1277 for row_offset in *thumb_start..*thumb_end {
1278 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
1279 frame.render_widget(
1280 paragraph,
1281 ratatui::layout::Rect::new(
1282 scrollbar_rect.x,
1283 scrollbar_rect.y + row_offset as u16,
1284 1,
1285 1,
1286 ),
1287 );
1288 }
1289 }
1290 }
1291 }
1292 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
1293 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
1295 &self.cached_layout.split_areas
1296 {
1297 if sid == split_id {
1298 let track_hover_style =
1299 Style::default().bg(self.theme.scrollbar_track_hover_fg);
1300 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
1301 frame.render_widget(
1302 paragraph,
1303 ratatui::layout::Rect::new(
1304 scrollbar_rect.x,
1305 scrollbar_rect.y + hovered_row,
1306 1,
1307 1,
1308 ),
1309 );
1310 }
1311 }
1312 }
1313 Some(HoverTarget::FileExplorerBorder) => {
1314 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1316 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1317 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1318 for row_offset in 0..explorer_area.height {
1319 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1320 frame.render_widget(
1321 paragraph,
1322 ratatui::layout::Rect::new(
1323 border_x,
1324 explorer_area.y + row_offset,
1325 1,
1326 1,
1327 ),
1328 );
1329 }
1330 }
1331 }
1332 _ => {}
1334 }
1335 }
1336
1337 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
1339 use ratatui::style::Style;
1340 use ratatui::text::{Line, Span};
1341 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1342
1343 let items = super::types::TabContextMenuItem::all();
1344 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
1349 let screen_height = frame.area().height;
1350
1351 let menu_x = if menu.position.0 + menu_width > screen_width {
1352 screen_width.saturating_sub(menu_width)
1353 } else {
1354 menu.position.0
1355 };
1356
1357 let menu_y = if menu.position.1 + menu_height > screen_height {
1358 screen_height.saturating_sub(menu_height)
1359 } else {
1360 menu.position.1
1361 };
1362
1363 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1364
1365 frame.render_widget(Clear, area);
1367
1368 let mut lines = Vec::new();
1370 for (idx, item) in items.iter().enumerate() {
1371 let is_highlighted = idx == menu.highlighted;
1372
1373 let style = if is_highlighted {
1374 Style::default()
1375 .fg(self.theme.menu_highlight_fg)
1376 .bg(self.theme.menu_highlight_bg)
1377 } else {
1378 Style::default()
1379 .fg(self.theme.menu_dropdown_fg)
1380 .bg(self.theme.menu_dropdown_bg)
1381 };
1382
1383 let label = item.label();
1385 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1387
1388 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1389 }
1390
1391 let block = Block::default()
1392 .borders(Borders::ALL)
1393 .border_style(Style::default().fg(self.theme.menu_border_fg))
1394 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1395
1396 let paragraph = Paragraph::new(lines).block(block);
1397 frame.render_widget(paragraph, area);
1398 }
1399
1400 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
1402 use ratatui::style::Modifier;
1403
1404 let Some(ref drop_zone) = drag_state.drop_zone else {
1405 return;
1406 };
1407
1408 let split_id = drop_zone.split_id();
1409
1410 let split_area = self
1412 .cached_layout
1413 .split_areas
1414 .iter()
1415 .find(|(sid, _, _, _, _, _)| *sid == split_id)
1416 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1417
1418 let Some(content_rect) = split_area else {
1419 return;
1420 };
1421
1422 use super::types::TabDropZone;
1424
1425 let highlight_area = match drop_zone {
1426 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
1427 content_rect
1430 }
1431 TabDropZone::SplitLeft(_) => {
1432 let width = (content_rect.width / 2).max(3);
1434 ratatui::layout::Rect::new(
1435 content_rect.x,
1436 content_rect.y,
1437 width,
1438 content_rect.height,
1439 )
1440 }
1441 TabDropZone::SplitRight(_) => {
1442 let width = (content_rect.width / 2).max(3);
1444 let x = content_rect.x + content_rect.width - width;
1445 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
1446 }
1447 TabDropZone::SplitTop(_) => {
1448 let height = (content_rect.height / 2).max(2);
1450 ratatui::layout::Rect::new(
1451 content_rect.x,
1452 content_rect.y,
1453 content_rect.width,
1454 height,
1455 )
1456 }
1457 TabDropZone::SplitBottom(_) => {
1458 let height = (content_rect.height / 2).max(2);
1460 let y = content_rect.y + content_rect.height - height;
1461 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
1462 }
1463 };
1464
1465 let buf = frame.buffer_mut();
1468 let drop_zone_bg = self.theme.tab_drop_zone_bg;
1469 let drop_zone_border = self.theme.tab_drop_zone_border;
1470
1471 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1473 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1474 if let Some(cell) = buf.cell_mut((x, y)) {
1475 cell.set_bg(drop_zone_bg);
1478
1479 let is_border = x == highlight_area.x
1481 || x == highlight_area.x + highlight_area.width - 1
1482 || y == highlight_area.y
1483 || y == highlight_area.y + highlight_area.height - 1;
1484
1485 if is_border {
1486 cell.set_fg(drop_zone_border);
1487 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
1488 }
1489 }
1490 }
1491 }
1492
1493 match drop_zone {
1495 TabDropZone::SplitLeft(_) => {
1496 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1498 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
1499 cell.set_symbol("▌");
1500 cell.set_fg(drop_zone_border);
1501 }
1502 }
1503 }
1504 TabDropZone::SplitRight(_) => {
1505 let x = highlight_area.x + highlight_area.width - 1;
1507 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1508 if let Some(cell) = buf.cell_mut((x, y)) {
1509 cell.set_symbol("▐");
1510 cell.set_fg(drop_zone_border);
1511 }
1512 }
1513 }
1514 TabDropZone::SplitTop(_) => {
1515 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1517 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
1518 cell.set_symbol("▀");
1519 cell.set_fg(drop_zone_border);
1520 }
1521 }
1522 }
1523 TabDropZone::SplitBottom(_) => {
1524 let y = highlight_area.y + highlight_area.height - 1;
1526 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1527 if let Some(cell) = buf.cell_mut((x, y)) {
1528 cell.set_symbol("▄");
1529 cell.set_fg(drop_zone_border);
1530 }
1531 }
1532 }
1533 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
1534 }
1536 }
1537 }
1538
1539 pub fn add_overlay(
1543 &mut self,
1544 namespace: Option<crate::view::overlay::OverlayNamespace>,
1545 range: Range<usize>,
1546 face: crate::model::event::OverlayFace,
1547 priority: i32,
1548 message: Option<String>,
1549 ) -> crate::view::overlay::OverlayHandle {
1550 let event = Event::AddOverlay {
1551 namespace,
1552 range,
1553 face,
1554 priority,
1555 message,
1556 extend_to_line_end: false,
1557 url: None,
1558 };
1559 self.apply_event_to_active_buffer(&event);
1560 let state = self.active_state();
1562 state
1563 .overlays
1564 .all()
1565 .last()
1566 .map(|o| o.handle.clone())
1567 .unwrap_or_default()
1568 }
1569
1570 pub fn remove_overlay(&mut self, handle: crate::view::overlay::OverlayHandle) {
1572 let event = Event::RemoveOverlay { handle };
1573 self.apply_event_to_active_buffer(&event);
1574 }
1575
1576 pub fn remove_overlays_in_range(&mut self, range: Range<usize>) {
1578 let event = Event::RemoveOverlaysInRange { range };
1579 self.active_event_log_mut().append(event.clone());
1580 self.apply_event_to_active_buffer(&event);
1581 }
1582
1583 pub fn clear_overlays(&mut self) {
1585 let event = Event::ClearOverlays;
1586 self.active_event_log_mut().append(event.clone());
1587 self.apply_event_to_active_buffer(&event);
1588 }
1589
1590 pub fn show_popup(&mut self, popup: crate::model::event::PopupData) {
1594 let event = Event::ShowPopup { popup };
1595 self.active_event_log_mut().append(event.clone());
1596 self.apply_event_to_active_buffer(&event);
1597 }
1598
1599 pub fn hide_popup(&mut self) {
1601 let event = Event::HidePopup;
1602 self.active_event_log_mut().append(event.clone());
1603 self.apply_event_to_active_buffer(&event);
1604
1605 let active = self.active_buffer();
1607 if let Some((wait_id, true)) = self.wait_tracking.remove(&active) {
1608 self.completed_waits.push(wait_id);
1609 }
1610
1611 if let Some(handle) = self.hover_symbol_overlay.take() {
1613 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1614 self.apply_event_to_active_buffer(&remove_overlay_event);
1615 }
1616 self.hover_symbol_range = None;
1617 }
1618
1619 pub(super) fn dismiss_transient_popups(&mut self) {
1622 let is_transient_popup = self
1623 .active_state()
1624 .popups
1625 .top()
1626 .is_some_and(|p| p.transient);
1627
1628 if is_transient_popup {
1629 self.hide_popup();
1630 tracing::trace!("Dismissed transient popup");
1631 }
1632 }
1633
1634 pub(super) fn scroll_popup(&mut self, delta: i32) {
1637 if let Some(popup) = self.active_state_mut().popups.top_mut() {
1638 popup.scroll_by(delta);
1639 tracing::debug!(
1640 "Scrolled popup by {}, new offset: {}",
1641 delta,
1642 popup.scroll_offset
1643 );
1644 }
1645 }
1646
1647 pub(super) fn on_editor_focus_lost(&mut self) {
1655 self.active_state_mut().on_focus_lost();
1657
1658 self.mouse_state.lsp_hover_state = None;
1660 self.mouse_state.lsp_hover_request_sent = false;
1661 self.pending_hover_request = None;
1662
1663 if let Some(handle) = self.hover_symbol_overlay.take() {
1665 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1666 self.apply_event_to_active_buffer(&remove_overlay_event);
1667 }
1668 self.hover_symbol_range = None;
1669 }
1670
1671 pub fn clear_popups(&mut self) {
1673 let event = Event::ClearPopups;
1674 self.active_event_log_mut().append(event.clone());
1675 self.apply_event_to_active_buffer(&event);
1676 }
1677
1678 pub fn show_lsp_confirmation_popup(&mut self, language: &str) {
1685 use crate::model::event::{
1686 PopupContentData, PopupData, PopupKindHint, PopupListItemData, PopupPositionData,
1687 };
1688
1689 self.pending_lsp_confirmation = Some(language.to_string());
1691
1692 let server_info = if let Some(lsp) = &self.lsp {
1694 if let Some(config) = lsp.get_config(language) {
1695 if !config.command.is_empty() {
1696 format!("{} ({})", language, config.command)
1697 } else {
1698 language.to_string()
1699 }
1700 } else {
1701 language.to_string()
1702 }
1703 } else {
1704 language.to_string()
1705 };
1706
1707 let popup = PopupData {
1708 kind: PopupKindHint::List,
1709 title: Some(format!("Start LSP Server: {}?", server_info)),
1710 description: None,
1711 transient: false,
1712 content: PopupContentData::List {
1713 items: vec![
1714 PopupListItemData {
1715 text: "Allow this time".to_string(),
1716 detail: Some("Start the LSP server for this session".to_string()),
1717 icon: None,
1718 data: Some("allow_once".to_string()),
1719 },
1720 PopupListItemData {
1721 text: "Always allow".to_string(),
1722 detail: Some("Always start this LSP server automatically".to_string()),
1723 icon: None,
1724 data: Some("allow_always".to_string()),
1725 },
1726 PopupListItemData {
1727 text: "Don't start".to_string(),
1728 detail: Some("Cancel LSP server startup".to_string()),
1729 icon: None,
1730 data: Some("deny".to_string()),
1731 },
1732 ],
1733 selected: 0,
1734 },
1735 position: PopupPositionData::Centered,
1736 width: 50,
1737 max_height: 8,
1738 bordered: true,
1739 };
1740
1741 self.show_popup(popup);
1742 }
1743
1744 pub fn handle_lsp_confirmation_response(&mut self, action: &str) -> bool {
1752 let Some(language) = self.pending_lsp_confirmation.take() else {
1753 return false;
1754 };
1755
1756 let file_path = self
1758 .buffer_metadata
1759 .get(&self.active_buffer())
1760 .and_then(|meta| meta.file_path().cloned());
1761
1762 match action {
1763 "allow_once" => {
1764 if let Some(lsp) = &mut self.lsp {
1766 lsp.allow_language(&language);
1768 if lsp.force_spawn(&language, file_path.as_deref()).is_some() {
1770 tracing::info!("LSP server for {} started (allowed once)", language);
1771 self.set_status_message(
1772 t!("lsp.server_started", language = language).to_string(),
1773 );
1774 } else {
1775 self.set_status_message(
1776 t!("lsp.failed_to_start", language = language).to_string(),
1777 );
1778 }
1779 }
1780 self.notify_lsp_current_file_opened(&language);
1782 }
1783 "allow_always" => {
1784 if let Some(lsp) = &mut self.lsp {
1786 lsp.allow_language(&language);
1787 if lsp.force_spawn(&language, file_path.as_deref()).is_some() {
1789 tracing::info!("LSP server for {} started (always allowed)", language);
1790 self.set_status_message(
1791 t!("lsp.server_started_auto", language = language).to_string(),
1792 );
1793 } else {
1794 self.set_status_message(
1795 t!("lsp.failed_to_start", language = language).to_string(),
1796 );
1797 }
1798 }
1799 self.notify_lsp_current_file_opened(&language);
1801 }
1802 _ => {
1803 tracing::info!("LSP server for {} startup declined by user", language);
1805 self.set_status_message(
1806 t!("lsp.startup_cancelled", language = language).to_string(),
1807 );
1808 }
1809 }
1810
1811 true
1812 }
1813
1814 fn notify_lsp_current_file_opened(&mut self, language: &str) {
1819 let metadata = match self.buffer_metadata.get(&self.active_buffer()) {
1821 Some(m) => m,
1822 None => {
1823 tracing::debug!(
1824 "notify_lsp_current_file_opened: no metadata for buffer {:?}",
1825 self.active_buffer()
1826 );
1827 return;
1828 }
1829 };
1830
1831 if !metadata.lsp_enabled {
1832 tracing::debug!("notify_lsp_current_file_opened: LSP disabled for this buffer");
1833 return;
1834 }
1835
1836 let file_path = metadata.file_path().cloned();
1838
1839 let uri = match metadata.file_uri() {
1841 Some(u) => u.clone(),
1842 None => {
1843 tracing::debug!(
1844 "notify_lsp_current_file_opened: no URI for buffer (not a file or URI creation failed)"
1845 );
1846 return;
1847 }
1848 };
1849
1850 let active_buffer = self.active_buffer();
1852
1853 let file_language = match self.buffers.get(&active_buffer).map(|s| s.language.clone()) {
1855 Some(l) => l,
1856 None => {
1857 tracing::debug!("notify_lsp_current_file_opened: no buffer state");
1858 return;
1859 }
1860 };
1861
1862 if file_language != language {
1864 tracing::debug!(
1865 "notify_lsp_current_file_opened: file language {} doesn't match server {}",
1866 file_language,
1867 language
1868 );
1869 return;
1870 }
1871 let (text, line_count) = if let Some(state) = self.buffers.get(&active_buffer) {
1872 let text = match state.buffer.to_string() {
1873 Some(t) => t,
1874 None => {
1875 tracing::debug!("notify_lsp_current_file_opened: buffer not fully loaded");
1876 return;
1877 }
1878 };
1879 let line_count = state.buffer.line_count().unwrap_or(1000);
1880 (text, line_count)
1881 } else {
1882 tracing::debug!("notify_lsp_current_file_opened: no buffer state");
1883 return;
1884 };
1885
1886 if let Some(lsp) = &mut self.lsp {
1888 if lsp.force_spawn(language, file_path.as_deref()).is_some() {
1890 tracing::info!("Sending didOpen to LSP servers for: {}", uri.as_str());
1891 let mut any_opened = false;
1892 for sh in lsp.get_handles_mut(language) {
1893 if let Err(e) =
1894 sh.handle
1895 .did_open(uri.clone(), text.clone(), file_language.clone())
1896 {
1897 tracing::warn!("Failed to send didOpen to '{}': {}", sh.name, e);
1898 } else {
1899 any_opened = true;
1900 }
1901 }
1902
1903 if any_opened {
1904 tracing::info!("Successfully sent didOpen to LSP after confirmation");
1905
1906 if let Some(handle) = lsp.get_handle_mut(language) {
1908 let previous_result_id =
1909 self.diagnostic_result_ids.get(uri.as_str()).cloned();
1910 let request_id = self.next_lsp_request_id;
1911 self.next_lsp_request_id += 1;
1912
1913 if let Err(e) =
1914 handle.document_diagnostic(request_id, uri.clone(), previous_result_id)
1915 {
1916 tracing::debug!(
1917 "Failed to request pull diagnostics (server may not support): {}",
1918 e
1919 );
1920 }
1921
1922 if self.config.editor.enable_inlay_hints {
1924 let request_id = self.next_lsp_request_id;
1925 self.next_lsp_request_id += 1;
1926 self.pending_inlay_hints_request = Some(request_id);
1927
1928 let last_line = line_count.saturating_sub(1) as u32;
1929 let last_char = 10000u32;
1930
1931 if let Err(e) = handle.inlay_hints(
1932 request_id,
1933 uri.clone(),
1934 0,
1935 0,
1936 last_line,
1937 last_char,
1938 ) {
1939 tracing::debug!(
1940 "Failed to request inlay hints (server may not support): {}",
1941 e
1942 );
1943 self.pending_inlay_hints_request = None;
1944 }
1945 }
1946 }
1947 }
1948 }
1949 }
1950 }
1951
1952 pub fn has_pending_lsp_confirmation(&self) -> bool {
1954 self.pending_lsp_confirmation.is_some()
1955 }
1956
1957 pub fn popup_select_next(&mut self) {
1959 let event = Event::PopupSelectNext;
1960 self.active_event_log_mut().append(event.clone());
1961 self.apply_event_to_active_buffer(&event);
1962 }
1963
1964 pub fn popup_select_prev(&mut self) {
1966 let event = Event::PopupSelectPrev;
1967 self.active_event_log_mut().append(event.clone());
1968 self.apply_event_to_active_buffer(&event);
1969 }
1970
1971 pub fn popup_page_down(&mut self) {
1973 let event = Event::PopupPageDown;
1974 self.active_event_log_mut().append(event.clone());
1975 self.apply_event_to_active_buffer(&event);
1976 }
1977
1978 pub fn popup_page_up(&mut self) {
1980 let event = Event::PopupPageUp;
1981 self.active_event_log_mut().append(event.clone());
1982 self.apply_event_to_active_buffer(&event);
1983 }
1984
1985 pub(super) fn collect_lsp_changes(&self, event: &Event) -> Vec<TextDocumentContentChangeEvent> {
1991 match event {
1992 Event::Insert { position, text, .. } => {
1993 tracing::trace!(
1994 "collect_lsp_changes: processing Insert at position {}",
1995 position
1996 );
1997 let (line, character) = self
1999 .active_state()
2000 .buffer
2001 .position_to_lsp_position(*position);
2002 let lsp_pos = Position::new(line as u32, character as u32);
2003 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2004 vec![TextDocumentContentChangeEvent {
2005 range: Some(lsp_range),
2006 range_length: None,
2007 text: text.clone(),
2008 }]
2009 }
2010 Event::Delete { range, .. } => {
2011 tracing::trace!("collect_lsp_changes: processing Delete range {:?}", range);
2012 let (start_line, start_char) = self
2014 .active_state()
2015 .buffer
2016 .position_to_lsp_position(range.start);
2017 let (end_line, end_char) = self
2018 .active_state()
2019 .buffer
2020 .position_to_lsp_position(range.end);
2021 let lsp_range = LspRange::new(
2022 Position::new(start_line as u32, start_char as u32),
2023 Position::new(end_line as u32, end_char as u32),
2024 );
2025 vec![TextDocumentContentChangeEvent {
2026 range: Some(lsp_range),
2027 range_length: None,
2028 text: String::new(),
2029 }]
2030 }
2031 Event::Batch { events, .. } => {
2032 tracing::trace!(
2035 "collect_lsp_changes: processing Batch with {} events",
2036 events.len()
2037 );
2038 let mut all_changes = Vec::new();
2039 for sub_event in events {
2040 all_changes.extend(self.collect_lsp_changes(sub_event));
2041 }
2042 all_changes
2043 }
2044 _ => Vec::new(), }
2046 }
2047
2048 pub(super) fn calculate_event_line_info(&self, event: &Event) -> super::types::EventLineInfo {
2070 match event {
2071 Event::Insert { position, text, .. } => {
2072 let start_line = self.active_state().buffer.get_line_number(*position);
2074
2075 let lines_added = text.matches('\n').count();
2077 let end_line = start_line + lines_added;
2078
2079 super::types::EventLineInfo {
2080 start_line,
2081 end_line,
2082 line_delta: lines_added as i32,
2083 }
2084 }
2085 Event::Delete {
2086 range,
2087 deleted_text,
2088 ..
2089 } => {
2090 let start_line = self.active_state().buffer.get_line_number(range.start);
2092 let end_line = self.active_state().buffer.get_line_number(range.end);
2093
2094 let lines_removed = deleted_text.matches('\n').count();
2096
2097 super::types::EventLineInfo {
2098 start_line,
2099 end_line,
2100 line_delta: -(lines_removed as i32),
2101 }
2102 }
2103 Event::Batch { events, .. } => {
2104 let mut min_line = usize::MAX;
2107 let mut max_line = 0usize;
2108 let mut total_delta = 0i32;
2109
2110 for sub_event in events {
2111 let info = self.calculate_event_line_info(sub_event);
2112 min_line = min_line.min(info.start_line);
2113 max_line = max_line.max(info.end_line);
2114 total_delta += info.line_delta;
2115 }
2116
2117 if min_line == usize::MAX {
2118 min_line = 0;
2119 }
2120
2121 super::types::EventLineInfo {
2122 start_line: min_line,
2123 end_line: max_line,
2124 line_delta: total_delta,
2125 }
2126 }
2127 _ => super::types::EventLineInfo::default(),
2128 }
2129 }
2130
2131 pub(super) fn notify_lsp_save(&mut self) {
2133 let buffer_id = self.active_buffer();
2134 self.notify_lsp_save_buffer(buffer_id);
2135 }
2136
2137 pub(super) fn notify_lsp_save_buffer(&mut self, buffer_id: BufferId) {
2139 let metadata = match self.buffer_metadata.get(&buffer_id) {
2141 Some(m) => m,
2142 None => {
2143 tracing::debug!(
2144 "notify_lsp_save_buffer: no metadata for buffer {:?}",
2145 buffer_id
2146 );
2147 return;
2148 }
2149 };
2150
2151 if !metadata.lsp_enabled {
2152 tracing::debug!(
2153 "notify_lsp_save_buffer: LSP disabled for buffer {:?}",
2154 buffer_id
2155 );
2156 return;
2157 }
2158
2159 let file_path = metadata.file_path().cloned();
2161
2162 let uri = match metadata.file_uri() {
2164 Some(u) => u.clone(),
2165 None => {
2166 tracing::debug!("notify_lsp_save_buffer: no URI for buffer {:?}", buffer_id);
2167 return;
2168 }
2169 };
2170
2171 let language = match self
2174 .buffers
2175 .get(&self.active_buffer())
2176 .map(|s| s.language.clone())
2177 {
2178 Some(l) => l,
2179 None => {
2180 tracing::debug!("notify_lsp_save: no buffer state");
2181 return;
2182 }
2183 };
2184
2185 let full_text = match self.active_state().buffer.to_string() {
2187 Some(t) => t,
2188 None => {
2189 tracing::debug!("notify_lsp_save: buffer not fully loaded");
2190 return;
2191 }
2192 };
2193 tracing::debug!(
2194 "notify_lsp_save: sending didSave to {} (text length: {} bytes)",
2195 uri.as_str(),
2196 full_text.len()
2197 );
2198
2199 if let Some(lsp) = &mut self.lsp {
2201 use crate::services::lsp::manager::LspSpawnResult;
2202 if lsp.try_spawn(&language, file_path.as_deref()) != LspSpawnResult::Spawned {
2203 tracing::debug!(
2204 "notify_lsp_save: LSP not running for {} (auto_start disabled)",
2205 language
2206 );
2207 return;
2208 }
2209 let mut any_sent = false;
2211 for sh in lsp.get_handles_mut(&language) {
2212 if let Err(e) = sh.handle.did_save(uri.clone(), Some(full_text.clone())) {
2213 tracing::warn!("Failed to send didSave to '{}': {}", sh.name, e);
2214 } else {
2215 any_sent = true;
2216 }
2217 }
2218 if any_sent {
2219 tracing::info!("Successfully sent didSave to LSP");
2220 } else {
2221 tracing::warn!("notify_lsp_save: no LSP handles for {}", language);
2222 }
2223 } else {
2224 tracing::debug!("notify_lsp_save: no LSP manager available");
2225 }
2226 }
2227
2228 pub fn action_to_events(&mut self, action: Action) -> Option<Vec<Event>> {
2231 let auto_indent = self.config.editor.auto_indent;
2232 let estimated_line_length = self.config.editor.estimated_line_length;
2233
2234 let active_split = self.effective_active_split();
2241 let viewport_height = self
2242 .split_view_states
2243 .get(&active_split)
2244 .map(|vs| vs.viewport.height)
2245 .unwrap_or(24);
2246
2247 if let Some(events) =
2251 self.handle_visual_line_movement(&action, active_split, estimated_line_length)
2252 {
2253 return Some(events);
2254 }
2255
2256 let buffer_id = self.active_buffer();
2257 let state = self.buffers.get_mut(&buffer_id).unwrap();
2258
2259 let tab_size = state.buffer_settings.tab_size;
2261 let auto_close = state.buffer_settings.auto_close;
2262 let auto_surround = state.buffer_settings.auto_surround;
2263
2264 let cursors = &mut self
2265 .split_view_states
2266 .get_mut(&active_split)
2267 .unwrap()
2268 .cursors;
2269 convert_action_to_events(
2270 state,
2271 cursors,
2272 action,
2273 tab_size,
2274 auto_indent,
2275 auto_close,
2276 auto_surround,
2277 estimated_line_length,
2278 viewport_height,
2279 )
2280 }
2281
2282 fn handle_visual_line_movement(
2285 &mut self,
2286 action: &Action,
2287 split_id: LeafId,
2288 _estimated_line_length: usize,
2289 ) -> Option<Vec<Event>> {
2290 enum VisualAction {
2292 UpDown { direction: i8, is_select: bool },
2293 LineEnd { is_select: bool },
2294 LineStart { is_select: bool },
2295 }
2296
2297 let visual_action = match action {
2300 Action::MoveUp => VisualAction::UpDown {
2301 direction: -1,
2302 is_select: false,
2303 },
2304 Action::MoveDown => VisualAction::UpDown {
2305 direction: 1,
2306 is_select: false,
2307 },
2308 Action::SelectUp => VisualAction::UpDown {
2309 direction: -1,
2310 is_select: true,
2311 },
2312 Action::SelectDown => VisualAction::UpDown {
2313 direction: 1,
2314 is_select: true,
2315 },
2316 Action::MoveLineEnd if self.config.editor.line_wrap => {
2320 VisualAction::LineEnd { is_select: false }
2321 }
2322 Action::SelectLineEnd if self.config.editor.line_wrap => {
2323 VisualAction::LineEnd { is_select: true }
2324 }
2325 Action::MoveLineStart if self.config.editor.line_wrap => {
2326 VisualAction::LineStart { is_select: false }
2327 }
2328 Action::SelectLineStart if self.config.editor.line_wrap => {
2329 VisualAction::LineStart { is_select: true }
2330 }
2331 _ => return None, };
2333
2334 let cursor_data: Vec<_> = {
2339 let active_split = self.effective_active_split();
2340 let active_buffer = self.active_buffer();
2341 let cursors = &self.split_view_states.get(&active_split).unwrap().cursors;
2342 let state = self.buffers.get(&active_buffer).unwrap();
2343 cursors
2344 .iter()
2345 .map(|(cursor_id, cursor)| {
2346 let at_line_ending = if cursor.position < state.buffer.len() {
2350 let bytes = state
2351 .buffer
2352 .slice_bytes(cursor.position..cursor.position + 1);
2353 bytes.first() == Some(&b'\n') || bytes.first() == Some(&b'\r')
2354 } else {
2355 true };
2357 let at_line_start = if cursor.position == 0 {
2358 true
2359 } else {
2360 let prev = state
2361 .buffer
2362 .slice_bytes(cursor.position - 1..cursor.position);
2363 prev.first() == Some(&b'\n')
2364 };
2365 (
2366 cursor_id,
2367 cursor.position,
2368 cursor.anchor,
2369 cursor.sticky_column,
2370 cursor.deselect_on_move,
2371 at_line_ending,
2372 at_line_start,
2373 )
2374 })
2375 .collect()
2376 };
2377
2378 let mut events = Vec::new();
2379
2380 for (
2381 cursor_id,
2382 position,
2383 anchor,
2384 sticky_column,
2385 deselect_on_move,
2386 at_line_ending,
2387 at_line_start,
2388 ) in cursor_data
2389 {
2390 let (new_pos, new_sticky) = match &visual_action {
2391 VisualAction::UpDown { direction, .. } => {
2392 let current_visual_col = self
2394 .cached_layout
2395 .byte_to_visual_column(split_id, position)?;
2396
2397 let goal_visual_col = if sticky_column > 0 {
2398 sticky_column
2399 } else {
2400 current_visual_col
2401 };
2402
2403 match self.cached_layout.move_visual_line(
2404 split_id,
2405 position,
2406 goal_visual_col,
2407 *direction,
2408 ) {
2409 Some(result) => result,
2410 None => continue, }
2412 }
2413 VisualAction::LineEnd { .. } => {
2414 let allow_advance = !at_line_ending;
2416 match self
2417 .cached_layout
2418 .visual_line_end(split_id, position, allow_advance)
2419 {
2420 Some(end_pos) => (end_pos, 0),
2421 None => return None,
2422 }
2423 }
2424 VisualAction::LineStart { .. } => {
2425 let allow_advance = !at_line_start;
2427 match self
2428 .cached_layout
2429 .visual_line_start(split_id, position, allow_advance)
2430 {
2431 Some(start_pos) => (start_pos, 0),
2432 None => return None,
2433 }
2434 }
2435 };
2436
2437 let is_select = match &visual_action {
2438 VisualAction::UpDown { is_select, .. } => *is_select,
2439 VisualAction::LineEnd { is_select } => *is_select,
2440 VisualAction::LineStart { is_select } => *is_select,
2441 };
2442
2443 let new_anchor = if is_select {
2444 Some(anchor.unwrap_or(position))
2445 } else if deselect_on_move {
2446 None
2447 } else {
2448 anchor
2449 };
2450
2451 events.push(Event::MoveCursor {
2452 cursor_id,
2453 old_position: position,
2454 new_position: new_pos,
2455 old_anchor: anchor,
2456 new_anchor,
2457 old_sticky_column: sticky_column,
2458 new_sticky_column: new_sticky,
2459 });
2460 }
2461
2462 if events.is_empty() {
2463 None } else {
2465 Some(events)
2466 }
2467 }
2468
2469 pub(super) fn clear_search_highlights(&mut self) {
2473 self.clear_search_overlays();
2474 self.search_state = None;
2476 }
2477
2478 pub(super) fn clear_search_overlays(&mut self) {
2481 let ns = self.search_namespace.clone();
2482 let state = self.active_state_mut();
2483 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2484 }
2485
2486 pub(super) fn update_search_highlights(&mut self, query: &str) {
2489 if query.is_empty() {
2491 self.clear_search_highlights();
2492 return;
2493 }
2494
2495 let search_bg = self.theme.search_match_bg;
2497 let search_fg = self.theme.search_match_fg;
2498 let case_sensitive = self.search_case_sensitive;
2499 let whole_word = self.search_whole_word;
2500 let use_regex = self.search_use_regex;
2501 let ns = self.search_namespace.clone();
2502
2503 let regex_pattern = if use_regex {
2505 if whole_word {
2506 format!(r"\b{}\b", query)
2507 } else {
2508 query.to_string()
2509 }
2510 } else {
2511 let escaped = regex::escape(query);
2512 if whole_word {
2513 format!(r"\b{}\b", escaped)
2514 } else {
2515 escaped
2516 }
2517 };
2518
2519 let regex = regex::RegexBuilder::new(®ex_pattern)
2521 .case_insensitive(!case_sensitive)
2522 .build();
2523
2524 let regex = match regex {
2525 Ok(r) => r,
2526 Err(_) => {
2527 self.clear_search_highlights();
2529 return;
2530 }
2531 };
2532
2533 let active_split = self.split_manager.active_split();
2535 let (top_byte, visible_height) = self
2536 .split_view_states
2537 .get(&active_split)
2538 .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2539 .unwrap_or((0, 20));
2540
2541 let state = self.active_state_mut();
2542
2543 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2545
2546 let visible_start = top_byte;
2548 let mut visible_end = top_byte;
2549
2550 {
2551 let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2552 for _ in 0..visible_height {
2553 if let Some((line_start, line_content)) = line_iter.next_line() {
2554 visible_end = line_start + line_content.len();
2555 } else {
2556 break;
2557 }
2558 }
2559 }
2560
2561 visible_end = visible_end.min(state.buffer.len());
2563
2564 let visible_text = state.get_text_range(visible_start, visible_end);
2566
2567 for mat in regex.find_iter(&visible_text) {
2569 let absolute_pos = visible_start + mat.start();
2570 let match_len = mat.end() - mat.start();
2571
2572 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2574 let overlay = crate::view::overlay::Overlay::with_namespace(
2575 &mut state.marker_list,
2576 absolute_pos..(absolute_pos + match_len),
2577 crate::view::overlay::OverlayFace::Style {
2578 style: search_style,
2579 },
2580 ns.clone(),
2581 )
2582 .with_priority_value(10); state.overlays.add(overlay);
2585 }
2586 }
2587
2588 fn build_search_regex(&self, query: &str) -> Result<regex::Regex, String> {
2590 let regex_pattern = if self.search_use_regex {
2591 if self.search_whole_word {
2592 format!(r"\b{}\b", query)
2593 } else {
2594 query.to_string()
2595 }
2596 } else {
2597 let escaped = regex::escape(query);
2598 if self.search_whole_word {
2599 format!(r"\b{}\b", escaped)
2600 } else {
2601 escaped
2602 }
2603 };
2604
2605 regex::RegexBuilder::new(®ex_pattern)
2606 .case_insensitive(!self.search_case_sensitive)
2607 .build()
2608 .map_err(|e| e.to_string())
2609 }
2610
2611 fn move_cursor_to_match(&mut self, position: usize) {
2623 let active_split = self.split_manager.active_split();
2624 let active_buffer = self.active_buffer();
2625 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2626 view_state.cursors.primary_mut().position = position;
2627 view_state.cursors.primary_mut().anchor = None;
2628 let state = self.buffers.get_mut(&active_buffer).unwrap();
2629 if let Some(pos) = state.buffer.offset_to_position(position) {
2630 state.primary_cursor_line_number =
2631 crate::model::buffer::LineNumber::Absolute(pos.line);
2632 }
2633 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
2634 }
2635 }
2636
2637 pub(super) fn perform_search(&mut self, query: &str) {
2638 if query.is_empty() {
2639 self.search_state = None;
2640 self.set_status_message(t!("search.cancelled").to_string());
2641 return;
2642 }
2643
2644 let search_range = self.pending_search_range.take();
2645
2646 let regex = match self.build_search_regex(query) {
2648 Ok(r) => r,
2649 Err(e) => {
2650 self.search_state = None;
2651 self.set_status_message(t!("error.invalid_regex", error = e).to_string());
2652 return;
2653 }
2654 };
2655
2656 let is_large = self.active_state().buffer.is_large_file();
2658 if is_large && search_range.is_none() {
2659 self.start_search_scan(query, regex);
2660 return;
2661 }
2662
2663 let buffer_content = {
2666 let state = self.active_state_mut();
2667 let total_bytes = state.buffer.len();
2668 match state.buffer.get_text_range_mut(0, total_bytes) {
2669 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2670 Err(e) => {
2671 tracing::warn!("Failed to load buffer for search: {}", e);
2672 self.set_status_message(t!("error.buffer_not_loaded").to_string());
2673 return;
2674 }
2675 }
2676 };
2677
2678 let (search_start, search_end) = if let Some(ref range) = search_range {
2679 (range.start, range.end)
2680 } else {
2681 (0, buffer_content.len())
2682 };
2683
2684 let search_slice = &buffer_content[search_start..search_end];
2685
2686 let mut match_ranges: Vec<(usize, usize)> = Vec::new();
2688 let mut capped = false;
2689 for m in regex.find_iter(search_slice) {
2690 if match_ranges.len() >= SearchState::MAX_MATCHES {
2691 capped = true;
2692 break;
2693 }
2694 match_ranges.push((search_start + m.start(), m.end() - m.start()));
2695 }
2696
2697 if match_ranges.is_empty() {
2698 self.search_state = None;
2699 let msg = if search_range.is_some() {
2700 format!("No matches found for '{}' in selection", query)
2701 } else {
2702 format!("No matches found for '{}'", query)
2703 };
2704 self.set_status_message(msg);
2705 return;
2706 }
2707
2708 self.finalize_search(query, match_ranges, capped, search_range);
2709 }
2710
2711 pub(super) fn finalize_search(
2720 &mut self,
2721 query: &str,
2722 match_ranges: Vec<(usize, usize)>,
2723 capped: bool,
2724 search_range: Option<std::ops::Range<usize>>,
2725 ) {
2726 let matches: Vec<usize> = match_ranges.iter().map(|(pos, _)| *pos).collect();
2727 let match_lengths: Vec<usize> = match_ranges.iter().map(|(_, len)| *len).collect();
2728 let is_large = self.active_state().buffer.is_large_file();
2729
2730 let cursor_pos = self.active_cursors().primary().position;
2732 let current_match_index = matches
2733 .iter()
2734 .position(|&pos| pos >= cursor_pos)
2735 .unwrap_or(0);
2736
2737 let match_pos = matches[current_match_index];
2739 self.move_cursor_to_match(match_pos);
2740
2741 let num_matches = matches.len();
2742
2743 self.search_state = Some(SearchState {
2744 query: query.to_string(),
2745 matches,
2746 match_lengths: match_lengths.clone(),
2747 current_match_index: Some(current_match_index),
2748 wrap_search: search_range.is_none(),
2749 search_range,
2750 capped,
2751 });
2752
2753 if is_large {
2754 self.refresh_search_overlays();
2756 } else {
2757 let search_bg = self.theme.search_match_bg;
2759 let search_fg = self.theme.search_match_fg;
2760 let ns = self.search_namespace.clone();
2761 let state = self.active_state_mut();
2762 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2763
2764 for (&pos, &len) in match_ranges
2765 .iter()
2766 .map(|(p, _)| p)
2767 .zip(match_lengths.iter())
2768 {
2769 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2770 let overlay = crate::view::overlay::Overlay::with_namespace(
2771 &mut state.marker_list,
2772 pos..(pos + len),
2773 crate::view::overlay::OverlayFace::Style {
2774 style: search_style,
2775 },
2776 ns.clone(),
2777 )
2778 .with_priority_value(10);
2779 state.overlays.add(overlay);
2780 }
2781 }
2782
2783 let cap_suffix = if capped { "+" } else { "" };
2784 let msg = if self.search_state.as_ref().unwrap().search_range.is_some() {
2785 format!(
2786 "Found {}{} match{} for '{}' in selection",
2787 num_matches,
2788 cap_suffix,
2789 if num_matches == 1 { "" } else { "es" },
2790 query
2791 )
2792 } else {
2793 format!(
2794 "Found {}{} match{} for '{}'",
2795 num_matches,
2796 cap_suffix,
2797 if num_matches == 1 { "" } else { "es" },
2798 query
2799 )
2800 };
2801 self.set_status_message(msg);
2802 }
2803
2804 pub(super) fn refresh_search_overlays(&mut self) {
2808 let _span = tracing::info_span!("refresh_search_overlays").entered();
2809 let search_bg = self.theme.search_match_bg;
2810 let search_fg = self.theme.search_match_fg;
2811 let ns = self.search_namespace.clone();
2812
2813 let active_split = self.split_manager.active_split();
2815 let (top_byte, visible_height) = self
2816 .split_view_states
2817 .get(&active_split)
2818 .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2819 .unwrap_or((0, 20));
2820
2821 self.search_overlay_top_byte = Some(top_byte);
2824
2825 let state = self.active_state_mut();
2826
2827 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2829
2830 let visible_start = top_byte;
2832 let mut visible_end = top_byte;
2833 {
2834 let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2835 for _ in 0..visible_height {
2836 if let Some((line_start, line_content)) = line_iter.next_line() {
2837 visible_end = line_start + line_content.len();
2838 } else {
2839 break;
2840 }
2841 }
2842 }
2843 visible_end = visible_end.min(state.buffer.len());
2844
2845 let _ = state;
2849
2850 let viewport_matches: Vec<(usize, usize)> = match &self.search_state {
2851 Some(ss) => {
2852 let start_idx = ss.matches.partition_point(|&pos| pos < visible_start);
2853 ss.matches[start_idx..]
2854 .iter()
2855 .zip(ss.match_lengths[start_idx..].iter())
2856 .take_while(|(&pos, _)| pos <= visible_end)
2857 .map(|(&pos, &len)| (pos, len))
2858 .collect()
2859 }
2860 None => return,
2861 };
2862
2863 let state = self.active_state_mut();
2864
2865 for (pos, len) in &viewport_matches {
2866 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2867 let overlay = crate::view::overlay::Overlay::with_namespace(
2868 &mut state.marker_list,
2869 *pos..(*pos + *len),
2870 crate::view::overlay::OverlayFace::Style {
2871 style: search_style,
2872 },
2873 ns.clone(),
2874 )
2875 .with_priority_value(10);
2876 state.overlays.add(overlay);
2877 }
2878 }
2879
2880 pub(super) fn check_search_overlay_refresh(&mut self) -> bool {
2888 if self.search_state.is_none() {
2889 return false;
2890 }
2891 if !self.active_state().buffer.is_large_file() {
2893 return false;
2894 }
2895 let active_split = self.split_manager.active_split();
2896 let current_top = self
2897 .split_view_states
2898 .get(&active_split)
2899 .map(|vs| vs.viewport.top_byte);
2900 if current_top != self.search_overlay_top_byte {
2901 self.refresh_search_overlays();
2902 true
2903 } else {
2904 false
2905 }
2906 }
2907
2908 fn start_search_scan(&mut self, query: &str, regex: regex::Regex) {
2913 let buffer_id = self.active_buffer();
2914 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2915 let leaves = state.buffer.piece_tree_leaves();
2916 let bytes_regex = regex::bytes::RegexBuilder::new(regex.as_str())
2918 .case_insensitive(!self.search_case_sensitive)
2919 .build()
2920 .expect("regex already validated");
2921 let scan = state.buffer.search_scan_init(
2922 bytes_regex,
2923 super::SearchState::MAX_MATCHES,
2924 query.len(),
2925 );
2926 self.search_scan_state = Some(super::SearchScanState {
2927 buffer_id,
2928 leaves,
2929 scan,
2930 query: query.to_string(),
2931 search_range: None,
2932 case_sensitive: self.search_case_sensitive,
2933 whole_word: self.search_whole_word,
2934 use_regex: self.search_use_regex,
2935 });
2936 self.set_status_message(t!("goto.scanning_progress", percent = 0).to_string());
2937 }
2938 }
2939
2940 fn get_search_match_positions(&self) -> Vec<usize> {
2944 let ns = &self.search_namespace;
2945 let state = self.active_state();
2946
2947 let mut positions: Vec<usize> = state
2948 .overlays
2949 .all()
2950 .iter()
2951 .filter(|o| o.namespace.as_ref() == Some(ns))
2952 .filter_map(|o| state.marker_list.get_position(o.start_marker))
2953 .collect();
2954
2955 positions.sort_unstable();
2956 positions.dedup();
2957 positions
2958 }
2959
2960 pub(super) fn find_next(&mut self) {
2967 self.find_match_in_direction(SearchDirection::Forward);
2968 }
2969
2970 pub(super) fn find_previous(&mut self) {
2976 self.find_match_in_direction(SearchDirection::Backward);
2977 }
2978
2979 fn find_match_in_direction(&mut self, direction: SearchDirection) {
2984 let overlay_positions = self.get_search_match_positions();
2985 let is_large = self.active_state().buffer.is_large_file();
2986
2987 if let Some(ref mut search_state) = self.search_state {
2988 let use_overlays =
2991 !is_large && !overlay_positions.is_empty() && search_state.search_range.is_none();
2992 let match_positions: &[usize] = if use_overlays {
2993 &overlay_positions
2994 } else {
2995 &search_state.matches
2996 };
2997
2998 if match_positions.is_empty() {
2999 return;
3000 }
3001
3002 let cursor_pos = {
3003 let active_split = self.split_manager.active_split();
3004 self.split_view_states
3005 .get(&active_split)
3006 .map(|vs| vs.cursors.primary().position)
3007 .unwrap_or(0)
3008 };
3009
3010 let target_index = match direction {
3011 SearchDirection::Forward => {
3012 let idx = match match_positions.binary_search(&(cursor_pos + 1)) {
3014 Ok(i) | Err(i) => {
3015 if i < match_positions.len() {
3016 Some(i)
3017 } else {
3018 None
3019 }
3020 }
3021 };
3022 match idx {
3023 Some(i) => i,
3024 None if search_state.wrap_search => 0,
3025 None => {
3026 self.set_status_message(t!("search.no_matches").to_string());
3027 return;
3028 }
3029 }
3030 }
3031 SearchDirection::Backward => {
3032 let idx = if cursor_pos == 0 {
3034 None
3035 } else {
3036 match match_positions.binary_search(&(cursor_pos - 1)) {
3037 Ok(i) => Some(i),
3038 Err(i) => {
3039 if i > 0 {
3040 Some(i - 1)
3041 } else {
3042 None
3043 }
3044 }
3045 }
3046 };
3047 match idx {
3048 Some(i) => i,
3049 None if search_state.wrap_search => match_positions.len() - 1,
3050 None => {
3051 self.set_status_message(t!("search.no_matches").to_string());
3052 return;
3053 }
3054 }
3055 }
3056 };
3057
3058 search_state.current_match_index = Some(target_index);
3059 let match_pos = match_positions[target_index];
3060 let matches_len = match_positions.len();
3061
3062 self.move_cursor_to_match(match_pos);
3063
3064 self.set_status_message(
3065 t!(
3066 "search.match_of",
3067 current = target_index + 1,
3068 total = matches_len
3069 )
3070 .to_string(),
3071 );
3072
3073 if is_large {
3074 self.refresh_search_overlays();
3075 }
3076 } else {
3077 let find_key = self
3078 .get_keybinding_for_action("find")
3079 .unwrap_or_else(|| "Ctrl+F".to_string());
3080 self.set_status_message(t!("search.no_active", find_key = find_key).to_string());
3081 }
3082 }
3083
3084 pub(super) fn find_selection_next(&mut self) {
3091 if let Some(ref search_state) = self.search_state {
3094 let cursor_pos = self.active_cursors().primary().position;
3095 if search_state.matches.binary_search(&cursor_pos).is_ok() {
3096 self.find_next();
3097 return;
3098 }
3099 }
3101 self.search_state = None;
3102
3103 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
3105
3106 match search_text {
3107 Some(text) if !text.is_empty() => {
3108 let cursor_before = self.active_cursors().primary().position;
3110
3111 self.perform_search(&text);
3113
3114 if let Some(ref search_state) = self.search_state {
3116 let cursor_after = self.active_cursors().primary().position;
3117
3118 let started_at_match = selection_start
3122 .map(|start| search_state.matches.binary_search(&start).is_ok())
3123 .unwrap_or(false);
3124
3125 let landed_at_start = selection_start
3126 .map(|start| cursor_after == start)
3127 .unwrap_or(false);
3128
3129 if ((started_at_match && landed_at_start) || cursor_before == cursor_after)
3133 && search_state.matches.len() > 1
3134 {
3135 self.find_next();
3136 }
3137 }
3138 }
3139 _ => {
3140 self.set_status_message(t!("search.no_text").to_string());
3141 }
3142 }
3143 }
3144
3145 pub(super) fn find_selection_previous(&mut self) {
3151 if let Some(ref search_state) = self.search_state {
3154 let cursor_pos = self.active_cursors().primary().position;
3155 if search_state.matches.binary_search(&cursor_pos).is_ok() {
3156 self.find_previous();
3157 return;
3158 }
3159 }
3161 self.search_state = None;
3162
3163 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
3165
3166 match search_text {
3167 Some(text) if !text.is_empty() => {
3168 let cursor_before = self.active_cursors().primary().position;
3170
3171 self.perform_search(&text);
3173
3174 if let Some(ref search_state) = self.search_state {
3176 let cursor_after = self.active_cursors().primary().position;
3177
3178 let started_at_match = selection_start
3180 .map(|start| search_state.matches.binary_search(&start).is_ok())
3181 .unwrap_or(false);
3182
3183 let landed_at_start = selection_start
3184 .map(|start| cursor_after == start)
3185 .unwrap_or(false);
3186
3187 if started_at_match && landed_at_start {
3192 self.find_previous();
3194 } else if cursor_before != cursor_after {
3195 self.find_previous();
3198 } else {
3199 self.find_previous();
3201 }
3202 }
3203 }
3204 _ => {
3205 self.set_status_message(t!("search.no_text").to_string());
3206 }
3207 }
3208 }
3209
3210 fn get_selection_or_word_for_search_with_pos(&mut self) -> (Option<String>, Option<usize>) {
3213 use crate::primitives::word_navigation::{find_word_end, find_word_start};
3214
3215 let (selection_range, cursor_pos) = {
3217 let primary = self.active_cursors().primary();
3218 (primary.selection_range(), primary.position)
3219 };
3220
3221 if let Some(range) = selection_range {
3223 let state = self.active_state_mut();
3224 let text = state.get_text_range(range.start, range.end);
3225 if !text.is_empty() {
3226 return (Some(text), Some(range.start));
3227 }
3228 }
3229
3230 let (word_start, word_end) = {
3232 let state = self.active_state();
3233 let word_start = find_word_start(&state.buffer, cursor_pos);
3234 let word_end = find_word_end(&state.buffer, cursor_pos);
3235 (word_start, word_end)
3236 };
3237
3238 if word_start < word_end {
3239 let state = self.active_state_mut();
3240 (
3241 Some(state.get_text_range(word_start, word_end)),
3242 Some(word_start),
3243 )
3244 } else {
3245 (None, None)
3246 }
3247 }
3248
3249 fn build_replace_regex(&self, search: &str) -> Option<regex::bytes::Regex> {
3253 super::regex_replace::build_regex(
3254 search,
3255 self.search_use_regex,
3256 self.search_whole_word,
3257 self.search_case_sensitive,
3258 )
3259 }
3260
3261 fn get_regex_match_len(&mut self, regex: ®ex::bytes::Regex, pos: usize) -> Option<usize> {
3263 let state = self.active_state_mut();
3264 let remaining = state.buffer.len().saturating_sub(pos);
3265 if remaining == 0 {
3266 return None;
3267 }
3268 let bytes = state.buffer.get_text_range_mut(pos, remaining).ok()?;
3269 regex.find(&bytes).map(|m| m.len())
3270 }
3271
3272 fn expand_regex_replacement(
3275 &mut self,
3276 regex: ®ex::bytes::Regex,
3277 pos: usize,
3278 match_len: usize,
3279 replacement: &str,
3280 ) -> String {
3281 let state = self.active_state_mut();
3282 if let Ok(bytes) = state.buffer.get_text_range_mut(pos, match_len) {
3283 return super::regex_replace::expand_replacement(regex, &bytes, replacement);
3284 }
3285 replacement.to_string()
3286 }
3287
3288 pub(super) fn perform_replace(&mut self, search: &str, replacement: &str) {
3293 if search.is_empty() {
3294 self.set_status_message(t!("replace.empty_query").to_string());
3295 return;
3296 }
3297
3298 let compiled_regex = self.build_replace_regex(search);
3299
3300 let matches: Vec<(usize, usize, String)> = if let Some(ref regex) = compiled_regex {
3303 let buffer_bytes = {
3306 let state = self.active_state_mut();
3307 let total_bytes = state.buffer.len();
3308 match state.buffer.get_text_range_mut(0, total_bytes) {
3309 Ok(bytes) => bytes,
3310 Err(e) => {
3311 tracing::warn!("Failed to load buffer for replace: {}", e);
3312 self.set_status_message(t!("error.buffer_not_loaded").to_string());
3313 return;
3314 }
3315 }
3316 };
3317 super::regex_replace::collect_regex_matches(regex, &buffer_bytes, replacement)
3318 .into_iter()
3319 .map(|m| (m.offset, m.len, m.replacement))
3320 .collect()
3321 } else {
3322 let state = self.active_state();
3324 let buffer_len = state.buffer.len();
3325 let mut matches = Vec::new();
3326 let mut current_pos = 0;
3327
3328 while current_pos < buffer_len {
3329 if let Some(offset) = state.buffer.find_next_in_range(
3330 search,
3331 current_pos,
3332 Some(current_pos..buffer_len),
3333 ) {
3334 matches.push((offset, search.len(), replacement.to_string()));
3335 current_pos = offset + search.len();
3336 } else {
3337 break;
3338 }
3339 }
3340 matches
3341 };
3342
3343 let count = matches.len();
3344
3345 if count == 0 {
3346 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3347 return;
3348 }
3349
3350 let cursor_id = self.active_cursors().primary_id();
3352
3353 let mut events = Vec::with_capacity(count * 2);
3356 for (match_pos, match_len, expanded_replacement) in &matches {
3357 let deleted_text = self
3359 .active_state_mut()
3360 .get_text_range(*match_pos, match_pos + match_len);
3361 events.push(Event::Delete {
3363 range: *match_pos..match_pos + match_len,
3364 deleted_text,
3365 cursor_id,
3366 });
3367 events.push(Event::Insert {
3369 position: *match_pos,
3370 text: expanded_replacement.clone(),
3371 cursor_id,
3372 });
3373 }
3374
3375 let description = format!("Replace all '{}' with '{}'", search, replacement);
3377 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3378 self.active_event_log_mut().append(bulk_edit);
3379 }
3380
3381 self.search_state = None;
3383
3384 let ns = self.search_namespace.clone();
3386 let state = self.active_state_mut();
3387 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3388
3389 self.set_status_message(
3391 t!(
3392 "search.replaced",
3393 count = count,
3394 search = search,
3395 replace = replacement
3396 )
3397 .to_string(),
3398 );
3399 }
3400
3401 pub(super) fn start_interactive_replace(&mut self, search: &str, replacement: &str) {
3403 if search.is_empty() {
3404 self.set_status_message(t!("replace.query_empty").to_string());
3405 return;
3406 }
3407
3408 let compiled_regex = self.build_replace_regex(search);
3409
3410 let start_pos = self.active_cursors().primary().position;
3412 let (first_match_pos, first_match_len) = if let Some(ref regex) = compiled_regex {
3413 let state = self.active_state();
3414 let buffer_len = state.buffer.len();
3415 let found = state
3417 .buffer
3418 .find_next_regex_in_range(regex, start_pos, Some(start_pos..buffer_len))
3419 .or_else(|| {
3420 if start_pos > 0 {
3421 state
3422 .buffer
3423 .find_next_regex_in_range(regex, 0, Some(0..start_pos))
3424 } else {
3425 None
3426 }
3427 });
3428 let Some(pos) = found else {
3429 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3430 return;
3431 };
3432 let match_len = self.get_regex_match_len(regex, pos).unwrap_or(search.len());
3434 (pos, match_len)
3435 } else {
3436 let state = self.active_state();
3437 let Some(pos) = state.buffer.find_next(search, start_pos) else {
3438 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3439 return;
3440 };
3441 (pos, search.len())
3442 };
3443
3444 self.interactive_replace_state = Some(InteractiveReplaceState {
3446 search: search.to_string(),
3447 replacement: replacement.to_string(),
3448 current_match_pos: first_match_pos,
3449 current_match_len: first_match_len,
3450 start_pos: first_match_pos,
3451 has_wrapped: false,
3452 replacements_made: 0,
3453 regex: compiled_regex,
3454 });
3455
3456 self.move_cursor_to_match(first_match_pos);
3458
3459 self.prompt = Some(Prompt::new(
3461 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string(),
3462 PromptType::QueryReplaceConfirm,
3463 ));
3464 }
3465
3466 pub(super) fn handle_interactive_replace_key(&mut self, c: char) -> AnyhowResult<()> {
3468 let state = self.interactive_replace_state.clone();
3469 let Some(mut ir_state) = state else {
3470 return Ok(());
3471 };
3472
3473 match c {
3474 'y' | 'Y' => {
3475 self.replace_current_match(&ir_state)?;
3477 ir_state.replacements_made += 1;
3478
3479 let search_pos = ir_state.current_match_pos + ir_state.replacement.len();
3481 if let Some((next_match, match_len, wrapped)) =
3482 self.find_next_match_for_replace(&ir_state, search_pos)
3483 {
3484 ir_state.current_match_pos = next_match;
3485 ir_state.current_match_len = match_len;
3486 if wrapped {
3487 ir_state.has_wrapped = true;
3488 }
3489 self.interactive_replace_state = Some(ir_state.clone());
3490 self.move_to_current_match(&ir_state);
3491 } else {
3492 self.finish_interactive_replace(ir_state.replacements_made);
3493 }
3494 }
3495 'n' | 'N' => {
3496 let search_pos = ir_state.current_match_pos + ir_state.current_match_len;
3498 if let Some((next_match, match_len, wrapped)) =
3499 self.find_next_match_for_replace(&ir_state, search_pos)
3500 {
3501 ir_state.current_match_pos = next_match;
3502 ir_state.current_match_len = match_len;
3503 if wrapped {
3504 ir_state.has_wrapped = true;
3505 }
3506 self.interactive_replace_state = Some(ir_state.clone());
3507 self.move_to_current_match(&ir_state);
3508 } else {
3509 self.finish_interactive_replace(ir_state.replacements_made);
3510 }
3511 }
3512 'a' | 'A' | '!' => {
3513 let all_matches: Vec<(usize, usize)> = {
3522 let mut matches = Vec::new();
3523 let mut temp_state = ir_state.clone();
3524 temp_state.has_wrapped = false; matches.push((ir_state.current_match_pos, ir_state.current_match_len));
3528 let mut current_pos = ir_state.current_match_pos + ir_state.current_match_len;
3529
3530 while let Some((next_match, match_len, wrapped)) =
3532 self.find_next_match_for_replace(&temp_state, current_pos)
3533 {
3534 matches.push((next_match, match_len));
3535 current_pos = next_match + match_len;
3536 if wrapped {
3537 temp_state.has_wrapped = true;
3538 }
3539 }
3540 matches
3541 };
3542
3543 let total_count = all_matches.len();
3544
3545 if total_count > 0 {
3546 let cursor_id = self.active_cursors().primary_id();
3548
3549 let mut events = Vec::with_capacity(total_count * 2);
3551 for &(match_pos, match_len) in &all_matches {
3552 let deleted_text = self
3553 .active_state_mut()
3554 .get_text_range(match_pos, match_pos + match_len);
3555 let replacement_text = if let Some(ref regex) = ir_state.regex {
3557 self.expand_regex_replacement(
3558 regex,
3559 match_pos,
3560 match_len,
3561 &ir_state.replacement,
3562 )
3563 } else {
3564 ir_state.replacement.clone()
3565 };
3566 events.push(Event::Delete {
3567 range: match_pos..match_pos + match_len,
3568 deleted_text,
3569 cursor_id,
3570 });
3571 events.push(Event::Insert {
3572 position: match_pos,
3573 text: replacement_text,
3574 cursor_id,
3575 });
3576 }
3577
3578 let description = format!(
3580 "Replace all {} occurrences of '{}' with '{}'",
3581 total_count, ir_state.search, ir_state.replacement
3582 );
3583 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3584 self.active_event_log_mut().append(bulk_edit);
3585 }
3586
3587 ir_state.replacements_made += total_count;
3588 }
3589
3590 self.finish_interactive_replace(ir_state.replacements_made);
3591 }
3592 'c' | 'C' | 'q' | 'Q' | '\x1b' => {
3593 self.finish_interactive_replace(ir_state.replacements_made);
3595 }
3596 _ => {
3597 }
3599 }
3600
3601 Ok(())
3602 }
3603
3604 pub(super) fn find_next_match_for_replace(
3607 &mut self,
3608 ir_state: &InteractiveReplaceState,
3609 start_pos: usize,
3610 ) -> Option<(usize, usize, bool)> {
3611 if let Some(ref regex) = ir_state.regex {
3612 let regex = regex.clone();
3614 let state = self.active_state();
3615 let buffer_len = state.buffer.len();
3616
3617 if ir_state.has_wrapped {
3618 let search_range = Some(start_pos..ir_state.start_pos);
3619 if let Some(match_pos) =
3620 state
3621 .buffer
3622 .find_next_regex_in_range(®ex, start_pos, search_range)
3623 {
3624 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3625 return Some((match_pos, match_len, true));
3626 }
3627 None
3628 } else {
3629 let search_range = Some(start_pos..buffer_len);
3630 if let Some(match_pos) =
3631 state
3632 .buffer
3633 .find_next_regex_in_range(®ex, start_pos, search_range)
3634 {
3635 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3636 return Some((match_pos, match_len, false));
3637 }
3638
3639 let wrap_range = Some(0..ir_state.start_pos);
3641 let state = self.active_state();
3642 if let Some(match_pos) =
3643 state.buffer.find_next_regex_in_range(®ex, 0, wrap_range)
3644 {
3645 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3646 return Some((match_pos, match_len, true));
3647 }
3648
3649 None
3650 }
3651 } else {
3652 let search_len = ir_state.search.len();
3654 let state = self.active_state();
3655
3656 if ir_state.has_wrapped {
3657 let search_range = Some(start_pos..ir_state.start_pos);
3658 if let Some(match_pos) =
3659 state
3660 .buffer
3661 .find_next_in_range(&ir_state.search, start_pos, search_range)
3662 {
3663 return Some((match_pos, search_len, true));
3664 }
3665 None
3666 } else {
3667 let buffer_len = state.buffer.len();
3668 let search_range = Some(start_pos..buffer_len);
3669 if let Some(match_pos) =
3670 state
3671 .buffer
3672 .find_next_in_range(&ir_state.search, start_pos, search_range)
3673 {
3674 return Some((match_pos, search_len, false));
3675 }
3676
3677 let wrap_range = Some(0..ir_state.start_pos);
3678 if let Some(match_pos) =
3679 state
3680 .buffer
3681 .find_next_in_range(&ir_state.search, 0, wrap_range)
3682 {
3683 return Some((match_pos, search_len, true));
3684 }
3685
3686 None
3687 }
3688 }
3689 }
3690
3691 pub(super) fn replace_current_match(
3693 &mut self,
3694 ir_state: &InteractiveReplaceState,
3695 ) -> AnyhowResult<()> {
3696 let match_pos = ir_state.current_match_pos;
3697 let match_len = ir_state.current_match_len;
3698 let range = match_pos..(match_pos + match_len);
3699
3700 let replacement_text = if let Some(ref regex) = ir_state.regex {
3702 self.expand_regex_replacement(regex, match_pos, match_len, &ir_state.replacement)
3703 } else {
3704 ir_state.replacement.clone()
3705 };
3706
3707 let deleted_text = self
3709 .active_state_mut()
3710 .get_text_range(range.start, range.end);
3711
3712 let cursor_id = self.active_cursors().primary_id();
3714 let cursor = *self.active_cursors().primary();
3715 let old_position = cursor.position;
3716 let old_anchor = cursor.anchor;
3717 let old_sticky_column = cursor.sticky_column;
3718
3719 let events = vec![
3722 Event::MoveCursor {
3723 cursor_id,
3724 old_position,
3725 new_position: match_pos,
3726 old_anchor,
3727 new_anchor: None,
3728 old_sticky_column,
3729 new_sticky_column: 0,
3730 },
3731 Event::Delete {
3732 range: range.clone(),
3733 deleted_text,
3734 cursor_id,
3735 },
3736 Event::Insert {
3737 position: match_pos,
3738 text: replacement_text,
3739 cursor_id,
3740 },
3741 ];
3742
3743 let batch = Event::Batch {
3745 events,
3746 description: format!(
3747 "Query replace '{}' with '{}'",
3748 ir_state.search, ir_state.replacement
3749 ),
3750 };
3751
3752 self.active_event_log_mut().append(batch.clone());
3754 self.apply_event_to_active_buffer(&batch);
3755
3756 Ok(())
3757 }
3758
3759 pub(super) fn move_to_current_match(&mut self, ir_state: &InteractiveReplaceState) {
3761 self.move_cursor_to_match(ir_state.current_match_pos);
3762
3763 let msg = if ir_state.has_wrapped {
3765 "[Wrapped] Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
3766 } else {
3767 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
3768 };
3769 if let Some(ref mut prompt) = self.prompt {
3770 if prompt.prompt_type == PromptType::QueryReplaceConfirm {
3771 prompt.message = msg;
3772 prompt.input.clear();
3773 prompt.cursor_pos = 0;
3774 }
3775 }
3776 }
3777
3778 pub(super) fn finish_interactive_replace(&mut self, replacements_made: usize) {
3780 self.interactive_replace_state = None;
3781 self.prompt = None; let ns = self.search_namespace.clone();
3785 let state = self.active_state_mut();
3786 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3787
3788 self.set_status_message(t!("search.replaced_count", count = replacements_made).to_string());
3789 }
3790
3791 pub(super) fn smart_home(&mut self) {
3793 let estimated_line_length = self.config.editor.estimated_line_length;
3794 let cursor = *self.active_cursors().primary();
3795 let cursor_id = self.active_cursors().primary_id();
3796
3797 if self.config.editor.line_wrap {
3799 let split_id = self.split_manager.active_split();
3800 if let Some(new_pos) =
3801 self.smart_home_visual_line(split_id, cursor.position, estimated_line_length)
3802 {
3803 let event = Event::MoveCursor {
3804 cursor_id,
3805 old_position: cursor.position,
3806 new_position: new_pos,
3807 old_anchor: cursor.anchor,
3808 new_anchor: None,
3809 old_sticky_column: cursor.sticky_column,
3810 new_sticky_column: 0,
3811 };
3812 self.active_event_log_mut().append(event.clone());
3813 self.apply_event_to_active_buffer(&event);
3814 return;
3815 }
3816 }
3818
3819 let state = self.active_state_mut();
3820
3821 let mut iter = state
3823 .buffer
3824 .line_iterator(cursor.position, estimated_line_length);
3825 if let Some((line_start, line_content)) = iter.next_line() {
3826 let first_non_ws = line_content
3828 .chars()
3829 .take_while(|c| *c != '\n')
3830 .position(|c| !c.is_whitespace())
3831 .map(|offset| line_start + offset)
3832 .unwrap_or(line_start);
3833
3834 let new_pos = if cursor.position == first_non_ws {
3836 line_start
3837 } else {
3838 first_non_ws
3839 };
3840
3841 let event = Event::MoveCursor {
3842 cursor_id,
3843 old_position: cursor.position,
3844 new_position: new_pos,
3845 old_anchor: cursor.anchor,
3846 new_anchor: None,
3847 old_sticky_column: cursor.sticky_column,
3848 new_sticky_column: 0,
3849 };
3850
3851 self.active_event_log_mut().append(event.clone());
3852 self.apply_event_to_active_buffer(&event);
3853 }
3854 }
3855
3856 fn smart_home_visual_line(
3865 &mut self,
3866 split_id: LeafId,
3867 cursor_pos: usize,
3868 estimated_line_length: usize,
3869 ) -> Option<usize> {
3870 let visual_start = self
3871 .cached_layout
3872 .visual_line_start(split_id, cursor_pos, false)?;
3873
3874 let buffer_id = self.split_manager.active_buffer_id()?;
3876 let state = self.buffers.get_mut(&buffer_id)?;
3877 let mut iter = state
3878 .buffer
3879 .line_iterator(visual_start, estimated_line_length);
3880 let (phys_line_start, content) = iter.next_line()?;
3881
3882 let is_first_visual_row = visual_start == phys_line_start;
3883
3884 if is_first_visual_row {
3885 let visual_end = self
3887 .cached_layout
3888 .visual_line_end(split_id, cursor_pos, false)
3889 .unwrap_or(visual_start);
3890 let visual_len = visual_end.saturating_sub(visual_start);
3891 let first_non_ws = content
3892 .chars()
3893 .take(visual_len)
3894 .take_while(|c| *c != '\n')
3895 .position(|c| !c.is_whitespace())
3896 .map(|offset| visual_start + offset)
3897 .unwrap_or(visual_start);
3898
3899 if cursor_pos == first_non_ws {
3900 Some(visual_start)
3901 } else {
3902 Some(first_non_ws)
3903 }
3904 } else {
3905 if cursor_pos == visual_start {
3907 self.cached_layout
3909 .visual_line_start(split_id, cursor_pos, true)
3910 } else {
3911 Some(visual_start)
3912 }
3913 }
3914 }
3915
3916 pub(super) fn toggle_comment(&mut self) {
3918 let language = &self.active_state().language;
3921 let comment_prefix = self
3922 .config
3923 .languages
3924 .get(language)
3925 .and_then(|lang_config| lang_config.comment_prefix.clone());
3926
3927 let comment_prefix: String = match comment_prefix {
3928 Some(prefix) => {
3929 if prefix.ends_with(' ') {
3931 prefix
3932 } else {
3933 format!("{} ", prefix)
3934 }
3935 }
3936 None => return, };
3938
3939 let estimated_line_length = self.config.editor.estimated_line_length;
3940
3941 let cursor = *self.active_cursors().primary();
3942 let cursor_id = self.active_cursors().primary_id();
3943 let state = self.active_state_mut();
3944
3945 let original_anchor = cursor.anchor;
3947 let original_position = cursor.position;
3948 let had_selection = original_anchor.is_some();
3949
3950 let (start_pos, end_pos) = if let Some(range) = cursor.selection_range() {
3951 (range.start, range.end)
3952 } else {
3953 let iter = state
3954 .buffer
3955 .line_iterator(cursor.position, estimated_line_length);
3956 let line_start = iter.current_position();
3957 (line_start, cursor.position)
3958 };
3959
3960 let buffer_len = state.buffer.len();
3962 let mut line_starts = Vec::new();
3963 let mut iter = state.buffer.line_iterator(start_pos, estimated_line_length);
3964 let mut current_pos = iter.current_position();
3965 line_starts.push(current_pos);
3966
3967 while let Some((_, content)) = iter.next_line() {
3968 current_pos += content.len();
3969 if current_pos >= end_pos || current_pos >= buffer_len {
3970 break;
3971 }
3972 let next_iter = state
3973 .buffer
3974 .line_iterator(current_pos, estimated_line_length);
3975 let next_start = next_iter.current_position();
3976 if next_start != *line_starts.last().unwrap() {
3977 line_starts.push(next_start);
3978 }
3979 iter = state
3980 .buffer
3981 .line_iterator(current_pos, estimated_line_length);
3982 }
3983
3984 let all_commented = line_starts.iter().all(|&line_start| {
3987 let line_bytes = state
3988 .buffer
3989 .slice_bytes(line_start..buffer_len.min(line_start + comment_prefix.len() + 10));
3990 let line_str = String::from_utf8_lossy(&line_bytes);
3991 let trimmed = line_str.trim_start();
3992 trimmed.starts_with(comment_prefix.trim())
3993 });
3994
3995 let mut events = Vec::new();
3996 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
3999
4000 if all_commented {
4001 for &line_start in line_starts.iter().rev() {
4003 let line_bytes = state
4004 .buffer
4005 .slice_bytes(line_start..buffer_len.min(line_start + 100));
4006 let line_str = String::from_utf8_lossy(&line_bytes);
4007
4008 let leading_ws: usize = line_str
4010 .chars()
4011 .take_while(|c| c.is_whitespace() && *c != '\n')
4012 .map(|c| c.len_utf8())
4013 .sum();
4014 let rest = &line_str[leading_ws..];
4015
4016 if rest.starts_with(comment_prefix.trim()) {
4017 let remove_len = if rest.starts_with(&comment_prefix) {
4018 comment_prefix.len()
4019 } else {
4020 comment_prefix.trim().len()
4021 };
4022 let deleted_text = String::from_utf8_lossy(&state.buffer.slice_bytes(
4023 line_start + leading_ws..line_start + leading_ws + remove_len,
4024 ))
4025 .to_string();
4026 events.push(Event::Delete {
4027 range: (line_start + leading_ws)..(line_start + leading_ws + remove_len),
4028 deleted_text,
4029 cursor_id,
4030 });
4031 position_deltas.push((line_start, -(remove_len as isize)));
4032 }
4033 }
4034 } else {
4035 let prefix_len = comment_prefix.len();
4037 for &line_start in line_starts.iter().rev() {
4038 events.push(Event::Insert {
4039 position: line_start,
4040 text: comment_prefix.to_string(),
4041 cursor_id,
4042 });
4043 position_deltas.push((line_start, prefix_len as isize));
4044 }
4045 }
4046
4047 if events.is_empty() {
4048 return;
4049 }
4050
4051 let action_desc = if all_commented {
4052 "Uncomment"
4053 } else {
4054 "Comment"
4055 };
4056
4057 if had_selection {
4059 position_deltas.sort_by_key(|(pos, _)| *pos);
4061
4062 let calc_shift = |original_pos: usize| -> isize {
4064 let mut shift: isize = 0;
4065 for (edit_pos, delta) in &position_deltas {
4066 if *edit_pos < original_pos {
4067 shift += delta;
4068 }
4069 }
4070 shift
4071 };
4072
4073 let anchor_shift = calc_shift(original_anchor.unwrap_or(0));
4074 let position_shift = calc_shift(original_position);
4075
4076 let new_anchor = (original_anchor.unwrap_or(0) as isize + anchor_shift).max(0) as usize;
4077 let new_position = (original_position as isize + position_shift).max(0) as usize;
4078
4079 events.push(Event::MoveCursor {
4080 cursor_id,
4081 old_position: original_position,
4082 new_position,
4083 old_anchor: original_anchor,
4084 new_anchor: Some(new_anchor),
4085 old_sticky_column: 0,
4086 new_sticky_column: 0,
4087 });
4088 }
4089
4090 let description = format!("{} lines", action_desc);
4092 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
4093 self.active_event_log_mut().append(bulk_edit);
4094 }
4095
4096 self.set_status_message(
4097 t!(
4098 "lines.action",
4099 action = action_desc,
4100 count = line_starts.len()
4101 )
4102 .to_string(),
4103 );
4104 }
4105
4106 pub(super) fn goto_matching_bracket(&mut self) {
4108 let cursor = *self.active_cursors().primary();
4109 let cursor_id = self.active_cursors().primary_id();
4110 let state = self.active_state_mut();
4111
4112 let pos = cursor.position;
4113 if pos >= state.buffer.len() {
4114 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4115 return;
4116 }
4117
4118 let bytes = state.buffer.slice_bytes(pos..pos + 1);
4119 if bytes.is_empty() {
4120 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4121 return;
4122 }
4123
4124 let ch = bytes[0] as char;
4125
4126 const BRACKET_PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
4128
4129 let bracket_info = match ch {
4130 '(' => Some(('(', ')', true)),
4131 ')' => Some(('(', ')', false)),
4132 '[' => Some(('[', ']', true)),
4133 ']' => Some(('[', ']', false)),
4134 '{' => Some(('{', '}', true)),
4135 '}' => Some(('{', '}', false)),
4136 '<' => Some(('<', '>', true)),
4137 '>' => Some(('<', '>', false)),
4138 _ => None,
4139 };
4140
4141 use crate::view::bracket_highlight_overlay::MAX_BRACKET_SEARCH_BYTES;
4143
4144 let (opening, closing, search_start, forward) =
4147 if let Some((opening, closing, forward)) = bracket_info {
4148 (opening, closing, pos, forward)
4149 } else {
4150 let mut depths: Vec<i32> = vec![0; BRACKET_PAIRS.len()];
4153 let mut found = None;
4154 let search_limit = pos.saturating_sub(MAX_BRACKET_SEARCH_BYTES);
4155 let mut search_pos = pos.saturating_sub(1);
4156 loop {
4157 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4158 if !b.is_empty() {
4159 let c = b[0] as char;
4160 for (i, &(open, close)) in BRACKET_PAIRS.iter().enumerate() {
4161 if c == close {
4162 depths[i] += 1;
4163 } else if c == open {
4164 if depths[i] > 0 {
4165 depths[i] -= 1;
4166 } else {
4167 found = Some((open, close, search_pos));
4169 break;
4170 }
4171 }
4172 }
4173 if found.is_some() {
4174 break;
4175 }
4176 }
4177 if search_pos <= search_limit {
4178 break;
4179 }
4180 search_pos -= 1;
4181 }
4182
4183 if let Some((opening, closing, bracket_pos)) = found {
4184 (opening, closing, bracket_pos, true)
4186 } else {
4187 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4188 return;
4189 }
4190 };
4191
4192 let buffer_len = state.buffer.len();
4194 let mut depth = 1;
4195 let matching_pos = if forward {
4196 let search_limit = (search_start + 1 + MAX_BRACKET_SEARCH_BYTES).min(buffer_len);
4197 let mut search_pos = search_start + 1;
4198 let mut found = None;
4199 while search_pos < search_limit && depth > 0 {
4200 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4201 if !b.is_empty() {
4202 let c = b[0] as char;
4203 if c == opening {
4204 depth += 1;
4205 } else if c == closing {
4206 depth -= 1;
4207 if depth == 0 {
4208 found = Some(search_pos);
4209 }
4210 }
4211 }
4212 search_pos += 1;
4213 }
4214 found
4215 } else {
4216 let search_limit = search_start.saturating_sub(MAX_BRACKET_SEARCH_BYTES);
4217 let mut search_pos = search_start.saturating_sub(1);
4218 let mut found = None;
4219 loop {
4220 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4221 if !b.is_empty() {
4222 let c = b[0] as char;
4223 if c == closing {
4224 depth += 1;
4225 } else if c == opening {
4226 depth -= 1;
4227 if depth == 0 {
4228 found = Some(search_pos);
4229 break;
4230 }
4231 }
4232 }
4233 if search_pos <= search_limit {
4234 break;
4235 }
4236 search_pos -= 1;
4237 }
4238 found
4239 };
4240
4241 if let Some(new_pos) = matching_pos {
4242 let event = Event::MoveCursor {
4243 cursor_id,
4244 old_position: cursor.position,
4245 new_position: new_pos,
4246 old_anchor: cursor.anchor,
4247 new_anchor: None,
4248 old_sticky_column: cursor.sticky_column,
4249 new_sticky_column: 0,
4250 };
4251 self.active_event_log_mut().append(event.clone());
4252 self.apply_event_to_active_buffer(&event);
4253 } else {
4254 self.set_status_message(t!("diagnostics.bracket_no_match").to_string());
4255 }
4256 }
4257
4258 pub(super) fn jump_to_next_error(&mut self) {
4260 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
4261 let cursor_pos = self.active_cursors().primary().position;
4262 let cursor_id = self.active_cursors().primary_id();
4263 let cursor = *self.active_cursors().primary();
4264 let state = self.active_state_mut();
4265
4266 let mut diagnostic_positions: Vec<usize> = state
4268 .overlays
4269 .all()
4270 .iter()
4271 .filter_map(|overlay| {
4272 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4274 Some(overlay.range(&state.marker_list).start)
4275 } else {
4276 None
4277 }
4278 })
4279 .collect();
4280
4281 if diagnostic_positions.is_empty() {
4282 self.set_status_message(t!("diagnostics.none").to_string());
4283 return;
4284 }
4285
4286 diagnostic_positions.sort_unstable();
4288 diagnostic_positions.dedup();
4289
4290 let next_pos = diagnostic_positions
4292 .iter()
4293 .find(|&&pos| pos > cursor_pos)
4294 .or_else(|| diagnostic_positions.first()) .copied();
4296
4297 if let Some(new_pos) = next_pos {
4298 let event = Event::MoveCursor {
4299 cursor_id,
4300 old_position: cursor.position,
4301 new_position: new_pos,
4302 old_anchor: cursor.anchor,
4303 new_anchor: None,
4304 old_sticky_column: cursor.sticky_column,
4305 new_sticky_column: 0,
4306 };
4307 self.active_event_log_mut().append(event.clone());
4308 self.apply_event_to_active_buffer(&event);
4309
4310 let state = self.active_state();
4312 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
4313 let range = overlay.range(&state.marker_list);
4314 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4315 overlay.message.clone()
4316 } else {
4317 None
4318 }
4319 }) {
4320 self.set_status_message(msg);
4321 }
4322 }
4323 }
4324
4325 pub(super) fn jump_to_previous_error(&mut self) {
4327 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
4328 let cursor_pos = self.active_cursors().primary().position;
4329 let cursor_id = self.active_cursors().primary_id();
4330 let cursor = *self.active_cursors().primary();
4331 let state = self.active_state_mut();
4332
4333 let mut diagnostic_positions: Vec<usize> = state
4335 .overlays
4336 .all()
4337 .iter()
4338 .filter_map(|overlay| {
4339 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4341 Some(overlay.range(&state.marker_list).start)
4342 } else {
4343 None
4344 }
4345 })
4346 .collect();
4347
4348 if diagnostic_positions.is_empty() {
4349 self.set_status_message(t!("diagnostics.none").to_string());
4350 return;
4351 }
4352
4353 diagnostic_positions.sort_unstable();
4355 diagnostic_positions.dedup();
4356
4357 let prev_pos = diagnostic_positions
4359 .iter()
4360 .rev()
4361 .find(|&&pos| pos < cursor_pos)
4362 .or_else(|| diagnostic_positions.last()) .copied();
4364
4365 if let Some(new_pos) = prev_pos {
4366 let event = Event::MoveCursor {
4367 cursor_id,
4368 old_position: cursor.position,
4369 new_position: new_pos,
4370 old_anchor: cursor.anchor,
4371 new_anchor: None,
4372 old_sticky_column: cursor.sticky_column,
4373 new_sticky_column: 0,
4374 };
4375 self.active_event_log_mut().append(event.clone());
4376 self.apply_event_to_active_buffer(&event);
4377
4378 let state = self.active_state();
4380 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
4381 let range = overlay.range(&state.marker_list);
4382 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4383 overlay.message.clone()
4384 } else {
4385 None
4386 }
4387 }) {
4388 self.set_status_message(msg);
4389 }
4390 }
4391 }
4392
4393 pub(super) fn toggle_macro_recording(&mut self, key: char) {
4395 if let Some(state) = &self.macro_recording {
4396 if state.key == key {
4397 self.stop_macro_recording();
4399 } else {
4400 self.stop_macro_recording();
4402 self.start_macro_recording(key);
4403 }
4404 } else {
4405 self.start_macro_recording(key);
4407 }
4408 }
4409
4410 pub(super) fn start_macro_recording(&mut self, key: char) {
4412 self.macro_recording = Some(MacroRecordingState {
4413 key,
4414 actions: Vec::new(),
4415 });
4416
4417 let stop_hint = self.build_macro_stop_hint(key);
4419 self.set_status_message(
4420 t!(
4421 "macro.recording_with_hint",
4422 key = key,
4423 stop_hint = stop_hint
4424 )
4425 .to_string(),
4426 );
4427 }
4428
4429 fn build_macro_stop_hint(&self, _key: char) -> String {
4431 let mut hints = Vec::new();
4432
4433 if let Some(stop_key) = self.get_keybinding_for_action("stop_macro_recording") {
4435 hints.push(stop_key);
4436 }
4437
4438 let palette_key = self
4440 .get_keybinding_for_action("command_palette")
4441 .unwrap_or_else(|| "Ctrl+P".to_string());
4442
4443 if hints.is_empty() {
4444 format!("{} → Stop Recording Macro", palette_key)
4446 } else {
4447 format!("{} or {} → Stop Recording", hints.join("/"), palette_key)
4449 }
4450 }
4451
4452 pub(super) fn stop_macro_recording(&mut self) {
4454 if let Some(state) = self.macro_recording.take() {
4455 let action_count = state.actions.len();
4456 let key = state.key;
4457 self.macros.insert(key, state.actions);
4458 self.last_macro_register = Some(key);
4459
4460 let play_hint = self.build_macro_play_hint();
4462 self.set_status_message(
4463 t!(
4464 "macro.saved",
4465 key = key,
4466 count = action_count,
4467 play_hint = play_hint
4468 )
4469 .to_string(),
4470 );
4471 } else {
4472 self.set_status_message(t!("macro.not_recording").to_string());
4473 }
4474 }
4475
4476 fn build_macro_play_hint(&self) -> String {
4478 if let Some(play_key) = self.get_keybinding_for_action("play_last_macro") {
4480 return format!("{} → Play Last Macro", play_key);
4481 }
4482
4483 let palette_key = self
4485 .get_keybinding_for_action("command_palette")
4486 .unwrap_or_else(|| "Ctrl+P".to_string());
4487
4488 format!("{} → Play Macro", palette_key)
4489 }
4490
4491 pub fn recompute_layout(&mut self, width: u16, height: u16) {
4496 let size = ratatui::layout::Rect::new(0, 0, width, height);
4497
4498 let active_split = self.split_manager.active_split();
4500 self.pre_sync_ensure_visible(active_split);
4501 self.sync_scroll_groups();
4502
4503 let constraints = vec![
4506 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
4507 Constraint::Min(0),
4508 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(if self.prompt_line_visible { 1 } else { 0 }), ];
4512 let main_chunks = Layout::default()
4513 .direction(Direction::Vertical)
4514 .constraints(constraints)
4515 .split(size);
4516 let main_content_area = main_chunks[1];
4517
4518 let file_explorer_should_show = self.file_explorer_visible
4520 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
4521 let editor_content_area = if file_explorer_should_show {
4522 let explorer_percent = (self.file_explorer_width_percent * 100.0) as u16;
4523 let editor_percent = 100 - explorer_percent;
4524 let horizontal_chunks = Layout::default()
4525 .direction(Direction::Horizontal)
4526 .constraints([
4527 Constraint::Percentage(explorer_percent),
4528 Constraint::Percentage(editor_percent),
4529 ])
4530 .split(main_content_area);
4531 horizontal_chunks[1]
4532 } else {
4533 main_content_area
4534 };
4535
4536 let view_line_mappings = SplitRenderer::compute_content_layout(
4538 editor_content_area,
4539 &self.split_manager,
4540 &mut self.buffers,
4541 &mut self.split_view_states,
4542 &self.theme,
4543 false, self.config.editor.estimated_line_length,
4545 self.config.editor.highlight_context_bytes,
4546 self.config.editor.relative_line_numbers,
4547 self.config.editor.use_terminal_bg,
4548 self.session_mode || !self.software_cursor_only,
4549 self.software_cursor_only,
4550 self.tab_bar_visible,
4551 self.config.editor.show_vertical_scrollbar,
4552 self.config.editor.show_horizontal_scrollbar,
4553 self.config.editor.diagnostics_inline_text,
4554 self.config.editor.show_tilde,
4555 );
4556
4557 self.cached_layout.view_line_mappings = view_line_mappings;
4558 }
4559
4560 pub(super) fn play_macro(&mut self, key: char) {
4567 if self.macro_playing {
4569 return;
4570 }
4571
4572 if let Some(actions) = self.macros.get(&key).cloned() {
4573 if actions.is_empty() {
4574 self.set_status_message(t!("macro.empty", key = key).to_string());
4575 return;
4576 }
4577
4578 self.macro_playing = true;
4579 let action_count = actions.len();
4580 let width = self.cached_layout.last_frame_width;
4581 let height = self.cached_layout.last_frame_height;
4582 for action in actions {
4583 if let Err(e) = self.handle_action(action) {
4584 tracing::warn!("Macro action failed: {}", e);
4585 }
4586 self.recompute_layout(width, height);
4587 }
4588 self.macro_playing = false;
4589
4590 self.set_status_message(
4591 t!("macro.played", key = key, count = action_count).to_string(),
4592 );
4593 } else {
4594 self.set_status_message(t!("macro.not_found", key = key).to_string());
4595 }
4596 }
4597
4598 pub(super) fn record_macro_action(&mut self, action: &Action) {
4600 if self.macro_playing {
4602 return;
4603 }
4604 if let Some(state) = &mut self.macro_recording {
4605 match action {
4607 Action::StartMacroRecording
4608 | Action::StopMacroRecording
4609 | Action::PlayMacro(_)
4610 | Action::ToggleMacroRecording(_)
4611 | Action::ShowMacro(_)
4612 | Action::ListMacros
4613 | Action::PromptRecordMacro
4614 | Action::PromptPlayMacro
4615 | Action::PlayLastMacro => {}
4616 Action::PromptConfirm => {
4619 if let Some(prompt) = &self.prompt {
4620 let text = prompt.get_text().to_string();
4621 state.actions.push(Action::PromptConfirmWithText(text));
4622 } else {
4623 state.actions.push(action.clone());
4624 }
4625 }
4626 _ => {
4627 state.actions.push(action.clone());
4628 }
4629 }
4630 }
4631 }
4632
4633 pub(super) fn show_macro_in_buffer(&mut self, key: char) {
4635 let (json, actions_len) = match self.macros.get(&key) {
4637 Some(actions) => {
4638 let json = match serde_json::to_string_pretty(actions) {
4639 Ok(json) => json,
4640 Err(e) => {
4641 self.set_status_message(
4642 t!("macro.serialize_failed", error = e.to_string()).to_string(),
4643 );
4644 return;
4645 }
4646 };
4647 (json, actions.len())
4648 }
4649 None => {
4650 self.set_status_message(t!("macro.not_found", key = key).to_string());
4651 return;
4652 }
4653 };
4654
4655 let content = format!(
4657 "// Macro '{}' ({} actions)\n// This buffer can be saved as a .json file for persistence\n\n{}",
4658 key,
4659 actions_len,
4660 json
4661 );
4662
4663 let buffer_id = BufferId(self.next_buffer_id);
4665 self.next_buffer_id += 1;
4666
4667 let mut state = EditorState::new(
4668 self.terminal_width,
4669 self.terminal_height,
4670 self.config.editor.large_file_threshold_bytes as usize,
4671 std::sync::Arc::clone(&self.filesystem),
4672 );
4673 state
4674 .margins
4675 .configure_for_line_numbers(self.config.editor.line_numbers);
4676
4677 self.buffers.insert(buffer_id, state);
4678 self.event_logs.insert(buffer_id, EventLog::new());
4679
4680 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4682 state.buffer = crate::model::buffer::Buffer::from_str(
4683 &content,
4684 self.config.editor.large_file_threshold_bytes as usize,
4685 std::sync::Arc::clone(&self.filesystem),
4686 );
4687 }
4688
4689 let metadata = BufferMetadata {
4691 kind: BufferKind::Virtual {
4692 mode: "macro-view".to_string(),
4693 },
4694 display_name: format!("*Macro {}*", key),
4695 lsp_enabled: false,
4696 lsp_disabled_reason: Some("Virtual macro buffer".to_string()),
4697 read_only: false, binary: false,
4699 lsp_opened_with: std::collections::HashSet::new(),
4700 hidden_from_tabs: false,
4701 recovery_id: None,
4702 };
4703 self.buffer_metadata.insert(buffer_id, metadata);
4704
4705 self.set_active_buffer(buffer_id);
4707 self.set_status_message(
4708 t!("macro.shown_buffer", key = key, count = actions_len).to_string(),
4709 );
4710 }
4711
4712 pub(super) fn list_macros_in_buffer(&mut self) {
4714 if self.macros.is_empty() {
4715 self.set_status_message(t!("macro.none_recorded").to_string());
4716 return;
4717 }
4718
4719 let mut content =
4721 String::from("// Recorded Macros\n// Use ShowMacro(key) to see details\n\n");
4722
4723 let mut keys: Vec<char> = self.macros.keys().copied().collect();
4724 keys.sort();
4725
4726 for key in keys {
4727 if let Some(actions) = self.macros.get(&key) {
4728 content.push_str(&format!("Macro '{}': {} actions\n", key, actions.len()));
4729
4730 for (i, action) in actions.iter().enumerate() {
4732 content.push_str(&format!(" {}. {:?}\n", i + 1, action));
4733 }
4734 content.push('\n');
4735 }
4736 }
4737
4738 let buffer_id = BufferId(self.next_buffer_id);
4740 self.next_buffer_id += 1;
4741
4742 let mut state = EditorState::new(
4743 self.terminal_width,
4744 self.terminal_height,
4745 self.config.editor.large_file_threshold_bytes as usize,
4746 std::sync::Arc::clone(&self.filesystem),
4747 );
4748 state
4749 .margins
4750 .configure_for_line_numbers(self.config.editor.line_numbers);
4751
4752 self.buffers.insert(buffer_id, state);
4753 self.event_logs.insert(buffer_id, EventLog::new());
4754
4755 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4757 state.buffer = crate::model::buffer::Buffer::from_str(
4758 &content,
4759 self.config.editor.large_file_threshold_bytes as usize,
4760 std::sync::Arc::clone(&self.filesystem),
4761 );
4762 }
4763
4764 let metadata = BufferMetadata {
4766 kind: BufferKind::Virtual {
4767 mode: "macro-list".to_string(),
4768 },
4769 display_name: "*Macros*".to_string(),
4770 lsp_enabled: false,
4771 lsp_disabled_reason: Some("Virtual macro list buffer".to_string()),
4772 read_only: true,
4773 binary: false,
4774 lsp_opened_with: std::collections::HashSet::new(),
4775 hidden_from_tabs: false,
4776 recovery_id: None,
4777 };
4778 self.buffer_metadata.insert(buffer_id, metadata);
4779
4780 self.set_active_buffer(buffer_id);
4782 self.set_status_message(t!("macro.showing", count = self.macros.len()).to_string());
4783 }
4784
4785 pub(super) fn set_bookmark(&mut self, key: char) {
4787 let buffer_id = self.active_buffer();
4788 let position = self.active_cursors().primary().position;
4789 self.bookmarks.insert(
4790 key,
4791 Bookmark {
4792 buffer_id,
4793 position,
4794 },
4795 );
4796 self.set_status_message(t!("bookmark.set", key = key).to_string());
4797 }
4798
4799 pub(super) fn jump_to_bookmark(&mut self, key: char) {
4801 if let Some(bookmark) = self.bookmarks.get(&key).cloned() {
4802 if bookmark.buffer_id != self.active_buffer() {
4804 if self.buffers.contains_key(&bookmark.buffer_id) {
4805 self.set_active_buffer(bookmark.buffer_id);
4806 } else {
4807 self.set_status_message(t!("bookmark.buffer_gone", key = key).to_string());
4808 self.bookmarks.remove(&key);
4809 return;
4810 }
4811 }
4812
4813 let cursor = *self.active_cursors().primary();
4815 let cursor_id = self.active_cursors().primary_id();
4816 let state = self.active_state_mut();
4817 let new_pos = bookmark.position.min(state.buffer.len());
4818
4819 let event = Event::MoveCursor {
4820 cursor_id,
4821 old_position: cursor.position,
4822 new_position: new_pos,
4823 old_anchor: cursor.anchor,
4824 new_anchor: None,
4825 old_sticky_column: cursor.sticky_column,
4826 new_sticky_column: 0,
4827 };
4828
4829 self.active_event_log_mut().append(event.clone());
4830 self.apply_event_to_active_buffer(&event);
4831 self.set_status_message(t!("bookmark.jumped", key = key).to_string());
4832 } else {
4833 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
4834 }
4835 }
4836
4837 pub(super) fn clear_bookmark(&mut self, key: char) {
4839 if self.bookmarks.remove(&key).is_some() {
4840 self.set_status_message(t!("bookmark.cleared", key = key).to_string());
4841 } else {
4842 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
4843 }
4844 }
4845
4846 pub(super) fn list_bookmarks(&mut self) {
4848 if self.bookmarks.is_empty() {
4849 self.set_status_message(t!("bookmark.none_set").to_string());
4850 return;
4851 }
4852
4853 let mut bookmark_list: Vec<_> = self.bookmarks.iter().collect();
4854 bookmark_list.sort_by_key(|(k, _)| *k);
4855
4856 let list_str: String = bookmark_list
4857 .iter()
4858 .map(|(k, bm)| {
4859 let buffer_name = self
4860 .buffer_metadata
4861 .get(&bm.buffer_id)
4862 .map(|m| m.display_name.as_str())
4863 .unwrap_or("unknown");
4864 format!("'{}': {} @ {}", k, buffer_name, bm.position)
4865 })
4866 .collect::<Vec<_>>()
4867 .join(", ");
4868
4869 self.set_status_message(t!("bookmark.list", list = list_str).to_string());
4870 }
4871
4872 pub fn clear_search_history(&mut self) {
4875 if let Some(history) = self.prompt_histories.get_mut("search") {
4876 history.clear();
4877 }
4878 }
4879
4880 pub fn save_histories(&self) {
4883 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.data_dir) {
4885 tracing::warn!("Failed to create data directory: {}", e);
4886 return;
4887 }
4888
4889 for (key, history) in &self.prompt_histories {
4891 let path = self.dir_context.prompt_history_path(key);
4892 if let Err(e) = history.save_to_file(&path) {
4893 tracing::warn!("Failed to save {} history: {}", key, e);
4894 } else {
4895 tracing::debug!("Saved {} history to {:?}", key, path);
4896 }
4897 }
4898 }
4899
4900 pub(super) fn ensure_active_tab_visible(
4904 &mut self,
4905 split_id: LeafId,
4906 active_buffer: BufferId,
4907 available_width: u16,
4908 ) {
4909 tracing::debug!(
4910 "ensure_active_tab_visible called: split={:?}, buffer={:?}, width={}",
4911 split_id,
4912 active_buffer,
4913 available_width
4914 );
4915 let Some(view_state) = self.split_view_states.get_mut(&split_id) else {
4916 tracing::debug!(" -> no view_state for split");
4917 return;
4918 };
4919
4920 let split_buffers = view_state.open_buffers.clone();
4921 let group_names: std::collections::HashMap<LeafId, String> = self
4923 .grouped_subtrees
4924 .iter()
4925 .filter_map(|(leaf_id, node)| {
4926 if let crate::view::split::SplitNode::Grouped { name, .. } = node {
4927 Some((*leaf_id, name.clone()))
4928 } else {
4929 None
4930 }
4931 })
4932 .collect();
4933
4934 let (tab_widths, rendered_targets) = crate::view::ui::tabs::calculate_tab_widths(
4936 &split_buffers,
4937 &self.buffers,
4938 &self.buffer_metadata,
4939 &self.composite_buffers,
4940 &group_names,
4941 );
4942
4943 let total_tabs_width: usize = tab_widths.iter().sum();
4944 let max_visible_width = available_width as usize;
4945
4946 let active_target = view_state.active_target();
4948 let active_target = if matches!(active_target, crate::view::split::TabTarget::Buffer(_)) {
4951 crate::view::split::TabTarget::Buffer(active_buffer)
4952 } else {
4953 active_target
4954 };
4955
4956 let active_tab_index = rendered_targets.iter().position(|t| *t == active_target);
4959
4960 let active_width_index = active_tab_index.map(|buf_idx| {
4964 if buf_idx == 0 {
4965 0
4966 } else {
4967 buf_idx * 2
4972 }
4973 });
4974
4975 let old_offset = view_state.tab_scroll_offset;
4977 let new_scroll_offset = if let Some(idx) = active_width_index {
4978 crate::view::ui::tabs::scroll_to_show_tab(
4979 &tab_widths,
4980 idx,
4981 view_state.tab_scroll_offset,
4982 max_visible_width,
4983 )
4984 } else {
4985 view_state
4986 .tab_scroll_offset
4987 .min(total_tabs_width.saturating_sub(max_visible_width))
4988 };
4989
4990 tracing::debug!(
4991 " -> offset: {} -> {} (idx={:?}, max_width={}, total={})",
4992 old_offset,
4993 new_scroll_offset,
4994 active_width_index,
4995 max_visible_width,
4996 total_tabs_width
4997 );
4998 view_state.tab_scroll_offset = new_scroll_offset;
4999 }
5000
5001 fn sync_scroll_groups(&mut self) {
5007 let active_split = self.split_manager.active_split();
5008 let group_count = self.scroll_sync_manager.groups().len();
5009
5010 if group_count > 0 {
5011 tracing::debug!(
5012 "sync_scroll_groups: active_split={:?}, {} groups",
5013 active_split,
5014 group_count
5015 );
5016 }
5017
5018 let sync_info: Vec<_> = self
5021 .scroll_sync_manager
5022 .groups()
5023 .iter()
5024 .filter_map(|group| {
5025 tracing::debug!(
5026 "sync_scroll_groups: checking group {}, left={:?}, right={:?}",
5027 group.id,
5028 group.left_split,
5029 group.right_split
5030 );
5031
5032 if !group.contains_split(active_split.into()) {
5033 tracing::debug!(
5034 "sync_scroll_groups: active split {:?} not in group",
5035 active_split
5036 );
5037 return None;
5038 }
5039
5040 let active_top_byte = self
5042 .split_view_states
5043 .get(&active_split)?
5044 .viewport
5045 .top_byte;
5046
5047 let active_buffer_id = self.split_manager.buffer_for_split(active_split)?;
5049 let buffer_state = self.buffers.get(&active_buffer_id)?;
5050 let buffer_len = buffer_state.buffer.len();
5051 let active_line = buffer_state.buffer.get_line_number(active_top_byte);
5052
5053 tracing::debug!(
5054 "sync_scroll_groups: active_split={:?}, buffer_id={:?}, top_byte={}, buffer_len={}, active_line={}",
5055 active_split,
5056 active_buffer_id,
5057 active_top_byte,
5058 buffer_len,
5059 active_line
5060 );
5061
5062 let (other_split, other_line) = if group.is_left_split(active_split.into()) {
5064 (group.right_split, group.left_to_right_line(active_line))
5066 } else {
5067 (group.left_split, group.right_to_left_line(active_line))
5069 };
5070
5071 tracing::debug!(
5072 "sync_scroll_groups: syncing other_split={:?} to line {}",
5073 other_split,
5074 other_line
5075 );
5076
5077 Some((other_split, other_line))
5078 })
5079 .collect();
5080
5081 for (other_split, target_line) in sync_info {
5083 let other_leaf = LeafId(other_split);
5084 if let Some(buffer_id) = self.split_manager.buffer_for_split(other_leaf) {
5085 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5086 let buffer = &mut state.buffer;
5087 if let Some(view_state) = self.split_view_states.get_mut(&other_leaf) {
5088 view_state.viewport.scroll_to(buffer, target_line);
5089 }
5090 }
5091 }
5092 }
5093
5094 let active_buffer_id = if self.same_buffer_scroll_sync {
5104 self.split_manager.buffer_for_split(active_split)
5105 } else {
5106 None
5107 };
5108 if let Some(active_buf_id) = active_buffer_id {
5109 let active_top_byte = self
5110 .split_view_states
5111 .get(&active_split)
5112 .map(|vs| vs.viewport.top_byte);
5113 let active_viewport_height = self
5114 .split_view_states
5115 .get(&active_split)
5116 .map(|vs| vs.viewport.visible_line_count())
5117 .unwrap_or(0);
5118
5119 if let Some(top_byte) = active_top_byte {
5120 let other_splits: Vec<_> = self
5122 .split_view_states
5123 .keys()
5124 .filter(|&&s| {
5125 s != active_split
5126 && self.split_manager.buffer_for_split(s) == Some(active_buf_id)
5127 && !self.scroll_sync_manager.is_split_synced(s.into())
5128 })
5129 .copied()
5130 .collect();
5131
5132 if !other_splits.is_empty() {
5133 let at_bottom = if let Some(state) = self.buffers.get_mut(&active_buf_id) {
5136 let mut iter = state.buffer.line_iterator(top_byte, 80);
5137 let mut lines_remaining = 0;
5138 while iter.next_line().is_some() {
5139 lines_remaining += 1;
5140 if lines_remaining > active_viewport_height {
5141 break;
5142 }
5143 }
5144 lines_remaining <= active_viewport_height
5145 } else {
5146 false
5147 };
5148
5149 for other_split in other_splits {
5150 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
5151 view_state.viewport.top_byte = top_byte;
5152 view_state.viewport.sync_scroll_to_end = at_bottom;
5155 }
5156 }
5157 }
5158 }
5159 }
5160 }
5161
5162 fn pre_sync_ensure_visible(&mut self, active_split: LeafId) {
5171 let group_info = self
5173 .scroll_sync_manager
5174 .find_group_for_split(active_split.into())
5175 .map(|g| (g.left_split, g.right_split));
5176
5177 if let Some((left_split, right_split)) = group_info {
5178 if let Some(buffer_id) = self.split_manager.buffer_for_split(active_split) {
5180 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5181 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
5182 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
5184
5185 tracing::debug!(
5186 "pre_sync_ensure_visible: updated active split {:?} viewport, top_byte={}",
5187 active_split,
5188 view_state.viewport.top_byte
5189 );
5190 }
5191 }
5192 }
5193
5194 let active_sid: SplitId = active_split.into();
5196 let other_split: SplitId = if active_sid == left_split {
5197 right_split
5198 } else {
5199 left_split
5200 };
5201
5202 if let Some(view_state) = self.split_view_states.get_mut(&LeafId(other_split)) {
5203 view_state.viewport.set_skip_ensure_visible();
5204 tracing::debug!(
5205 "pre_sync_ensure_visible: marked other split {:?} to skip ensure_visible",
5206 other_split
5207 );
5208 }
5209 }
5210
5211 if !self.same_buffer_scroll_sync {
5214 } else if let Some(active_buf_id) = self.split_manager.buffer_for_split(active_split) {
5216 let other_same_buffer_splits: Vec<_> = self
5217 .split_view_states
5218 .keys()
5219 .filter(|&&s| {
5220 s != active_split
5221 && self.split_manager.buffer_for_split(s) == Some(active_buf_id)
5222 && !self.scroll_sync_manager.is_split_synced(s.into())
5223 })
5224 .copied()
5225 .collect();
5226
5227 for other_split in other_same_buffer_splits {
5228 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
5229 view_state.viewport.set_skip_ensure_visible();
5230 }
5231 }
5232 }
5233 }
5234}