1use super::*;
2use anyhow::Result as AnyhowResult;
3use rust_i18n::t;
4
5impl Editor {
6 pub fn render(&mut self, frame: &mut Frame) {
8 let _span = tracing::info_span!("render").entered();
9 let size = frame.area();
10
11 self.cached_layout.last_frame_width = size.width;
13 self.cached_layout.last_frame_height = size.height;
14
15 let active_split = self.split_manager.active_split();
20 {
21 let _span = tracing::info_span!("pre_sync_ensure_visible").entered();
22 self.pre_sync_ensure_visible(active_split);
23 }
24
25 {
28 let _span = tracing::info_span!("sync_scroll_groups").entered();
29 self.sync_scroll_groups();
30 }
31
32 let mut semantic_ranges: std::collections::HashMap<BufferId, (usize, usize)> =
38 std::collections::HashMap::new();
39 {
40 let _span = tracing::info_span!("compute_semantic_ranges").entered();
41 for (split_id, view_state) in &self.split_view_states {
42 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
43 if let Some(state) = self.buffers.get(&buffer_id) {
44 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
45 let visible_lines =
46 view_state.viewport.visible_line_count().saturating_sub(1);
47 let end_line = start_line.saturating_add(visible_lines);
48 semantic_ranges
49 .entry(buffer_id)
50 .and_modify(|(min_start, max_end)| {
51 *min_start = (*min_start).min(start_line);
52 *max_end = (*max_end).max(end_line);
53 })
54 .or_insert((start_line, end_line));
55 }
56 }
57 }
58 }
59 for (buffer_id, (start_line, end_line)) in semantic_ranges {
60 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
61 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
62 self.maybe_request_folding_ranges_debounced(buffer_id);
63 }
64
65 {
66 let _span = tracing::info_span!("prepare_for_render").entered();
67 for (split_id, view_state) in &self.split_view_states {
68 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
69 if let Some(state) = self.buffers.get_mut(&buffer_id) {
70 let top_byte = view_state.viewport.top_byte;
71 let height = view_state.viewport.height;
72 if let Err(e) = state.prepare_for_render(top_byte, height) {
73 tracing::error!("Failed to prepare buffer for render: {}", e);
74 }
76 }
77 }
78 }
79 }
80
81 let is_search_prompt_active = self.prompt.as_ref().is_some_and(|p| {
84 matches!(
85 p.prompt_type,
86 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
87 )
88 });
89 if is_search_prompt_active {
90 if let Some(ref search_state) = self.search_state {
91 let query = search_state.query.clone();
92 self.update_search_highlights(&query);
93 }
94 }
95
96 let show_search_options = self.prompt.as_ref().is_some_and(|p| {
98 matches!(
99 p.prompt_type,
100 PromptType::Search
101 | PromptType::ReplaceSearch
102 | PromptType::Replace { .. }
103 | PromptType::QueryReplaceSearch
104 | PromptType::QueryReplace { .. }
105 )
106 });
107
108 let has_suggestions = self
110 .prompt
111 .as_ref()
112 .is_some_and(|p| !p.suggestions.is_empty());
113 let has_file_browser = self.prompt.as_ref().is_some_and(|p| {
114 matches!(
115 p.prompt_type,
116 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
117 )
118 }) && self.file_open_state.is_some();
119
120 let constraints = vec![
124 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }), Constraint::Min(0), Constraint::Length(
127 if !self.status_bar_visible || has_suggestions || has_file_browser {
128 0
129 } else {
130 1
131 },
132 ), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(1), ];
136
137 let main_chunks = Layout::default()
138 .direction(Direction::Vertical)
139 .constraints(constraints)
140 .split(size);
141
142 let menu_bar_area = main_chunks[0];
143 let main_content_area = main_chunks[1];
144 let status_bar_idx = 2;
145 let search_options_idx = 3;
146 let prompt_line_idx = 4;
147
148 let editor_content_area;
151 let file_explorer_should_show = self.file_explorer_visible
152 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
153
154 if file_explorer_should_show {
155 tracing::trace!(
157 "render: file explorer layout active (present={}, sync_in_progress={})",
158 self.file_explorer.is_some(),
159 self.file_explorer_sync_in_progress
160 );
161 let explorer_percent = (self.file_explorer_width_percent * 100.0) as u16;
163 let editor_percent = 100 - explorer_percent;
164 let horizontal_chunks = Layout::default()
165 .direction(Direction::Horizontal)
166 .constraints([
167 Constraint::Percentage(explorer_percent), Constraint::Percentage(editor_percent), ])
170 .split(main_content_area);
171
172 self.cached_layout.file_explorer_area = Some(horizontal_chunks[0]);
173 editor_content_area = horizontal_chunks[1];
174
175 let remote_connection = self.remote_connection_info().map(|s| s.to_string());
177
178 if let Some(ref mut explorer) = self.file_explorer {
180 let is_focused = self.key_context == KeyContext::FileExplorer;
181
182 let mut files_with_unsaved_changes = std::collections::HashSet::new();
184 for (buffer_id, state) in &self.buffers {
185 if state.buffer.is_modified() {
186 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
187 if let Some(file_path) = metadata.file_path() {
188 files_with_unsaved_changes.insert(file_path.clone());
189 }
190 }
191 }
192 }
193
194 let close_button_hovered = matches!(
195 &self.mouse_state.hover_target,
196 Some(HoverTarget::FileExplorerCloseButton)
197 );
198 FileExplorerRenderer::render(
199 explorer,
200 frame,
201 horizontal_chunks[0],
202 is_focused,
203 &files_with_unsaved_changes,
204 &self.file_explorer_decoration_cache,
205 &self.keybindings,
206 self.key_context,
207 &self.theme,
208 close_button_hovered,
209 remote_connection.as_deref(),
210 );
211 }
212 } else {
215 self.cached_layout.file_explorer_area = None;
217 editor_content_area = main_content_area;
218 }
219
220 if self.plugin_manager.is_active() {
227 let hooks_start = std::time::Instant::now();
228 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
230
231 let mut total_new_lines = 0usize;
232 for (split_id, buffer_id, split_area) in visible_buffers {
233 let viewport_top_byte = self
235 .split_view_states
236 .get(&split_id)
237 .map(|vs| vs.viewport.top_byte)
238 .unwrap_or(0);
239
240 if let Some(state) = self.buffers.get_mut(&buffer_id) {
241 self.plugin_manager.run_hook(
243 "render_start",
244 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
245 );
246
247 let visible_count = split_area.height as usize;
250 let is_binary = state.buffer.is_binary();
251 let line_ending = state.buffer.line_ending();
252 let base_tokens =
253 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
254 &mut state.buffer,
255 viewport_top_byte,
256 self.config.editor.estimated_line_length,
257 visible_count,
258 is_binary,
259 line_ending,
260 );
261 let viewport_start = viewport_top_byte;
262 let viewport_end = base_tokens
263 .last()
264 .and_then(|t| t.source_offset)
265 .unwrap_or(viewport_start);
266 let cursor_positions: Vec<usize> = self
267 .split_view_states
268 .get(&split_id)
269 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
270 .unwrap_or_default();
271 self.plugin_manager.run_hook(
272 "view_transform_request",
273 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
274 buffer_id,
275 split_id: split_id.into(),
276 viewport_start,
277 viewport_end,
278 tokens: base_tokens,
279 cursor_positions,
280 },
281 );
282
283 if let Some(vs) = self.split_view_states.get_mut(&split_id) {
287 vs.view_transform_stale = false;
288 }
289
290 let visible_count = split_area.height as usize;
292 let top_byte = viewport_top_byte;
293
294 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
296
297 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
299 let mut line_number = state.buffer.get_line_number(top_byte);
300 let mut iter = state
301 .buffer
302 .line_iterator(top_byte, self.config.editor.estimated_line_length);
303
304 for _ in 0..visible_count {
305 if let Some((line_start, line_content)) = iter.next_line() {
306 let byte_end = line_start + line_content.len();
307 let byte_range = (line_start, byte_end);
308
309 if !seen_byte_ranges.contains(&byte_range) {
311 new_lines.push(crate::services::plugins::hooks::LineInfo {
312 line_number,
313 byte_start: line_start,
314 byte_end,
315 content: line_content,
316 });
317 seen_byte_ranges.insert(byte_range);
318 }
319 line_number += 1;
320 } else {
321 break;
322 }
323 }
324
325 if !new_lines.is_empty() {
327 total_new_lines += new_lines.len();
328 self.plugin_manager.run_hook(
329 "lines_changed",
330 crate::services::plugins::hooks::HookArgs::LinesChanged {
331 buffer_id,
332 lines: new_lines,
333 },
334 );
335 }
336 }
337 }
338 let hooks_elapsed = hooks_start.elapsed();
339 tracing::trace!(
340 new_lines = total_new_lines,
341 elapsed_ms = hooks_elapsed.as_millis(),
342 elapsed_us = hooks_elapsed.as_micros(),
343 "lines_changed hooks total"
344 );
345
346 let commands = self.plugin_manager.process_commands();
358 if !commands.is_empty() {
359 let cmd_names: Vec<String> =
360 commands.iter().map(|c| c.debug_variant_name()).collect();
361 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
362 }
363 for command in commands {
364 if let Err(e) = self.handle_plugin_command(command) {
365 tracing::error!("Error handling plugin command: {}", e);
366 }
367 }
368
369 self.flush_pending_grammars();
371 }
372
373 let lsp_waiting = self.pending_completion_request.is_some()
375 || self.pending_goto_definition_request.is_some();
376
377 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
384 let hide_cursor = self.menu_state.active_menu.is_some()
385 || self.key_context == KeyContext::FileExplorer
386 || self.terminal_mode
387 || settings_visible
388 || self.keybinding_editor.is_some();
389
390 let hovered_tab = match &self.mouse_state.hover_target {
392 Some(HoverTarget::TabName(buffer_id, split_id)) => Some((*buffer_id, *split_id, false)),
393 Some(HoverTarget::TabCloseButton(buffer_id, split_id)) => {
394 Some((*buffer_id, *split_id, true))
395 }
396 _ => None,
397 };
398
399 let hovered_close_split = match &self.mouse_state.hover_target {
401 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
402 _ => None,
403 };
404
405 let hovered_maximize_split = match &self.mouse_state.hover_target {
407 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
408 _ => None,
409 };
410
411 let is_maximized = self.split_manager.is_maximized();
412
413 let _content_span = tracing::info_span!("render_content").entered();
414 let (
415 split_areas,
416 tab_layouts,
417 close_split_areas,
418 maximize_split_areas,
419 view_line_mappings,
420 horizontal_scrollbar_areas,
421 ) = SplitRenderer::render_content(
422 frame,
423 editor_content_area,
424 &self.split_manager,
425 &mut self.buffers,
426 &self.buffer_metadata,
427 &mut self.event_logs,
428 &self.composite_buffers,
429 &mut self.composite_view_states,
430 &self.theme,
431 self.ansi_background.as_ref(),
432 self.background_fade,
433 lsp_waiting,
434 self.config.editor.large_file_threshold_bytes,
435 self.config.editor.line_wrap,
436 self.config.editor.estimated_line_length,
437 self.config.editor.highlight_context_bytes,
438 Some(&mut self.split_view_states),
439 hide_cursor,
440 hovered_tab,
441 hovered_close_split,
442 hovered_maximize_split,
443 is_maximized,
444 self.config.editor.relative_line_numbers,
445 self.tab_bar_visible,
446 self.config.editor.use_terminal_bg,
447 self.session_mode || !self.config.editor.cursor_style.is_block(),
448 self.software_cursor_only,
449 self.config.editor.show_vertical_scrollbar,
450 self.config.editor.show_horizontal_scrollbar,
451 self.config.editor.diagnostics_inline_text,
452 );
453
454 drop(_content_span);
455
456 if self.plugin_manager.is_active() {
460 for (split_id, view_state) in &self.split_view_states {
461 let current = (
462 view_state.viewport.top_byte,
463 view_state.viewport.width,
464 view_state.viewport.height,
465 );
466 let (changed, previous) = match self.previous_viewports.get(split_id) {
471 Some(previous) => (*previous != current, Some(*previous)),
472 None => (false, None), };
474 tracing::trace!(
475 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
476 split_id,
477 current,
478 previous,
479 changed
480 );
481 if changed {
482 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
483 let top_line = self.buffers.get(&buffer_id).and_then(|state| {
485 if state.buffer.line_count().is_some() {
486 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
487 } else {
488 None
489 }
490 });
491 tracing::debug!(
492 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
493 split_id,
494 buffer_id,
495 view_state.viewport.top_byte,
496 top_line
497 );
498 self.plugin_manager.run_hook(
499 "viewport_changed",
500 crate::services::plugins::hooks::HookArgs::ViewportChanged {
501 split_id: (*split_id).into(),
502 buffer_id,
503 top_byte: view_state.viewport.top_byte,
504 top_line,
505 width: view_state.viewport.width,
506 height: view_state.viewport.height,
507 },
508 );
509 }
510 }
511 }
512 }
513
514 self.previous_viewports.clear();
516 for (split_id, view_state) in &self.split_view_states {
517 self.previous_viewports.insert(
518 *split_id,
519 (
520 view_state.viewport.top_byte,
521 view_state.viewport.width,
522 view_state.viewport.height,
523 ),
524 );
525 }
526
527 self.render_terminal_splits(frame, &split_areas);
529
530 self.cached_layout.split_areas = split_areas;
531 self.cached_layout.horizontal_scrollbar_areas = horizontal_scrollbar_areas;
532 self.cached_layout.tab_layouts = tab_layouts;
533 self.cached_layout.close_split_areas = close_split_areas;
534 self.cached_layout.maximize_split_areas = maximize_split_areas;
535 self.cached_layout.view_line_mappings = view_line_mappings;
536 self.cached_layout.separator_areas = self
537 .split_manager
538 .get_separators_with_ids(editor_content_area);
539 self.cached_layout.editor_content_area = Some(editor_content_area);
540
541 self.render_hover_highlights(frame);
543
544 self.cached_layout.suggestions_area = None;
546 self.file_browser_layout = None;
547
548 let display_name = self
550 .buffer_metadata
551 .get(&self.active_buffer())
552 .map(|m| m.display_name.clone())
553 .unwrap_or_else(|| "[No Name]".to_string());
554 let status_message = self.status_message.clone();
555 let plugin_status_message = self.plugin_status_message.clone();
556 let prompt = self.prompt.clone();
557 let lsp_status = self.lsp_status.clone();
558 let theme = self.theme.clone();
559 let keybindings_cloned = self.keybindings.clone(); let chord_state_cloned = self.chord_state.clone(); let update_available = self.latest_version().map(|v| v.to_string());
564
565 if self.status_bar_visible && !has_suggestions && !has_file_browser {
567 let (warning_level, general_warning_count) =
569 if self.config.warnings.show_status_indicator {
570 (
571 self.get_effective_warning_level(),
572 self.get_general_warning_count(),
573 )
574 } else {
575 (WarningLevel::None, 0)
576 };
577
578 use crate::view::ui::status_bar::StatusBarHover;
580 let status_bar_hover = match &self.mouse_state.hover_target {
581 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
582 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
583 Some(HoverTarget::StatusBarLineEndingIndicator) => {
584 StatusBarHover::LineEndingIndicator
585 }
586 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
587 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
588 _ => StatusBarHover::None,
589 };
590
591 let remote_connection = self.remote_connection_info().map(|s| s.to_string());
593
594 let session_name = self.session_name().map(|s| s.to_string());
596
597 let active_split = self.split_manager.active_split();
598 let active_buf = self.active_buffer();
599 let default_cursors = crate::model::cursor::Cursors::new();
600 let status_cursors = self
601 .split_view_states
602 .get(&active_split)
603 .map(|vs| &vs.cursors)
604 .unwrap_or(&default_cursors);
605 let is_read_only = self
606 .buffer_metadata
607 .get(&active_buf)
608 .map(|m| m.read_only)
609 .unwrap_or(false);
610 let status_bar_layout = StatusBarRenderer::render_status_bar(
611 frame,
612 main_chunks[status_bar_idx],
613 self.buffers.get_mut(&active_buf).unwrap(),
614 status_cursors,
615 &status_message,
616 &plugin_status_message,
617 &lsp_status,
618 &theme,
619 &display_name,
620 &keybindings_cloned, &chord_state_cloned, update_available.as_deref(), warning_level, general_warning_count, status_bar_hover, remote_connection.as_deref(), session_name.as_deref(), is_read_only, );
630
631 let status_bar_area = main_chunks[status_bar_idx];
633 self.cached_layout.status_bar_area =
634 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
635 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
636 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
637 self.cached_layout.status_bar_line_ending_area =
638 status_bar_layout.line_ending_indicator;
639 self.cached_layout.status_bar_encoding_area = status_bar_layout.encoding_indicator;
640 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
641 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
642 }
643
644 if show_search_options {
646 let confirm_each = self.prompt.as_ref().and_then(|p| {
648 if matches!(
649 p.prompt_type,
650 PromptType::ReplaceSearch
651 | PromptType::Replace { .. }
652 | PromptType::QueryReplaceSearch
653 | PromptType::QueryReplace { .. }
654 ) {
655 Some(self.search_confirm_each)
656 } else {
657 None
658 }
659 });
660
661 use crate::view::ui::status_bar::SearchOptionsHover;
663 let search_options_hover = match &self.mouse_state.hover_target {
664 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
665 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
666 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
667 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
668 _ => SearchOptionsHover::None,
669 };
670
671 let search_options_layout = StatusBarRenderer::render_search_options(
672 frame,
673 main_chunks[search_options_idx],
674 self.search_case_sensitive,
675 self.search_whole_word,
676 self.search_use_regex,
677 confirm_each,
678 &theme,
679 &keybindings_cloned,
680 search_options_hover,
681 );
682 self.cached_layout.search_options_layout = Some(search_options_layout);
683 } else {
684 self.cached_layout.search_options_layout = None;
685 }
686
687 if let Some(prompt) = &prompt {
689 if matches!(
691 prompt.prompt_type,
692 crate::view::prompt::PromptType::OpenFile
693 | crate::view::prompt::PromptType::SwitchProject
694 ) {
695 if let Some(file_open_state) = &self.file_open_state {
696 StatusBarRenderer::render_file_open_prompt(
697 frame,
698 main_chunks[prompt_line_idx],
699 prompt,
700 file_open_state,
701 &theme,
702 );
703 } else {
704 StatusBarRenderer::render_prompt(
705 frame,
706 main_chunks[prompt_line_idx],
707 prompt,
708 &theme,
709 );
710 }
711 } else {
712 StatusBarRenderer::render_prompt(
713 frame,
714 main_chunks[prompt_line_idx],
715 prompt,
716 &theme,
717 );
718 }
719 }
720
721 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
724
725 let theme_clone = self.theme.clone();
728 let hover_target = self.mouse_state.hover_target.clone();
729
730 self.cached_layout.popup_areas.clear();
732
733 let popup_info: Vec<_> = {
735 let active_split = self.split_manager.active_split();
737 let viewport = self
738 .split_view_states
739 .get(&active_split)
740 .map(|vs| vs.viewport.clone());
741
742 let content_rect = self
747 .cached_layout
748 .split_areas
749 .iter()
750 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
751 .map(|(_, _, rect, _, _, _)| *rect);
752
753 let primary_cursor = self
754 .split_view_states
755 .get(&active_split)
756 .map(|vs| *vs.cursors.primary());
757 let state = self.active_state_mut();
758 if state.popups.is_visible() {
759 let primary_cursor =
761 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
762
763 let gutter_width = viewport
765 .as_ref()
766 .map(|vp| vp.gutter_width(&state.buffer) as u16)
767 .unwrap_or(0);
768
769 let cursor_screen_pos = viewport
770 .as_ref()
771 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
772 .unwrap_or((0, 0));
773
774 let word_start_screen_pos = {
778 use crate::primitives::word_navigation::find_completion_word_start;
779 let word_start =
780 find_completion_word_start(&state.buffer, primary_cursor.position);
781 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
782 viewport
783 .as_ref()
784 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
785 .unwrap_or((0, 0))
786 };
787
788 let (base_x, base_y) = content_rect
793 .map(|r| (r.x + gutter_width, r.y))
794 .unwrap_or((gutter_width, 1));
795
796 let cursor_screen_pos =
797 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
798 let word_start_screen_pos = (
799 word_start_screen_pos.0 + base_x,
800 word_start_screen_pos.1 + base_y,
801 );
802
803 state
805 .popups
806 .all()
807 .iter()
808 .enumerate()
809 .map(|(popup_idx, popup)| {
810 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
812 (word_start_screen_pos.0, cursor_screen_pos.1)
813 } else {
814 cursor_screen_pos
815 };
816 let popup_area = popup.calculate_area(size, Some(popup_pos));
817
818 let desc_height = popup.description_height();
821 let inner_area = if popup.bordered {
822 ratatui::layout::Rect {
823 x: popup_area.x + 1,
824 y: popup_area.y + 1 + desc_height,
825 width: popup_area.width.saturating_sub(2),
826 height: popup_area.height.saturating_sub(2 + desc_height),
827 }
828 } else {
829 ratatui::layout::Rect {
830 x: popup_area.x,
831 y: popup_area.y + desc_height,
832 width: popup_area.width,
833 height: popup_area.height.saturating_sub(desc_height),
834 }
835 };
836
837 let num_items = match &popup.content {
838 crate::view::popup::PopupContent::List { items, .. } => items.len(),
839 _ => 0,
840 };
841
842 let total_lines = popup.item_count();
844 let visible_lines = inner_area.height as usize;
845 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
846 {
847 Some(ratatui::layout::Rect {
848 x: inner_area.x + inner_area.width - 1,
849 y: inner_area.y,
850 width: 1,
851 height: inner_area.height,
852 })
853 } else {
854 None
855 };
856
857 (
858 popup_idx,
859 popup_area,
860 inner_area,
861 popup.scroll_offset,
862 num_items,
863 scrollbar_rect,
864 total_lines,
865 )
866 })
867 .collect()
868 } else {
869 Vec::new()
870 }
871 };
872
873 self.cached_layout.popup_areas = popup_info.clone();
875
876 let state = self.active_state_mut();
878 if state.popups.is_visible() {
879 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
880 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
881 popup.render_with_hover(
882 frame,
883 *popup_area,
884 &theme_clone,
885 hover_target.as_ref(),
886 );
887 }
888 }
889 }
890
891 self.update_menu_context();
894
895 let settings_visible = self
898 .settings_state
899 .as_ref()
900 .map(|s| s.visible)
901 .unwrap_or(false);
902 if settings_visible {
903 crate::view::dimming::apply_dimming(frame, size);
905 }
906 if let Some(ref mut settings_state) = self.settings_state {
907 if settings_state.visible {
908 settings_state.update_focus_states();
909 let settings_layout = crate::view::settings::render_settings(
910 frame,
911 size,
912 settings_state,
913 &self.theme,
914 );
915 self.cached_layout.settings_layout = Some(settings_layout);
916 }
917 }
918
919 if let Some(ref wizard) = self.calibration_wizard {
921 crate::view::dimming::apply_dimming(frame, size);
923 crate::view::calibration_wizard::render_calibration_wizard(
924 frame,
925 size,
926 wizard,
927 &self.theme,
928 );
929 }
930
931 if let Some(ref mut kb_editor) = self.keybinding_editor {
933 crate::view::dimming::apply_dimming(frame, size);
934 crate::view::keybinding_editor::render_keybinding_editor(
935 frame,
936 size,
937 kb_editor,
938 &self.theme,
939 );
940 }
941
942 if let Some(ref debug) = self.event_debug {
944 crate::view::dimming::apply_dimming(frame, size);
946 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
947 }
948
949 if self.menu_bar_visible {
950 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
951 frame,
952 menu_bar_area,
953 &self.menus,
954 &self.menu_state,
955 &self.keybindings,
956 &self.theme,
957 self.mouse_state.hover_target.as_ref(),
958 ));
959 } else {
960 self.cached_layout.menu_layout = None;
961 }
962
963 if let Some(ref menu) = self.tab_context_menu {
965 self.render_tab_context_menu(frame, menu);
966 }
967
968 self.render_theme_info_popup(frame);
970
971 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
973 if drag_state.is_dragging() {
974 self.render_tab_drop_zone(frame, drag_state);
975 }
976 }
977
978 if self.gpm_active {
984 if let Some((col, row)) = self.mouse_cursor_position {
985 use ratatui::style::Modifier;
986
987 if col < size.width && row < size.height {
989 let buf = frame.buffer_mut();
991 if let Some(cell) = buf.cell_mut((col, row)) {
992 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
993 }
994 }
995 }
996 }
997
998 if self.keyboard_capture && self.terminal_mode {
1001 let active_split = self.split_manager.active_split();
1003 let active_split_area = self
1004 .cached_layout
1005 .split_areas
1006 .iter()
1007 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1008 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1009
1010 if let Some(terminal_area) = active_split_area {
1011 self.apply_keyboard_capture_dimming(frame, terminal_area);
1012 }
1013 }
1014
1015 crate::view::color_support::convert_buffer_colors(
1017 frame.buffer_mut(),
1018 self.color_capability,
1019 );
1020 }
1021
1022 fn render_quick_open_hints(
1024 frame: &mut Frame,
1025 area: ratatui::layout::Rect,
1026 theme: &crate::view::theme::Theme,
1027 ) {
1028 use ratatui::style::{Modifier, Style};
1029 use ratatui::text::{Line, Span};
1030 use ratatui::widgets::Paragraph;
1031 use rust_i18n::t;
1032
1033 let hints_style = Style::default()
1034 .fg(theme.line_number_fg)
1035 .bg(theme.suggestion_selected_bg)
1036 .add_modifier(Modifier::DIM);
1037 let hints_text = t!("quick_open.mode_hints");
1038 let left_margin = 2;
1040 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1041 let mut spans = Vec::new();
1042 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1043 spans.push(Span::styled(hints_text.to_string(), hints_style));
1044 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1045 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1046
1047 let paragraph = Paragraph::new(Line::from(spans));
1048 frame.render_widget(paragraph, area);
1049 }
1050
1051 fn apply_keyboard_capture_dimming(
1054 &self,
1055 frame: &mut Frame,
1056 terminal_area: ratatui::layout::Rect,
1057 ) {
1058 let size = frame.area();
1059 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1060 }
1061
1062 fn render_prompt_popups(
1065 &mut self,
1066 frame: &mut Frame,
1067 prompt_area: ratatui::layout::Rect,
1068 width: u16,
1069 ) {
1070 let Some(prompt) = &self.prompt else { return };
1071
1072 if matches!(
1073 prompt.prompt_type,
1074 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1075 ) {
1076 let Some(file_open_state) = &self.file_open_state else {
1077 return;
1078 };
1079 let max_height = prompt_area.y.saturating_sub(1).min(20);
1080 let popup_area = ratatui::layout::Rect {
1081 x: 0,
1082 y: prompt_area.y.saturating_sub(max_height),
1083 width,
1084 height: max_height,
1085 };
1086 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1087 frame,
1088 popup_area,
1089 file_open_state,
1090 &self.theme,
1091 &self.mouse_state.hover_target,
1092 Some(&self.keybindings),
1093 );
1094 return;
1095 }
1096
1097 if prompt.suggestions.is_empty() {
1098 return;
1099 }
1100
1101 let suggestion_count = prompt.suggestions.len().min(10);
1102 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1103 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1104 let height = suggestion_count as u16 + 2 + hints_height;
1105
1106 let suggestions_area = ratatui::layout::Rect {
1107 x: 0,
1108 y: prompt_area.y.saturating_sub(height),
1109 width,
1110 height: height - hints_height,
1111 };
1112
1113 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1114
1115 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
1116 frame,
1117 suggestions_area,
1118 prompt,
1119 &self.theme,
1120 self.mouse_state.hover_target.as_ref(),
1121 );
1122
1123 if is_quick_open {
1124 let hints_area = ratatui::layout::Rect {
1125 x: 0,
1126 y: prompt_area.y.saturating_sub(hints_height),
1127 width,
1128 height: hints_height,
1129 };
1130 frame.render_widget(ratatui::widgets::Clear, hints_area);
1131 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1132 }
1133 }
1134
1135 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
1137 use ratatui::style::Style;
1138 use ratatui::text::Span;
1139 use ratatui::widgets::Paragraph;
1140
1141 match &self.mouse_state.hover_target {
1142 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
1143 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
1145 if sid == split_id && dir == direction {
1146 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1147 match dir {
1148 SplitDirection::Horizontal => {
1149 let line_text = "─".repeat(*length as usize);
1150 let paragraph =
1151 Paragraph::new(Span::styled(line_text, hover_style));
1152 frame.render_widget(
1153 paragraph,
1154 ratatui::layout::Rect::new(*x, *y, *length, 1),
1155 );
1156 }
1157 SplitDirection::Vertical => {
1158 for offset in 0..*length {
1159 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1160 frame.render_widget(
1161 paragraph,
1162 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
1163 );
1164 }
1165 }
1166 }
1167 }
1168 }
1169 }
1170 Some(HoverTarget::ScrollbarThumb(split_id)) => {
1171 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1173 &self.cached_layout.split_areas
1174 {
1175 if sid == split_id {
1176 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
1177 for row_offset in *thumb_start..*thumb_end {
1178 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
1179 frame.render_widget(
1180 paragraph,
1181 ratatui::layout::Rect::new(
1182 scrollbar_rect.x,
1183 scrollbar_rect.y + row_offset as u16,
1184 1,
1185 1,
1186 ),
1187 );
1188 }
1189 }
1190 }
1191 }
1192 Some(HoverTarget::ScrollbarTrack(split_id)) => {
1193 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1195 &self.cached_layout.split_areas
1196 {
1197 if sid == split_id {
1198 let track_hover_style =
1199 Style::default().bg(self.theme.scrollbar_track_hover_fg);
1200 let thumb_style = Style::default().bg(self.theme.scrollbar_thumb_fg);
1201 for row_offset in 0..scrollbar_rect.height {
1202 let is_thumb = (row_offset as usize) >= *thumb_start
1203 && (row_offset as usize) < *thumb_end;
1204 let style = if is_thumb {
1205 thumb_style
1206 } else {
1207 track_hover_style
1208 };
1209 let paragraph = Paragraph::new(Span::styled(" ", style));
1210 frame.render_widget(
1211 paragraph,
1212 ratatui::layout::Rect::new(
1213 scrollbar_rect.x,
1214 scrollbar_rect.y + row_offset,
1215 1,
1216 1,
1217 ),
1218 );
1219 }
1220 }
1221 }
1222 }
1223 Some(HoverTarget::FileExplorerBorder) => {
1224 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1226 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1227 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1228 for row_offset in 0..explorer_area.height {
1229 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1230 frame.render_widget(
1231 paragraph,
1232 ratatui::layout::Rect::new(
1233 border_x,
1234 explorer_area.y + row_offset,
1235 1,
1236 1,
1237 ),
1238 );
1239 }
1240 }
1241 }
1242 _ => {}
1244 }
1245 }
1246
1247 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
1249 use ratatui::style::Style;
1250 use ratatui::text::{Line, Span};
1251 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1252
1253 let items = super::types::TabContextMenuItem::all();
1254 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
1259 let screen_height = frame.area().height;
1260
1261 let menu_x = if menu.position.0 + menu_width > screen_width {
1262 screen_width.saturating_sub(menu_width)
1263 } else {
1264 menu.position.0
1265 };
1266
1267 let menu_y = if menu.position.1 + menu_height > screen_height {
1268 screen_height.saturating_sub(menu_height)
1269 } else {
1270 menu.position.1
1271 };
1272
1273 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1274
1275 frame.render_widget(Clear, area);
1277
1278 let mut lines = Vec::new();
1280 for (idx, item) in items.iter().enumerate() {
1281 let is_highlighted = idx == menu.highlighted;
1282
1283 let style = if is_highlighted {
1284 Style::default()
1285 .fg(self.theme.menu_highlight_fg)
1286 .bg(self.theme.menu_highlight_bg)
1287 } else {
1288 Style::default()
1289 .fg(self.theme.menu_dropdown_fg)
1290 .bg(self.theme.menu_dropdown_bg)
1291 };
1292
1293 let label = item.label();
1295 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1297
1298 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1299 }
1300
1301 let block = Block::default()
1302 .borders(Borders::ALL)
1303 .border_style(Style::default().fg(self.theme.menu_border_fg))
1304 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1305
1306 let paragraph = Paragraph::new(lines).block(block);
1307 frame.render_widget(paragraph, area);
1308 }
1309
1310 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
1312 use ratatui::style::Modifier;
1313
1314 let Some(ref drop_zone) = drag_state.drop_zone else {
1315 return;
1316 };
1317
1318 let split_id = drop_zone.split_id();
1319
1320 let split_area = self
1322 .cached_layout
1323 .split_areas
1324 .iter()
1325 .find(|(sid, _, _, _, _, _)| *sid == split_id)
1326 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1327
1328 let Some(content_rect) = split_area else {
1329 return;
1330 };
1331
1332 use super::types::TabDropZone;
1334
1335 let highlight_area = match drop_zone {
1336 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
1337 content_rect
1340 }
1341 TabDropZone::SplitLeft(_) => {
1342 let width = (content_rect.width / 2).max(3);
1344 ratatui::layout::Rect::new(
1345 content_rect.x,
1346 content_rect.y,
1347 width,
1348 content_rect.height,
1349 )
1350 }
1351 TabDropZone::SplitRight(_) => {
1352 let width = (content_rect.width / 2).max(3);
1354 let x = content_rect.x + content_rect.width - width;
1355 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
1356 }
1357 TabDropZone::SplitTop(_) => {
1358 let height = (content_rect.height / 2).max(2);
1360 ratatui::layout::Rect::new(
1361 content_rect.x,
1362 content_rect.y,
1363 content_rect.width,
1364 height,
1365 )
1366 }
1367 TabDropZone::SplitBottom(_) => {
1368 let height = (content_rect.height / 2).max(2);
1370 let y = content_rect.y + content_rect.height - height;
1371 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
1372 }
1373 };
1374
1375 let buf = frame.buffer_mut();
1378 let drop_zone_bg = self.theme.tab_drop_zone_bg;
1379 let drop_zone_border = self.theme.tab_drop_zone_border;
1380
1381 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1383 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1384 if let Some(cell) = buf.cell_mut((x, y)) {
1385 cell.set_bg(drop_zone_bg);
1388
1389 let is_border = x == highlight_area.x
1391 || x == highlight_area.x + highlight_area.width - 1
1392 || y == highlight_area.y
1393 || y == highlight_area.y + highlight_area.height - 1;
1394
1395 if is_border {
1396 cell.set_fg(drop_zone_border);
1397 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
1398 }
1399 }
1400 }
1401 }
1402
1403 match drop_zone {
1405 TabDropZone::SplitLeft(_) => {
1406 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1408 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
1409 cell.set_symbol("▌");
1410 cell.set_fg(drop_zone_border);
1411 }
1412 }
1413 }
1414 TabDropZone::SplitRight(_) => {
1415 let x = highlight_area.x + highlight_area.width - 1;
1417 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1418 if let Some(cell) = buf.cell_mut((x, y)) {
1419 cell.set_symbol("▐");
1420 cell.set_fg(drop_zone_border);
1421 }
1422 }
1423 }
1424 TabDropZone::SplitTop(_) => {
1425 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1427 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
1428 cell.set_symbol("▀");
1429 cell.set_fg(drop_zone_border);
1430 }
1431 }
1432 }
1433 TabDropZone::SplitBottom(_) => {
1434 let y = highlight_area.y + highlight_area.height - 1;
1436 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1437 if let Some(cell) = buf.cell_mut((x, y)) {
1438 cell.set_symbol("▄");
1439 cell.set_fg(drop_zone_border);
1440 }
1441 }
1442 }
1443 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
1444 }
1446 }
1447 }
1448
1449 pub fn add_overlay(
1453 &mut self,
1454 namespace: Option<crate::view::overlay::OverlayNamespace>,
1455 range: Range<usize>,
1456 face: crate::model::event::OverlayFace,
1457 priority: i32,
1458 message: Option<String>,
1459 ) -> crate::view::overlay::OverlayHandle {
1460 let event = Event::AddOverlay {
1461 namespace,
1462 range,
1463 face,
1464 priority,
1465 message,
1466 extend_to_line_end: false,
1467 url: None,
1468 };
1469 self.apply_event_to_active_buffer(&event);
1470 let state = self.active_state();
1472 state
1473 .overlays
1474 .all()
1475 .last()
1476 .map(|o| o.handle.clone())
1477 .unwrap_or_default()
1478 }
1479
1480 pub fn remove_overlay(&mut self, handle: crate::view::overlay::OverlayHandle) {
1482 let event = Event::RemoveOverlay { handle };
1483 self.apply_event_to_active_buffer(&event);
1484 }
1485
1486 pub fn remove_overlays_in_range(&mut self, range: Range<usize>) {
1488 let event = Event::RemoveOverlaysInRange { range };
1489 self.active_event_log_mut().append(event.clone());
1490 self.apply_event_to_active_buffer(&event);
1491 }
1492
1493 pub fn clear_overlays(&mut self) {
1495 let event = Event::ClearOverlays;
1496 self.active_event_log_mut().append(event.clone());
1497 self.apply_event_to_active_buffer(&event);
1498 }
1499
1500 pub fn show_popup(&mut self, popup: crate::model::event::PopupData) {
1504 let event = Event::ShowPopup { popup };
1505 self.active_event_log_mut().append(event.clone());
1506 self.apply_event_to_active_buffer(&event);
1507 }
1508
1509 pub fn hide_popup(&mut self) {
1511 let event = Event::HidePopup;
1512 self.active_event_log_mut().append(event.clone());
1513 self.apply_event_to_active_buffer(&event);
1514
1515 let active = self.active_buffer();
1517 if let Some((wait_id, true)) = self.wait_tracking.remove(&active) {
1518 self.completed_waits.push(wait_id);
1519 }
1520
1521 if let Some(handle) = self.hover_symbol_overlay.take() {
1523 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1524 self.apply_event_to_active_buffer(&remove_overlay_event);
1525 }
1526 self.hover_symbol_range = None;
1527 }
1528
1529 pub(super) fn dismiss_transient_popups(&mut self) {
1532 let is_transient_popup = self
1533 .active_state()
1534 .popups
1535 .top()
1536 .is_some_and(|p| p.transient);
1537
1538 if is_transient_popup {
1539 self.hide_popup();
1540 tracing::trace!("Dismissed transient popup");
1541 }
1542 }
1543
1544 pub(super) fn scroll_popup(&mut self, delta: i32) {
1547 if let Some(popup) = self.active_state_mut().popups.top_mut() {
1548 popup.scroll_by(delta);
1549 tracing::debug!(
1550 "Scrolled popup by {}, new offset: {}",
1551 delta,
1552 popup.scroll_offset
1553 );
1554 }
1555 }
1556
1557 pub(super) fn on_editor_focus_lost(&mut self) {
1565 self.active_state_mut().on_focus_lost();
1567
1568 self.mouse_state.lsp_hover_state = None;
1570 self.mouse_state.lsp_hover_request_sent = false;
1571 self.pending_hover_request = None;
1572
1573 if let Some(handle) = self.hover_symbol_overlay.take() {
1575 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1576 self.apply_event_to_active_buffer(&remove_overlay_event);
1577 }
1578 self.hover_symbol_range = None;
1579 }
1580
1581 pub fn clear_popups(&mut self) {
1583 let event = Event::ClearPopups;
1584 self.active_event_log_mut().append(event.clone());
1585 self.apply_event_to_active_buffer(&event);
1586 }
1587
1588 pub fn show_lsp_confirmation_popup(&mut self, language: &str) {
1595 use crate::model::event::{
1596 PopupContentData, PopupData, PopupKindHint, PopupListItemData, PopupPositionData,
1597 };
1598
1599 self.pending_lsp_confirmation = Some(language.to_string());
1601
1602 let server_info = if let Some(lsp) = &self.lsp {
1604 if let Some(config) = lsp.get_config(language) {
1605 if !config.command.is_empty() {
1606 format!("{} ({})", language, config.command)
1607 } else {
1608 language.to_string()
1609 }
1610 } else {
1611 language.to_string()
1612 }
1613 } else {
1614 language.to_string()
1615 };
1616
1617 let popup = PopupData {
1618 kind: PopupKindHint::List,
1619 title: Some(format!("Start LSP Server: {}?", server_info)),
1620 description: None,
1621 transient: false,
1622 content: PopupContentData::List {
1623 items: vec![
1624 PopupListItemData {
1625 text: "Allow this time".to_string(),
1626 detail: Some("Start the LSP server for this session".to_string()),
1627 icon: None,
1628 data: Some("allow_once".to_string()),
1629 },
1630 PopupListItemData {
1631 text: "Always allow".to_string(),
1632 detail: Some("Always start this LSP server automatically".to_string()),
1633 icon: None,
1634 data: Some("allow_always".to_string()),
1635 },
1636 PopupListItemData {
1637 text: "Don't start".to_string(),
1638 detail: Some("Cancel LSP server startup".to_string()),
1639 icon: None,
1640 data: Some("deny".to_string()),
1641 },
1642 ],
1643 selected: 0,
1644 },
1645 position: PopupPositionData::Centered,
1646 width: 50,
1647 max_height: 8,
1648 bordered: true,
1649 };
1650
1651 self.show_popup(popup);
1652 }
1653
1654 pub fn handle_lsp_confirmation_response(&mut self, action: &str) -> bool {
1662 let Some(language) = self.pending_lsp_confirmation.take() else {
1663 return false;
1664 };
1665
1666 match action {
1667 "allow_once" => {
1668 if let Some(lsp) = &mut self.lsp {
1670 lsp.allow_language(&language);
1672 if lsp.force_spawn(&language).is_some() {
1674 tracing::info!("LSP server for {} started (allowed once)", language);
1675 self.set_status_message(
1676 t!("lsp.server_started", language = language).to_string(),
1677 );
1678 } else {
1679 self.set_status_message(
1680 t!("lsp.failed_to_start", language = language).to_string(),
1681 );
1682 }
1683 }
1684 self.notify_lsp_current_file_opened(&language);
1686 }
1687 "allow_always" => {
1688 if let Some(lsp) = &mut self.lsp {
1690 lsp.allow_language(&language);
1691 if lsp.force_spawn(&language).is_some() {
1693 tracing::info!("LSP server for {} started (always allowed)", language);
1694 self.set_status_message(
1695 t!("lsp.server_started_auto", language = language).to_string(),
1696 );
1697 } else {
1698 self.set_status_message(
1699 t!("lsp.failed_to_start", language = language).to_string(),
1700 );
1701 }
1702 }
1703 self.notify_lsp_current_file_opened(&language);
1705 }
1706 _ => {
1707 tracing::info!("LSP server for {} startup declined by user", language);
1709 self.set_status_message(
1710 t!("lsp.startup_cancelled", language = language).to_string(),
1711 );
1712 }
1713 }
1714
1715 true
1716 }
1717
1718 fn notify_lsp_current_file_opened(&mut self, language: &str) {
1723 let metadata = match self.buffer_metadata.get(&self.active_buffer()) {
1725 Some(m) => m,
1726 None => {
1727 tracing::debug!(
1728 "notify_lsp_current_file_opened: no metadata for buffer {:?}",
1729 self.active_buffer()
1730 );
1731 return;
1732 }
1733 };
1734
1735 if !metadata.lsp_enabled {
1736 tracing::debug!("notify_lsp_current_file_opened: LSP disabled for this buffer");
1737 return;
1738 }
1739
1740 let uri = match metadata.file_uri() {
1742 Some(u) => u.clone(),
1743 None => {
1744 tracing::debug!(
1745 "notify_lsp_current_file_opened: no URI for buffer (not a file or URI creation failed)"
1746 );
1747 return;
1748 }
1749 };
1750
1751 let active_buffer = self.active_buffer();
1753
1754 let file_language = match self.buffers.get(&active_buffer).map(|s| s.language.clone()) {
1756 Some(l) => l,
1757 None => {
1758 tracing::debug!("notify_lsp_current_file_opened: no buffer state");
1759 return;
1760 }
1761 };
1762
1763 if file_language != language {
1765 tracing::debug!(
1766 "notify_lsp_current_file_opened: file language {} doesn't match server {}",
1767 file_language,
1768 language
1769 );
1770 return;
1771 }
1772 let (text, line_count) = if let Some(state) = self.buffers.get(&active_buffer) {
1773 let text = match state.buffer.to_string() {
1774 Some(t) => t,
1775 None => {
1776 tracing::debug!("notify_lsp_current_file_opened: buffer not fully loaded");
1777 return;
1778 }
1779 };
1780 let line_count = state.buffer.line_count().unwrap_or(1000);
1781 (text, line_count)
1782 } else {
1783 tracing::debug!("notify_lsp_current_file_opened: no buffer state");
1784 return;
1785 };
1786
1787 if let Some(lsp) = &mut self.lsp {
1789 if let Some(client) = lsp.force_spawn(language) {
1790 tracing::info!("Sending didOpen to newly started LSP for: {}", uri.as_str());
1791 if let Err(e) = client.did_open(uri.clone(), text, file_language) {
1792 tracing::warn!("Failed to send didOpen to LSP: {}", e);
1793 } else {
1794 tracing::info!("Successfully sent didOpen to LSP after confirmation");
1795
1796 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
1798 let request_id = self.next_lsp_request_id;
1799 self.next_lsp_request_id += 1;
1800
1801 if let Err(e) =
1802 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
1803 {
1804 tracing::debug!(
1805 "Failed to request pull diagnostics (server may not support): {}",
1806 e
1807 );
1808 }
1809
1810 if self.config.editor.enable_inlay_hints {
1812 let request_id = self.next_lsp_request_id;
1813 self.next_lsp_request_id += 1;
1814 self.pending_inlay_hints_request = Some(request_id);
1815
1816 let last_line = line_count.saturating_sub(1) as u32;
1817 let last_char = 10000u32;
1818
1819 if let Err(e) =
1820 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
1821 {
1822 tracing::debug!(
1823 "Failed to request inlay hints (server may not support): {}",
1824 e
1825 );
1826 self.pending_inlay_hints_request = None;
1827 }
1828 }
1829 }
1830 }
1831 }
1832 }
1833
1834 pub fn has_pending_lsp_confirmation(&self) -> bool {
1836 self.pending_lsp_confirmation.is_some()
1837 }
1838
1839 pub fn popup_select_next(&mut self) {
1841 let event = Event::PopupSelectNext;
1842 self.active_event_log_mut().append(event.clone());
1843 self.apply_event_to_active_buffer(&event);
1844 }
1845
1846 pub fn popup_select_prev(&mut self) {
1848 let event = Event::PopupSelectPrev;
1849 self.active_event_log_mut().append(event.clone());
1850 self.apply_event_to_active_buffer(&event);
1851 }
1852
1853 pub fn popup_page_down(&mut self) {
1855 let event = Event::PopupPageDown;
1856 self.active_event_log_mut().append(event.clone());
1857 self.apply_event_to_active_buffer(&event);
1858 }
1859
1860 pub fn popup_page_up(&mut self) {
1862 let event = Event::PopupPageUp;
1863 self.active_event_log_mut().append(event.clone());
1864 self.apply_event_to_active_buffer(&event);
1865 }
1866
1867 pub(super) fn collect_lsp_changes(&self, event: &Event) -> Vec<TextDocumentContentChangeEvent> {
1873 match event {
1874 Event::Insert { position, text, .. } => {
1875 tracing::trace!(
1876 "collect_lsp_changes: processing Insert at position {}",
1877 position
1878 );
1879 let (line, character) = self
1881 .active_state()
1882 .buffer
1883 .position_to_lsp_position(*position);
1884 let lsp_pos = Position::new(line as u32, character as u32);
1885 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
1886 vec![TextDocumentContentChangeEvent {
1887 range: Some(lsp_range),
1888 range_length: None,
1889 text: text.clone(),
1890 }]
1891 }
1892 Event::Delete { range, .. } => {
1893 tracing::trace!("collect_lsp_changes: processing Delete range {:?}", range);
1894 let (start_line, start_char) = self
1896 .active_state()
1897 .buffer
1898 .position_to_lsp_position(range.start);
1899 let (end_line, end_char) = self
1900 .active_state()
1901 .buffer
1902 .position_to_lsp_position(range.end);
1903 let lsp_range = LspRange::new(
1904 Position::new(start_line as u32, start_char as u32),
1905 Position::new(end_line as u32, end_char as u32),
1906 );
1907 vec![TextDocumentContentChangeEvent {
1908 range: Some(lsp_range),
1909 range_length: None,
1910 text: String::new(),
1911 }]
1912 }
1913 Event::Batch { events, .. } => {
1914 tracing::trace!(
1917 "collect_lsp_changes: processing Batch with {} events",
1918 events.len()
1919 );
1920 let mut all_changes = Vec::new();
1921 for sub_event in events {
1922 all_changes.extend(self.collect_lsp_changes(sub_event));
1923 }
1924 all_changes
1925 }
1926 _ => Vec::new(), }
1928 }
1929
1930 pub(super) fn calculate_event_line_info(&self, event: &Event) -> super::types::EventLineInfo {
1952 match event {
1953 Event::Insert { position, text, .. } => {
1954 let start_line = self.active_state().buffer.get_line_number(*position);
1956
1957 let lines_added = text.matches('\n').count();
1959 let end_line = start_line + lines_added;
1960
1961 super::types::EventLineInfo {
1962 start_line,
1963 end_line,
1964 line_delta: lines_added as i32,
1965 }
1966 }
1967 Event::Delete {
1968 range,
1969 deleted_text,
1970 ..
1971 } => {
1972 let start_line = self.active_state().buffer.get_line_number(range.start);
1974 let end_line = self.active_state().buffer.get_line_number(range.end);
1975
1976 let lines_removed = deleted_text.matches('\n').count();
1978
1979 super::types::EventLineInfo {
1980 start_line,
1981 end_line,
1982 line_delta: -(lines_removed as i32),
1983 }
1984 }
1985 Event::Batch { events, .. } => {
1986 let mut min_line = usize::MAX;
1989 let mut max_line = 0usize;
1990 let mut total_delta = 0i32;
1991
1992 for sub_event in events {
1993 let info = self.calculate_event_line_info(sub_event);
1994 min_line = min_line.min(info.start_line);
1995 max_line = max_line.max(info.end_line);
1996 total_delta += info.line_delta;
1997 }
1998
1999 if min_line == usize::MAX {
2000 min_line = 0;
2001 }
2002
2003 super::types::EventLineInfo {
2004 start_line: min_line,
2005 end_line: max_line,
2006 line_delta: total_delta,
2007 }
2008 }
2009 _ => super::types::EventLineInfo::default(),
2010 }
2011 }
2012
2013 pub(super) fn notify_lsp_save(&mut self) {
2015 let buffer_id = self.active_buffer();
2016 self.notify_lsp_save_buffer(buffer_id);
2017 }
2018
2019 pub(super) fn notify_lsp_save_buffer(&mut self, buffer_id: BufferId) {
2021 let metadata = match self.buffer_metadata.get(&buffer_id) {
2023 Some(m) => m,
2024 None => {
2025 tracing::debug!(
2026 "notify_lsp_save_buffer: no metadata for buffer {:?}",
2027 buffer_id
2028 );
2029 return;
2030 }
2031 };
2032
2033 if !metadata.lsp_enabled {
2034 tracing::debug!(
2035 "notify_lsp_save_buffer: LSP disabled for buffer {:?}",
2036 buffer_id
2037 );
2038 return;
2039 }
2040
2041 let uri = match metadata.file_uri() {
2043 Some(u) => u.clone(),
2044 None => {
2045 tracing::debug!("notify_lsp_save_buffer: no URI for buffer {:?}", buffer_id);
2046 return;
2047 }
2048 };
2049
2050 let language = match self
2053 .buffers
2054 .get(&self.active_buffer())
2055 .map(|s| s.language.clone())
2056 {
2057 Some(l) => l,
2058 None => {
2059 tracing::debug!("notify_lsp_save: no buffer state");
2060 return;
2061 }
2062 };
2063
2064 let full_text = match self.active_state().buffer.to_string() {
2066 Some(t) => t,
2067 None => {
2068 tracing::debug!("notify_lsp_save: buffer not fully loaded");
2069 return;
2070 }
2071 };
2072 tracing::debug!(
2073 "notify_lsp_save: sending didSave to {} (text length: {} bytes)",
2074 uri.as_str(),
2075 full_text.len()
2076 );
2077
2078 if let Some(lsp) = &mut self.lsp {
2080 use crate::services::lsp::manager::LspSpawnResult;
2081 if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
2082 tracing::debug!(
2083 "notify_lsp_save: LSP not running for {} (auto_start disabled)",
2084 language
2085 );
2086 return;
2087 }
2088 if let Some(client) = lsp.get_handle_mut(&language) {
2089 if let Err(e) = client.did_save(uri, Some(full_text)) {
2091 tracing::warn!("Failed to send didSave to LSP: {}", e);
2092 } else {
2093 tracing::info!("Successfully sent didSave to LSP");
2094 }
2095 } else {
2096 tracing::warn!("notify_lsp_save: failed to get LSP client for {}", language);
2097 }
2098 } else {
2099 tracing::debug!("notify_lsp_save: no LSP manager available");
2100 }
2101 }
2102
2103 pub fn action_to_events(&mut self, action: Action) -> Option<Vec<Event>> {
2106 let auto_indent = self.config.editor.auto_indent;
2107 let estimated_line_length = self.config.editor.estimated_line_length;
2108
2109 let active_split = self.split_manager.active_split();
2111 let viewport_height = self
2112 .split_view_states
2113 .get(&active_split)
2114 .map(|vs| vs.viewport.height)
2115 .unwrap_or(24);
2116
2117 if let Some(events) =
2121 self.handle_visual_line_movement(&action, active_split, estimated_line_length)
2122 {
2123 return Some(events);
2124 }
2125
2126 let buffer_id = self.active_buffer();
2127 let state = self.buffers.get_mut(&buffer_id).unwrap();
2128
2129 let tab_size = state.buffer_settings.tab_size;
2131 let auto_close = state.buffer_settings.auto_close;
2132 let auto_surround = state.buffer_settings.auto_surround;
2133
2134 let cursors = &mut self
2135 .split_view_states
2136 .get_mut(&active_split)
2137 .unwrap()
2138 .cursors;
2139 convert_action_to_events(
2140 state,
2141 cursors,
2142 action,
2143 tab_size,
2144 auto_indent,
2145 auto_close,
2146 auto_surround,
2147 estimated_line_length,
2148 viewport_height,
2149 )
2150 }
2151
2152 fn handle_visual_line_movement(
2155 &mut self,
2156 action: &Action,
2157 split_id: LeafId,
2158 _estimated_line_length: usize,
2159 ) -> Option<Vec<Event>> {
2160 enum VisualAction {
2162 UpDown { direction: i8, is_select: bool },
2163 LineEnd { is_select: bool },
2164 LineStart { is_select: bool },
2165 }
2166
2167 let visual_action = match action {
2170 Action::MoveUp => VisualAction::UpDown {
2171 direction: -1,
2172 is_select: false,
2173 },
2174 Action::MoveDown => VisualAction::UpDown {
2175 direction: 1,
2176 is_select: false,
2177 },
2178 Action::SelectUp => VisualAction::UpDown {
2179 direction: -1,
2180 is_select: true,
2181 },
2182 Action::SelectDown => VisualAction::UpDown {
2183 direction: 1,
2184 is_select: true,
2185 },
2186 Action::MoveLineEnd if self.config.editor.line_wrap => {
2190 VisualAction::LineEnd { is_select: false }
2191 }
2192 Action::SelectLineEnd if self.config.editor.line_wrap => {
2193 VisualAction::LineEnd { is_select: true }
2194 }
2195 Action::MoveLineStart if self.config.editor.line_wrap => {
2196 VisualAction::LineStart { is_select: false }
2197 }
2198 Action::SelectLineStart if self.config.editor.line_wrap => {
2199 VisualAction::LineStart { is_select: true }
2200 }
2201 _ => return None, };
2203
2204 let cursor_data: Vec<_> = {
2206 let active_split = self.split_manager.active_split();
2207 let active_buffer = self.split_manager.active_buffer_id().unwrap();
2208 let cursors = &self.split_view_states.get(&active_split).unwrap().cursors;
2209 let state = self.buffers.get(&active_buffer).unwrap();
2210 cursors
2211 .iter()
2212 .map(|(cursor_id, cursor)| {
2213 let at_line_ending = if cursor.position < state.buffer.len() {
2217 let bytes = state
2218 .buffer
2219 .slice_bytes(cursor.position..cursor.position + 1);
2220 bytes.first() == Some(&b'\n') || bytes.first() == Some(&b'\r')
2221 } else {
2222 true };
2224 let at_line_start = if cursor.position == 0 {
2225 true
2226 } else {
2227 let prev = state
2228 .buffer
2229 .slice_bytes(cursor.position - 1..cursor.position);
2230 prev.first() == Some(&b'\n')
2231 };
2232 (
2233 cursor_id,
2234 cursor.position,
2235 cursor.anchor,
2236 cursor.sticky_column,
2237 cursor.deselect_on_move,
2238 at_line_ending,
2239 at_line_start,
2240 )
2241 })
2242 .collect()
2243 };
2244
2245 let mut events = Vec::new();
2246
2247 for (
2248 cursor_id,
2249 position,
2250 anchor,
2251 sticky_column,
2252 deselect_on_move,
2253 at_line_ending,
2254 at_line_start,
2255 ) in cursor_data
2256 {
2257 let (new_pos, new_sticky) = match &visual_action {
2258 VisualAction::UpDown { direction, .. } => {
2259 let current_visual_col = self
2261 .cached_layout
2262 .byte_to_visual_column(split_id, position)?;
2263
2264 let goal_visual_col = if sticky_column > 0 {
2265 sticky_column
2266 } else {
2267 current_visual_col
2268 };
2269
2270 match self.cached_layout.move_visual_line(
2271 split_id,
2272 position,
2273 goal_visual_col,
2274 *direction,
2275 ) {
2276 Some(result) => result,
2277 None => continue, }
2279 }
2280 VisualAction::LineEnd { .. } => {
2281 let allow_advance = !at_line_ending;
2283 match self
2284 .cached_layout
2285 .visual_line_end(split_id, position, allow_advance)
2286 {
2287 Some(end_pos) => (end_pos, 0),
2288 None => return None,
2289 }
2290 }
2291 VisualAction::LineStart { .. } => {
2292 let allow_advance = !at_line_start;
2294 match self
2295 .cached_layout
2296 .visual_line_start(split_id, position, allow_advance)
2297 {
2298 Some(start_pos) => (start_pos, 0),
2299 None => return None,
2300 }
2301 }
2302 };
2303
2304 let is_select = match &visual_action {
2305 VisualAction::UpDown { is_select, .. } => *is_select,
2306 VisualAction::LineEnd { is_select } => *is_select,
2307 VisualAction::LineStart { is_select } => *is_select,
2308 };
2309
2310 let new_anchor = if is_select {
2311 Some(anchor.unwrap_or(position))
2312 } else if deselect_on_move {
2313 None
2314 } else {
2315 anchor
2316 };
2317
2318 events.push(Event::MoveCursor {
2319 cursor_id,
2320 old_position: position,
2321 new_position: new_pos,
2322 old_anchor: anchor,
2323 new_anchor,
2324 old_sticky_column: sticky_column,
2325 new_sticky_column: new_sticky,
2326 });
2327 }
2328
2329 if events.is_empty() {
2330 None } else {
2332 Some(events)
2333 }
2334 }
2335
2336 pub(super) fn clear_search_highlights(&mut self) {
2340 self.clear_search_overlays();
2341 self.search_state = None;
2343 }
2344
2345 pub(super) fn clear_search_overlays(&mut self) {
2348 let ns = self.search_namespace.clone();
2349 let state = self.active_state_mut();
2350 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2351 }
2352
2353 pub(super) fn update_search_highlights(&mut self, query: &str) {
2356 if query.is_empty() {
2358 self.clear_search_highlights();
2359 return;
2360 }
2361
2362 let search_bg = self.theme.search_match_bg;
2364 let search_fg = self.theme.search_match_fg;
2365 let case_sensitive = self.search_case_sensitive;
2366 let whole_word = self.search_whole_word;
2367 let use_regex = self.search_use_regex;
2368 let ns = self.search_namespace.clone();
2369
2370 let regex_pattern = if use_regex {
2372 if whole_word {
2373 format!(r"\b{}\b", query)
2374 } else {
2375 query.to_string()
2376 }
2377 } else {
2378 let escaped = regex::escape(query);
2379 if whole_word {
2380 format!(r"\b{}\b", escaped)
2381 } else {
2382 escaped
2383 }
2384 };
2385
2386 let regex = regex::RegexBuilder::new(®ex_pattern)
2388 .case_insensitive(!case_sensitive)
2389 .build();
2390
2391 let regex = match regex {
2392 Ok(r) => r,
2393 Err(_) => {
2394 self.clear_search_highlights();
2396 return;
2397 }
2398 };
2399
2400 let active_split = self.split_manager.active_split();
2402 let (top_byte, visible_height) = self
2403 .split_view_states
2404 .get(&active_split)
2405 .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2406 .unwrap_or((0, 20));
2407
2408 let state = self.active_state_mut();
2409
2410 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2412
2413 let visible_start = top_byte;
2415 let mut visible_end = top_byte;
2416
2417 {
2418 let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2419 for _ in 0..visible_height {
2420 if let Some((line_start, line_content)) = line_iter.next_line() {
2421 visible_end = line_start + line_content.len();
2422 } else {
2423 break;
2424 }
2425 }
2426 }
2427
2428 visible_end = visible_end.min(state.buffer.len());
2430
2431 let visible_text = state.get_text_range(visible_start, visible_end);
2433
2434 for mat in regex.find_iter(&visible_text) {
2436 let absolute_pos = visible_start + mat.start();
2437 let match_len = mat.end() - mat.start();
2438
2439 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2441 let overlay = crate::view::overlay::Overlay::with_namespace(
2442 &mut state.marker_list,
2443 absolute_pos..(absolute_pos + match_len),
2444 crate::view::overlay::OverlayFace::Style {
2445 style: search_style,
2446 },
2447 ns.clone(),
2448 )
2449 .with_priority_value(10); state.overlays.add(overlay);
2452 }
2453 }
2454
2455 fn build_search_regex(&self, query: &str) -> Result<regex::Regex, String> {
2457 let regex_pattern = if self.search_use_regex {
2458 if self.search_whole_word {
2459 format!(r"\b{}\b", query)
2460 } else {
2461 query.to_string()
2462 }
2463 } else {
2464 let escaped = regex::escape(query);
2465 if self.search_whole_word {
2466 format!(r"\b{}\b", escaped)
2467 } else {
2468 escaped
2469 }
2470 };
2471
2472 regex::RegexBuilder::new(®ex_pattern)
2473 .case_insensitive(!self.search_case_sensitive)
2474 .build()
2475 .map_err(|e| e.to_string())
2476 }
2477
2478 pub(super) fn perform_search(&mut self, query: &str) {
2487 if query.is_empty() {
2488 self.search_state = None;
2489 self.set_status_message(t!("search.cancelled").to_string());
2490 return;
2491 }
2492
2493 let search_range = self.pending_search_range.take();
2494
2495 let regex = match self.build_search_regex(query) {
2497 Ok(r) => r,
2498 Err(e) => {
2499 self.search_state = None;
2500 self.set_status_message(t!("error.invalid_regex", error = e).to_string());
2501 return;
2502 }
2503 };
2504
2505 let is_large = self.active_state().buffer.is_large_file();
2507 if is_large && search_range.is_none() {
2508 self.start_search_scan(query, regex);
2509 return;
2510 }
2511
2512 let buffer_content = {
2515 let state = self.active_state_mut();
2516 let total_bytes = state.buffer.len();
2517 match state.buffer.get_text_range_mut(0, total_bytes) {
2518 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2519 Err(e) => {
2520 tracing::warn!("Failed to load buffer for search: {}", e);
2521 self.set_status_message(t!("error.buffer_not_loaded").to_string());
2522 return;
2523 }
2524 }
2525 };
2526
2527 let (search_start, search_end) = if let Some(ref range) = search_range {
2528 (range.start, range.end)
2529 } else {
2530 (0, buffer_content.len())
2531 };
2532
2533 let search_slice = &buffer_content[search_start..search_end];
2534
2535 let mut match_ranges: Vec<(usize, usize)> = Vec::new();
2537 let mut capped = false;
2538 for m in regex.find_iter(search_slice) {
2539 if match_ranges.len() >= SearchState::MAX_MATCHES {
2540 capped = true;
2541 break;
2542 }
2543 match_ranges.push((search_start + m.start(), m.end() - m.start()));
2544 }
2545
2546 if match_ranges.is_empty() {
2547 self.search_state = None;
2548 let msg = if search_range.is_some() {
2549 format!("No matches found for '{}' in selection", query)
2550 } else {
2551 format!("No matches found for '{}'", query)
2552 };
2553 self.set_status_message(msg);
2554 return;
2555 }
2556
2557 self.finalize_search(query, match_ranges, capped, search_range);
2558 }
2559
2560 pub(super) fn finalize_search(
2569 &mut self,
2570 query: &str,
2571 match_ranges: Vec<(usize, usize)>,
2572 capped: bool,
2573 search_range: Option<std::ops::Range<usize>>,
2574 ) {
2575 let matches: Vec<usize> = match_ranges.iter().map(|(pos, _)| *pos).collect();
2576 let match_lengths: Vec<usize> = match_ranges.iter().map(|(_, len)| *len).collect();
2577 let is_large = self.active_state().buffer.is_large_file();
2578
2579 let cursor_pos = self.active_cursors().primary().position;
2581 let current_match_index = matches
2582 .iter()
2583 .position(|&pos| pos >= cursor_pos)
2584 .unwrap_or(0);
2585
2586 let match_pos = matches[current_match_index];
2588 {
2589 let active_split = self.split_manager.active_split();
2590 let active_buffer = self.active_buffer();
2591 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2592 view_state.cursors.primary_mut().position = match_pos;
2593 view_state.cursors.primary_mut().anchor = None;
2594 let state = self.buffers.get_mut(&active_buffer).unwrap();
2595 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
2596 }
2597 }
2598
2599 let num_matches = matches.len();
2600
2601 self.search_state = Some(SearchState {
2602 query: query.to_string(),
2603 matches,
2604 match_lengths: match_lengths.clone(),
2605 current_match_index: Some(current_match_index),
2606 wrap_search: search_range.is_none(),
2607 search_range,
2608 capped,
2609 });
2610
2611 if is_large {
2612 self.refresh_search_overlays();
2614 } else {
2615 let search_bg = self.theme.search_match_bg;
2617 let search_fg = self.theme.search_match_fg;
2618 let ns = self.search_namespace.clone();
2619 let state = self.active_state_mut();
2620 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2621
2622 for (&pos, &len) in match_ranges
2623 .iter()
2624 .map(|(p, _)| p)
2625 .zip(match_lengths.iter())
2626 {
2627 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2628 let overlay = crate::view::overlay::Overlay::with_namespace(
2629 &mut state.marker_list,
2630 pos..(pos + len),
2631 crate::view::overlay::OverlayFace::Style {
2632 style: search_style,
2633 },
2634 ns.clone(),
2635 )
2636 .with_priority_value(10);
2637 state.overlays.add(overlay);
2638 }
2639 }
2640
2641 let cap_suffix = if capped { "+" } else { "" };
2642 let msg = if self.search_state.as_ref().unwrap().search_range.is_some() {
2643 format!(
2644 "Found {}{} match{} for '{}' in selection",
2645 num_matches,
2646 cap_suffix,
2647 if num_matches == 1 { "" } else { "es" },
2648 query
2649 )
2650 } else {
2651 format!(
2652 "Found {}{} match{} for '{}'",
2653 num_matches,
2654 cap_suffix,
2655 if num_matches == 1 { "" } else { "es" },
2656 query
2657 )
2658 };
2659 self.set_status_message(msg);
2660 }
2661
2662 pub(super) fn refresh_search_overlays(&mut self) {
2666 let _span = tracing::info_span!("refresh_search_overlays").entered();
2667 let search_bg = self.theme.search_match_bg;
2668 let search_fg = self.theme.search_match_fg;
2669 let ns = self.search_namespace.clone();
2670
2671 let active_split = self.split_manager.active_split();
2673 let (top_byte, visible_height) = self
2674 .split_view_states
2675 .get(&active_split)
2676 .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2677 .unwrap_or((0, 20));
2678
2679 self.search_overlay_top_byte = Some(top_byte);
2682
2683 let state = self.active_state_mut();
2684
2685 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2687
2688 let visible_start = top_byte;
2690 let mut visible_end = top_byte;
2691 {
2692 let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2693 for _ in 0..visible_height {
2694 if let Some((line_start, line_content)) = line_iter.next_line() {
2695 visible_end = line_start + line_content.len();
2696 } else {
2697 break;
2698 }
2699 }
2700 }
2701 visible_end = visible_end.min(state.buffer.len());
2702
2703 let _ = state;
2707
2708 let viewport_matches: Vec<(usize, usize)> = match &self.search_state {
2709 Some(ss) => {
2710 let start_idx = ss.matches.partition_point(|&pos| pos < visible_start);
2711 ss.matches[start_idx..]
2712 .iter()
2713 .zip(ss.match_lengths[start_idx..].iter())
2714 .take_while(|(&pos, _)| pos <= visible_end)
2715 .map(|(&pos, &len)| (pos, len))
2716 .collect()
2717 }
2718 None => return,
2719 };
2720
2721 let state = self.active_state_mut();
2722
2723 for (pos, len) in &viewport_matches {
2724 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2725 let overlay = crate::view::overlay::Overlay::with_namespace(
2726 &mut state.marker_list,
2727 *pos..(*pos + *len),
2728 crate::view::overlay::OverlayFace::Style {
2729 style: search_style,
2730 },
2731 ns.clone(),
2732 )
2733 .with_priority_value(10);
2734 state.overlays.add(overlay);
2735 }
2736 }
2737
2738 pub(super) fn check_search_overlay_refresh(&mut self) -> bool {
2746 if self.search_state.is_none() {
2747 return false;
2748 }
2749 if !self.active_state().buffer.is_large_file() {
2751 return false;
2752 }
2753 let active_split = self.split_manager.active_split();
2754 let current_top = self
2755 .split_view_states
2756 .get(&active_split)
2757 .map(|vs| vs.viewport.top_byte);
2758 if current_top != self.search_overlay_top_byte {
2759 self.refresh_search_overlays();
2760 true
2761 } else {
2762 false
2763 }
2764 }
2765
2766 fn start_search_scan(&mut self, query: &str, regex: regex::Regex) {
2771 let buffer_id = self.active_buffer();
2772 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2773 let (chunks, total_bytes) = state.buffer.prepare_line_scan();
2774 let leaves = state.buffer.piece_tree_leaves();
2775 self.search_scan_state = Some(super::SearchScanState {
2776 buffer_id,
2777 leaves,
2778 chunks,
2779 next_chunk: 0,
2780 next_doc_offset: 0,
2781 total_bytes,
2782 scanned_bytes: 0,
2783 regex,
2784 query: query.to_string(),
2785 match_ranges: Vec::new(),
2786 overlap_tail: Vec::new(),
2787 overlap_doc_offset: 0,
2788 search_range: None,
2789 capped: false,
2790 case_sensitive: self.search_case_sensitive,
2791 whole_word: self.search_whole_word,
2792 use_regex: self.search_use_regex,
2793 });
2794 self.set_status_message(t!("goto.scanning_progress", percent = 0).to_string());
2795 }
2796 }
2797
2798 fn get_search_match_positions(&self) -> Vec<usize> {
2802 let ns = &self.search_namespace;
2803 let state = self.active_state();
2804
2805 let mut positions: Vec<usize> = state
2806 .overlays
2807 .all()
2808 .iter()
2809 .filter(|o| o.namespace.as_ref() == Some(ns))
2810 .filter_map(|o| state.marker_list.get_position(o.start_marker))
2811 .collect();
2812
2813 positions.sort_unstable();
2814 positions.dedup();
2815 positions
2816 }
2817
2818 pub(super) fn find_next(&mut self) {
2825 let overlay_positions = self.get_search_match_positions();
2827 let is_large = self.active_state().buffer.is_large_file();
2828
2829 if let Some(ref mut search_state) = self.search_state {
2830 let use_overlays =
2833 !is_large && !overlay_positions.is_empty() && search_state.search_range.is_none();
2834 let match_positions: &[usize] = if use_overlays {
2835 &overlay_positions
2836 } else {
2837 &search_state.matches
2838 };
2839
2840 if match_positions.is_empty() {
2841 return;
2842 }
2843
2844 let current_index = search_state.current_match_index.unwrap_or(0);
2845 let next_index = if current_index + 1 < match_positions.len() {
2846 current_index + 1
2847 } else if search_state.wrap_search {
2848 0 } else {
2850 self.set_status_message(t!("search.no_matches").to_string());
2851 return;
2852 };
2853
2854 search_state.current_match_index = Some(next_index);
2855 let match_pos = match_positions[next_index];
2856 let matches_len = match_positions.len();
2857
2858 {
2859 let active_split = self.split_manager.active_split();
2860 let active_buffer = self.active_buffer();
2861 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2862 view_state.cursors.primary_mut().position = match_pos;
2863 view_state.cursors.primary_mut().anchor = None;
2864 let state = self.buffers.get_mut(&active_buffer).unwrap();
2865 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
2866 }
2867 }
2868
2869 self.set_status_message(
2870 t!(
2871 "search.match_of",
2872 current = next_index + 1,
2873 total = matches_len
2874 )
2875 .to_string(),
2876 );
2877
2878 if is_large {
2879 self.refresh_search_overlays();
2880 }
2881 } else {
2882 let find_key = self
2883 .get_keybinding_for_action("find")
2884 .unwrap_or_else(|| "Ctrl+F".to_string());
2885 self.set_status_message(t!("search.no_active", find_key = find_key).to_string());
2886 }
2887 }
2888
2889 pub(super) fn find_previous(&mut self) {
2895 let overlay_positions = self.get_search_match_positions();
2896 let is_large = self.active_state().buffer.is_large_file();
2897
2898 if let Some(ref mut search_state) = self.search_state {
2899 let use_overlays =
2900 !is_large && !overlay_positions.is_empty() && search_state.search_range.is_none();
2901 let match_positions: &[usize] = if use_overlays {
2902 &overlay_positions
2903 } else {
2904 &search_state.matches
2905 };
2906
2907 if match_positions.is_empty() {
2908 return;
2909 }
2910
2911 let current_index = search_state.current_match_index.unwrap_or(0);
2912 let prev_index = if current_index > 0 {
2913 current_index - 1
2914 } else if search_state.wrap_search {
2915 match_positions.len() - 1 } else {
2917 self.set_status_message(t!("search.no_matches").to_string());
2918 return;
2919 };
2920
2921 search_state.current_match_index = Some(prev_index);
2922 let match_pos = match_positions[prev_index];
2923 let matches_len = match_positions.len();
2924
2925 {
2926 let active_split = self.split_manager.active_split();
2927 let active_buffer = self.active_buffer();
2928 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2929 view_state.cursors.primary_mut().position = match_pos;
2930 view_state.cursors.primary_mut().anchor = None;
2931 let state = self.buffers.get_mut(&active_buffer).unwrap();
2932 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
2933 }
2934 }
2935
2936 self.set_status_message(
2937 t!(
2938 "search.match_of",
2939 current = prev_index + 1,
2940 total = matches_len
2941 )
2942 .to_string(),
2943 );
2944
2945 if is_large {
2946 self.refresh_search_overlays();
2947 }
2948 } else {
2949 let find_key = self
2950 .get_keybinding_for_action("find")
2951 .unwrap_or_else(|| "Ctrl+F".to_string());
2952 self.set_status_message(t!("search.no_active", find_key = find_key).to_string());
2953 }
2954 }
2955
2956 pub(super) fn find_selection_next(&mut self) {
2963 if let Some(ref search_state) = self.search_state {
2966 let cursor_pos = self.active_cursors().primary().position;
2967 if search_state.matches.binary_search(&cursor_pos).is_ok() {
2968 self.find_next();
2969 return;
2970 }
2971 }
2973 self.search_state = None;
2974
2975 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
2977
2978 match search_text {
2979 Some(text) if !text.is_empty() => {
2980 let cursor_before = self.active_cursors().primary().position;
2982
2983 self.perform_search(&text);
2985
2986 if let Some(ref search_state) = self.search_state {
2988 let cursor_after = self.active_cursors().primary().position;
2989
2990 let started_at_match = selection_start
2994 .map(|start| search_state.matches.binary_search(&start).is_ok())
2995 .unwrap_or(false);
2996
2997 let landed_at_start = selection_start
2998 .map(|start| cursor_after == start)
2999 .unwrap_or(false);
3000
3001 if ((started_at_match && landed_at_start) || cursor_before == cursor_after)
3005 && search_state.matches.len() > 1
3006 {
3007 self.find_next();
3008 }
3009 }
3010 }
3011 _ => {
3012 self.set_status_message(t!("search.no_text").to_string());
3013 }
3014 }
3015 }
3016
3017 pub(super) fn find_selection_previous(&mut self) {
3023 if let Some(ref search_state) = self.search_state {
3026 let cursor_pos = self.active_cursors().primary().position;
3027 if search_state.matches.binary_search(&cursor_pos).is_ok() {
3028 self.find_previous();
3029 return;
3030 }
3031 }
3033 self.search_state = None;
3034
3035 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
3037
3038 match search_text {
3039 Some(text) if !text.is_empty() => {
3040 let cursor_before = self.active_cursors().primary().position;
3042
3043 self.perform_search(&text);
3045
3046 if let Some(ref search_state) = self.search_state {
3048 let cursor_after = self.active_cursors().primary().position;
3049
3050 let started_at_match = selection_start
3052 .map(|start| search_state.matches.binary_search(&start).is_ok())
3053 .unwrap_or(false);
3054
3055 let landed_at_start = selection_start
3056 .map(|start| cursor_after == start)
3057 .unwrap_or(false);
3058
3059 if started_at_match && landed_at_start {
3064 self.find_previous();
3066 } else if cursor_before != cursor_after {
3067 self.find_previous();
3070 } else {
3071 self.find_previous();
3073 }
3074 }
3075 }
3076 _ => {
3077 self.set_status_message(t!("search.no_text").to_string());
3078 }
3079 }
3080 }
3081
3082 fn get_selection_or_word_for_search_with_pos(&mut self) -> (Option<String>, Option<usize>) {
3085 use crate::primitives::word_navigation::{find_word_end, find_word_start};
3086
3087 let (selection_range, cursor_pos) = {
3089 let primary = self.active_cursors().primary();
3090 (primary.selection_range(), primary.position)
3091 };
3092
3093 if let Some(range) = selection_range {
3095 let state = self.active_state_mut();
3096 let text = state.get_text_range(range.start, range.end);
3097 if !text.is_empty() {
3098 return (Some(text), Some(range.start));
3099 }
3100 }
3101
3102 let (word_start, word_end) = {
3104 let state = self.active_state();
3105 let word_start = find_word_start(&state.buffer, cursor_pos);
3106 let word_end = find_word_end(&state.buffer, cursor_pos);
3107 (word_start, word_end)
3108 };
3109
3110 if word_start < word_end {
3111 let state = self.active_state_mut();
3112 (
3113 Some(state.get_text_range(word_start, word_end)),
3114 Some(word_start),
3115 )
3116 } else {
3117 (None, None)
3118 }
3119 }
3120
3121 fn build_replace_regex(&self, search: &str) -> Option<regex::bytes::Regex> {
3125 super::regex_replace::build_regex(
3126 search,
3127 self.search_use_regex,
3128 self.search_whole_word,
3129 self.search_case_sensitive,
3130 )
3131 }
3132
3133 fn get_regex_match_len(&mut self, regex: ®ex::bytes::Regex, pos: usize) -> Option<usize> {
3135 let state = self.active_state_mut();
3136 let remaining = state.buffer.len().saturating_sub(pos);
3137 if remaining == 0 {
3138 return None;
3139 }
3140 let bytes = state.buffer.get_text_range_mut(pos, remaining).ok()?;
3141 regex.find(&bytes).map(|m| m.len())
3142 }
3143
3144 fn expand_regex_replacement(
3147 &mut self,
3148 regex: ®ex::bytes::Regex,
3149 pos: usize,
3150 match_len: usize,
3151 replacement: &str,
3152 ) -> String {
3153 let state = self.active_state_mut();
3154 if let Ok(bytes) = state.buffer.get_text_range_mut(pos, match_len) {
3155 return super::regex_replace::expand_replacement(regex, &bytes, replacement);
3156 }
3157 replacement.to_string()
3158 }
3159
3160 pub(super) fn perform_replace(&mut self, search: &str, replacement: &str) {
3165 if search.is_empty() {
3166 self.set_status_message(t!("replace.empty_query").to_string());
3167 return;
3168 }
3169
3170 let compiled_regex = self.build_replace_regex(search);
3171
3172 let matches: Vec<(usize, usize, String)> = if let Some(ref regex) = compiled_regex {
3175 let buffer_bytes = {
3178 let state = self.active_state_mut();
3179 let total_bytes = state.buffer.len();
3180 match state.buffer.get_text_range_mut(0, total_bytes) {
3181 Ok(bytes) => bytes,
3182 Err(e) => {
3183 tracing::warn!("Failed to load buffer for replace: {}", e);
3184 self.set_status_message(t!("error.buffer_not_loaded").to_string());
3185 return;
3186 }
3187 }
3188 };
3189 super::regex_replace::collect_regex_matches(regex, &buffer_bytes, replacement)
3190 .into_iter()
3191 .map(|m| (m.offset, m.len, m.replacement))
3192 .collect()
3193 } else {
3194 let state = self.active_state();
3196 let buffer_len = state.buffer.len();
3197 let mut matches = Vec::new();
3198 let mut current_pos = 0;
3199
3200 while current_pos < buffer_len {
3201 if let Some(offset) = state.buffer.find_next_in_range(
3202 search,
3203 current_pos,
3204 Some(current_pos..buffer_len),
3205 ) {
3206 matches.push((offset, search.len(), replacement.to_string()));
3207 current_pos = offset + search.len();
3208 } else {
3209 break;
3210 }
3211 }
3212 matches
3213 };
3214
3215 let count = matches.len();
3216
3217 if count == 0 {
3218 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3219 return;
3220 }
3221
3222 let cursor_id = self.active_cursors().primary_id();
3224
3225 let mut events = Vec::with_capacity(count * 2);
3228 for (match_pos, match_len, expanded_replacement) in &matches {
3229 let deleted_text = self
3231 .active_state_mut()
3232 .get_text_range(*match_pos, match_pos + match_len);
3233 events.push(Event::Delete {
3235 range: *match_pos..match_pos + match_len,
3236 deleted_text,
3237 cursor_id,
3238 });
3239 events.push(Event::Insert {
3241 position: *match_pos,
3242 text: expanded_replacement.clone(),
3243 cursor_id,
3244 });
3245 }
3246
3247 let description = format!("Replace all '{}' with '{}'", search, replacement);
3249 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3250 self.active_event_log_mut().append(bulk_edit);
3251 }
3252
3253 self.search_state = None;
3255
3256 let ns = self.search_namespace.clone();
3258 let state = self.active_state_mut();
3259 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3260
3261 self.set_status_message(
3263 t!(
3264 "search.replaced",
3265 count = count,
3266 search = search,
3267 replace = replacement
3268 )
3269 .to_string(),
3270 );
3271 }
3272
3273 pub(super) fn start_interactive_replace(&mut self, search: &str, replacement: &str) {
3275 if search.is_empty() {
3276 self.set_status_message(t!("replace.query_empty").to_string());
3277 return;
3278 }
3279
3280 let compiled_regex = self.build_replace_regex(search);
3281
3282 let start_pos = self.active_cursors().primary().position;
3284 let (first_match_pos, first_match_len) = if let Some(ref regex) = compiled_regex {
3285 let state = self.active_state();
3286 let buffer_len = state.buffer.len();
3287 let found = state
3289 .buffer
3290 .find_next_regex_in_range(regex, start_pos, Some(start_pos..buffer_len))
3291 .or_else(|| {
3292 if start_pos > 0 {
3293 state
3294 .buffer
3295 .find_next_regex_in_range(regex, 0, Some(0..start_pos))
3296 } else {
3297 None
3298 }
3299 });
3300 let Some(pos) = found else {
3301 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3302 return;
3303 };
3304 let match_len = self.get_regex_match_len(regex, pos).unwrap_or(search.len());
3306 (pos, match_len)
3307 } else {
3308 let state = self.active_state();
3309 let Some(pos) = state.buffer.find_next(search, start_pos) else {
3310 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
3311 return;
3312 };
3313 (pos, search.len())
3314 };
3315
3316 self.interactive_replace_state = Some(InteractiveReplaceState {
3318 search: search.to_string(),
3319 replacement: replacement.to_string(),
3320 current_match_pos: first_match_pos,
3321 current_match_len: first_match_len,
3322 start_pos: first_match_pos,
3323 has_wrapped: false,
3324 replacements_made: 0,
3325 regex: compiled_regex,
3326 });
3327
3328 let active_split = self.split_manager.active_split();
3330 let active_buffer = self.active_buffer();
3331 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
3332 view_state.cursors.primary_mut().position = first_match_pos;
3333 view_state.cursors.primary_mut().anchor = None;
3334 let state = self.buffers.get_mut(&active_buffer).unwrap();
3336 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
3337 }
3338
3339 self.prompt = Some(Prompt::new(
3341 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string(),
3342 PromptType::QueryReplaceConfirm,
3343 ));
3344 }
3345
3346 pub(super) fn handle_interactive_replace_key(&mut self, c: char) -> AnyhowResult<()> {
3348 let state = self.interactive_replace_state.clone();
3349 let Some(mut ir_state) = state else {
3350 return Ok(());
3351 };
3352
3353 match c {
3354 'y' | 'Y' => {
3355 self.replace_current_match(&ir_state)?;
3357 ir_state.replacements_made += 1;
3358
3359 let search_pos = ir_state.current_match_pos + ir_state.replacement.len();
3361 if let Some((next_match, match_len, wrapped)) =
3362 self.find_next_match_for_replace(&ir_state, search_pos)
3363 {
3364 ir_state.current_match_pos = next_match;
3365 ir_state.current_match_len = match_len;
3366 if wrapped {
3367 ir_state.has_wrapped = true;
3368 }
3369 self.interactive_replace_state = Some(ir_state.clone());
3370 self.move_to_current_match(&ir_state);
3371 } else {
3372 self.finish_interactive_replace(ir_state.replacements_made);
3373 }
3374 }
3375 'n' | 'N' => {
3376 let search_pos = ir_state.current_match_pos + ir_state.current_match_len;
3378 if let Some((next_match, match_len, wrapped)) =
3379 self.find_next_match_for_replace(&ir_state, search_pos)
3380 {
3381 ir_state.current_match_pos = next_match;
3382 ir_state.current_match_len = match_len;
3383 if wrapped {
3384 ir_state.has_wrapped = true;
3385 }
3386 self.interactive_replace_state = Some(ir_state.clone());
3387 self.move_to_current_match(&ir_state);
3388 } else {
3389 self.finish_interactive_replace(ir_state.replacements_made);
3390 }
3391 }
3392 'a' | 'A' | '!' => {
3393 let all_matches: Vec<(usize, usize)> = {
3402 let mut matches = Vec::new();
3403 let mut temp_state = ir_state.clone();
3404 temp_state.has_wrapped = false; matches.push((ir_state.current_match_pos, ir_state.current_match_len));
3408 let mut current_pos = ir_state.current_match_pos + ir_state.current_match_len;
3409
3410 while let Some((next_match, match_len, wrapped)) =
3412 self.find_next_match_for_replace(&temp_state, current_pos)
3413 {
3414 matches.push((next_match, match_len));
3415 current_pos = next_match + match_len;
3416 if wrapped {
3417 temp_state.has_wrapped = true;
3418 }
3419 }
3420 matches
3421 };
3422
3423 let total_count = all_matches.len();
3424
3425 if total_count > 0 {
3426 let cursor_id = self.active_cursors().primary_id();
3428
3429 let mut events = Vec::with_capacity(total_count * 2);
3431 for &(match_pos, match_len) in &all_matches {
3432 let deleted_text = self
3433 .active_state_mut()
3434 .get_text_range(match_pos, match_pos + match_len);
3435 let replacement_text = if let Some(ref regex) = ir_state.regex {
3437 self.expand_regex_replacement(
3438 regex,
3439 match_pos,
3440 match_len,
3441 &ir_state.replacement,
3442 )
3443 } else {
3444 ir_state.replacement.clone()
3445 };
3446 events.push(Event::Delete {
3447 range: match_pos..match_pos + match_len,
3448 deleted_text,
3449 cursor_id,
3450 });
3451 events.push(Event::Insert {
3452 position: match_pos,
3453 text: replacement_text,
3454 cursor_id,
3455 });
3456 }
3457
3458 let description = format!(
3460 "Replace all {} occurrences of '{}' with '{}'",
3461 total_count, ir_state.search, ir_state.replacement
3462 );
3463 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3464 self.active_event_log_mut().append(bulk_edit);
3465 }
3466
3467 ir_state.replacements_made += total_count;
3468 }
3469
3470 self.finish_interactive_replace(ir_state.replacements_made);
3471 }
3472 'c' | 'C' | 'q' | 'Q' | '\x1b' => {
3473 self.finish_interactive_replace(ir_state.replacements_made);
3475 }
3476 _ => {
3477 }
3479 }
3480
3481 Ok(())
3482 }
3483
3484 pub(super) fn find_next_match_for_replace(
3487 &mut self,
3488 ir_state: &InteractiveReplaceState,
3489 start_pos: usize,
3490 ) -> Option<(usize, usize, bool)> {
3491 if let Some(ref regex) = ir_state.regex {
3492 let regex = regex.clone();
3494 let state = self.active_state();
3495 let buffer_len = state.buffer.len();
3496
3497 if ir_state.has_wrapped {
3498 let search_range = Some(start_pos..ir_state.start_pos);
3499 if let Some(match_pos) =
3500 state
3501 .buffer
3502 .find_next_regex_in_range(®ex, start_pos, search_range)
3503 {
3504 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3505 return Some((match_pos, match_len, true));
3506 }
3507 None
3508 } else {
3509 let search_range = Some(start_pos..buffer_len);
3510 if let Some(match_pos) =
3511 state
3512 .buffer
3513 .find_next_regex_in_range(®ex, start_pos, search_range)
3514 {
3515 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3516 return Some((match_pos, match_len, false));
3517 }
3518
3519 let wrap_range = Some(0..ir_state.start_pos);
3521 let state = self.active_state();
3522 if let Some(match_pos) =
3523 state.buffer.find_next_regex_in_range(®ex, 0, wrap_range)
3524 {
3525 let match_len = self.get_regex_match_len(®ex, match_pos).unwrap_or(0);
3526 return Some((match_pos, match_len, true));
3527 }
3528
3529 None
3530 }
3531 } else {
3532 let search_len = ir_state.search.len();
3534 let state = self.active_state();
3535
3536 if ir_state.has_wrapped {
3537 let search_range = Some(start_pos..ir_state.start_pos);
3538 if let Some(match_pos) =
3539 state
3540 .buffer
3541 .find_next_in_range(&ir_state.search, start_pos, search_range)
3542 {
3543 return Some((match_pos, search_len, true));
3544 }
3545 None
3546 } else {
3547 let buffer_len = state.buffer.len();
3548 let search_range = Some(start_pos..buffer_len);
3549 if let Some(match_pos) =
3550 state
3551 .buffer
3552 .find_next_in_range(&ir_state.search, start_pos, search_range)
3553 {
3554 return Some((match_pos, search_len, false));
3555 }
3556
3557 let wrap_range = Some(0..ir_state.start_pos);
3558 if let Some(match_pos) =
3559 state
3560 .buffer
3561 .find_next_in_range(&ir_state.search, 0, wrap_range)
3562 {
3563 return Some((match_pos, search_len, true));
3564 }
3565
3566 None
3567 }
3568 }
3569 }
3570
3571 pub(super) fn replace_current_match(
3573 &mut self,
3574 ir_state: &InteractiveReplaceState,
3575 ) -> AnyhowResult<()> {
3576 let match_pos = ir_state.current_match_pos;
3577 let match_len = ir_state.current_match_len;
3578 let range = match_pos..(match_pos + match_len);
3579
3580 let replacement_text = if let Some(ref regex) = ir_state.regex {
3582 self.expand_regex_replacement(regex, match_pos, match_len, &ir_state.replacement)
3583 } else {
3584 ir_state.replacement.clone()
3585 };
3586
3587 let deleted_text = self
3589 .active_state_mut()
3590 .get_text_range(range.start, range.end);
3591
3592 let cursor_id = self.active_cursors().primary_id();
3594 let cursor = *self.active_cursors().primary();
3595 let old_position = cursor.position;
3596 let old_anchor = cursor.anchor;
3597 let old_sticky_column = cursor.sticky_column;
3598
3599 let events = vec![
3602 Event::MoveCursor {
3603 cursor_id,
3604 old_position,
3605 new_position: match_pos,
3606 old_anchor,
3607 new_anchor: None,
3608 old_sticky_column,
3609 new_sticky_column: 0,
3610 },
3611 Event::Delete {
3612 range: range.clone(),
3613 deleted_text,
3614 cursor_id,
3615 },
3616 Event::Insert {
3617 position: match_pos,
3618 text: replacement_text,
3619 cursor_id,
3620 },
3621 ];
3622
3623 let batch = Event::Batch {
3625 events,
3626 description: format!(
3627 "Query replace '{}' with '{}'",
3628 ir_state.search, ir_state.replacement
3629 ),
3630 };
3631
3632 self.active_event_log_mut().append(batch.clone());
3634 self.apply_event_to_active_buffer(&batch);
3635
3636 Ok(())
3637 }
3638
3639 pub(super) fn move_to_current_match(&mut self, ir_state: &InteractiveReplaceState) {
3641 let match_pos = ir_state.current_match_pos;
3642 let active_split = self.split_manager.active_split();
3643 let active_buffer = self.active_buffer();
3644 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
3645 view_state.cursors.primary_mut().position = match_pos;
3646 view_state.cursors.primary_mut().anchor = None;
3647 let state = self.buffers.get_mut(&active_buffer).unwrap();
3649 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
3650 }
3651
3652 let msg = if ir_state.has_wrapped {
3654 "[Wrapped] Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
3655 } else {
3656 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
3657 };
3658 if let Some(ref mut prompt) = self.prompt {
3659 if prompt.prompt_type == PromptType::QueryReplaceConfirm {
3660 prompt.message = msg;
3661 prompt.input.clear();
3662 prompt.cursor_pos = 0;
3663 }
3664 }
3665 }
3666
3667 pub(super) fn finish_interactive_replace(&mut self, replacements_made: usize) {
3669 self.interactive_replace_state = None;
3670 self.prompt = None; let ns = self.search_namespace.clone();
3674 let state = self.active_state_mut();
3675 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3676
3677 self.set_status_message(t!("search.replaced_count", count = replacements_made).to_string());
3678 }
3679
3680 pub(super) fn smart_home(&mut self) {
3682 let estimated_line_length = self.config.editor.estimated_line_length;
3683 let cursor = *self.active_cursors().primary();
3684 let cursor_id = self.active_cursors().primary_id();
3685
3686 if self.config.editor.line_wrap {
3688 let split_id = self.split_manager.active_split();
3689 if let Some(new_pos) =
3690 self.smart_home_visual_line(split_id, cursor.position, estimated_line_length)
3691 {
3692 let event = Event::MoveCursor {
3693 cursor_id,
3694 old_position: cursor.position,
3695 new_position: new_pos,
3696 old_anchor: cursor.anchor,
3697 new_anchor: None,
3698 old_sticky_column: cursor.sticky_column,
3699 new_sticky_column: 0,
3700 };
3701 self.active_event_log_mut().append(event.clone());
3702 self.apply_event_to_active_buffer(&event);
3703 return;
3704 }
3705 }
3707
3708 let state = self.active_state_mut();
3709
3710 let mut iter = state
3712 .buffer
3713 .line_iterator(cursor.position, estimated_line_length);
3714 if let Some((line_start, line_content)) = iter.next_line() {
3715 let first_non_ws = line_content
3717 .chars()
3718 .take_while(|c| *c != '\n')
3719 .position(|c| !c.is_whitespace())
3720 .map(|offset| line_start + offset)
3721 .unwrap_or(line_start);
3722
3723 let new_pos = if cursor.position == first_non_ws {
3725 line_start
3726 } else {
3727 first_non_ws
3728 };
3729
3730 let event = Event::MoveCursor {
3731 cursor_id,
3732 old_position: cursor.position,
3733 new_position: new_pos,
3734 old_anchor: cursor.anchor,
3735 new_anchor: None,
3736 old_sticky_column: cursor.sticky_column,
3737 new_sticky_column: 0,
3738 };
3739
3740 self.active_event_log_mut().append(event.clone());
3741 self.apply_event_to_active_buffer(&event);
3742 }
3743 }
3744
3745 fn smart_home_visual_line(
3754 &mut self,
3755 split_id: LeafId,
3756 cursor_pos: usize,
3757 estimated_line_length: usize,
3758 ) -> Option<usize> {
3759 let visual_start = self
3760 .cached_layout
3761 .visual_line_start(split_id, cursor_pos, false)?;
3762
3763 let buffer_id = self.split_manager.active_buffer_id()?;
3765 let state = self.buffers.get_mut(&buffer_id)?;
3766 let mut iter = state
3767 .buffer
3768 .line_iterator(visual_start, estimated_line_length);
3769 let (phys_line_start, content) = iter.next_line()?;
3770
3771 let is_first_visual_row = visual_start == phys_line_start;
3772
3773 if is_first_visual_row {
3774 let visual_end = self
3776 .cached_layout
3777 .visual_line_end(split_id, cursor_pos, false)
3778 .unwrap_or(visual_start);
3779 let visual_len = visual_end.saturating_sub(visual_start);
3780 let first_non_ws = content
3781 .chars()
3782 .take(visual_len)
3783 .take_while(|c| *c != '\n')
3784 .position(|c| !c.is_whitespace())
3785 .map(|offset| visual_start + offset)
3786 .unwrap_or(visual_start);
3787
3788 if cursor_pos == first_non_ws {
3789 Some(visual_start)
3790 } else {
3791 Some(first_non_ws)
3792 }
3793 } else {
3794 if cursor_pos == visual_start {
3796 self.cached_layout
3798 .visual_line_start(split_id, cursor_pos, true)
3799 } else {
3800 Some(visual_start)
3801 }
3802 }
3803 }
3804
3805 pub(super) fn toggle_comment(&mut self) {
3807 let language = &self.active_state().language;
3810 let comment_prefix = self
3811 .config
3812 .languages
3813 .get(language)
3814 .and_then(|lang_config| lang_config.comment_prefix.clone());
3815
3816 let comment_prefix: String = match comment_prefix {
3817 Some(prefix) => {
3818 if prefix.ends_with(' ') {
3820 prefix
3821 } else {
3822 format!("{} ", prefix)
3823 }
3824 }
3825 None => return, };
3827
3828 let estimated_line_length = self.config.editor.estimated_line_length;
3829
3830 let cursor = *self.active_cursors().primary();
3831 let cursor_id = self.active_cursors().primary_id();
3832 let state = self.active_state_mut();
3833
3834 let original_anchor = cursor.anchor;
3836 let original_position = cursor.position;
3837 let had_selection = original_anchor.is_some();
3838
3839 let (start_pos, end_pos) = if let Some(range) = cursor.selection_range() {
3840 (range.start, range.end)
3841 } else {
3842 let iter = state
3843 .buffer
3844 .line_iterator(cursor.position, estimated_line_length);
3845 let line_start = iter.current_position();
3846 (line_start, cursor.position)
3847 };
3848
3849 let buffer_len = state.buffer.len();
3851 let mut line_starts = Vec::new();
3852 let mut iter = state.buffer.line_iterator(start_pos, estimated_line_length);
3853 let mut current_pos = iter.current_position();
3854 line_starts.push(current_pos);
3855
3856 while let Some((_, content)) = iter.next_line() {
3857 current_pos += content.len();
3858 if current_pos >= end_pos || current_pos >= buffer_len {
3859 break;
3860 }
3861 let next_iter = state
3862 .buffer
3863 .line_iterator(current_pos, estimated_line_length);
3864 let next_start = next_iter.current_position();
3865 if next_start != *line_starts.last().unwrap() {
3866 line_starts.push(next_start);
3867 }
3868 iter = state
3869 .buffer
3870 .line_iterator(current_pos, estimated_line_length);
3871 }
3872
3873 let all_commented = line_starts.iter().all(|&line_start| {
3876 let line_bytes = state
3877 .buffer
3878 .slice_bytes(line_start..buffer_len.min(line_start + comment_prefix.len() + 10));
3879 let line_str = String::from_utf8_lossy(&line_bytes);
3880 let trimmed = line_str.trim_start();
3881 trimmed.starts_with(comment_prefix.trim())
3882 });
3883
3884 let mut events = Vec::new();
3885 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
3888
3889 if all_commented {
3890 for &line_start in line_starts.iter().rev() {
3892 let line_bytes = state
3893 .buffer
3894 .slice_bytes(line_start..buffer_len.min(line_start + 100));
3895 let line_str = String::from_utf8_lossy(&line_bytes);
3896
3897 let leading_ws: usize = line_str
3899 .chars()
3900 .take_while(|c| c.is_whitespace() && *c != '\n')
3901 .map(|c| c.len_utf8())
3902 .sum();
3903 let rest = &line_str[leading_ws..];
3904
3905 if rest.starts_with(comment_prefix.trim()) {
3906 let remove_len = if rest.starts_with(&comment_prefix) {
3907 comment_prefix.len()
3908 } else {
3909 comment_prefix.trim().len()
3910 };
3911 let deleted_text = String::from_utf8_lossy(&state.buffer.slice_bytes(
3912 line_start + leading_ws..line_start + leading_ws + remove_len,
3913 ))
3914 .to_string();
3915 events.push(Event::Delete {
3916 range: (line_start + leading_ws)..(line_start + leading_ws + remove_len),
3917 deleted_text,
3918 cursor_id,
3919 });
3920 position_deltas.push((line_start, -(remove_len as isize)));
3921 }
3922 }
3923 } else {
3924 let prefix_len = comment_prefix.len();
3926 for &line_start in line_starts.iter().rev() {
3927 events.push(Event::Insert {
3928 position: line_start,
3929 text: comment_prefix.to_string(),
3930 cursor_id,
3931 });
3932 position_deltas.push((line_start, prefix_len as isize));
3933 }
3934 }
3935
3936 if events.is_empty() {
3937 return;
3938 }
3939
3940 let action_desc = if all_commented {
3941 "Uncomment"
3942 } else {
3943 "Comment"
3944 };
3945
3946 if had_selection {
3948 position_deltas.sort_by_key(|(pos, _)| *pos);
3950
3951 let calc_shift = |original_pos: usize| -> isize {
3953 let mut shift: isize = 0;
3954 for (edit_pos, delta) in &position_deltas {
3955 if *edit_pos < original_pos {
3956 shift += delta;
3957 }
3958 }
3959 shift
3960 };
3961
3962 let anchor_shift = calc_shift(original_anchor.unwrap_or(0));
3963 let position_shift = calc_shift(original_position);
3964
3965 let new_anchor = (original_anchor.unwrap_or(0) as isize + anchor_shift).max(0) as usize;
3966 let new_position = (original_position as isize + position_shift).max(0) as usize;
3967
3968 events.push(Event::MoveCursor {
3969 cursor_id,
3970 old_position: original_position,
3971 new_position,
3972 old_anchor: original_anchor,
3973 new_anchor: Some(new_anchor),
3974 old_sticky_column: 0,
3975 new_sticky_column: 0,
3976 });
3977 }
3978
3979 let description = format!("{} lines", action_desc);
3981 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3982 self.active_event_log_mut().append(bulk_edit);
3983 }
3984
3985 self.set_status_message(
3986 t!(
3987 "lines.action",
3988 action = action_desc,
3989 count = line_starts.len()
3990 )
3991 .to_string(),
3992 );
3993 }
3994
3995 pub(super) fn goto_matching_bracket(&mut self) {
3997 let cursor = *self.active_cursors().primary();
3998 let cursor_id = self.active_cursors().primary_id();
3999 let state = self.active_state_mut();
4000
4001 let pos = cursor.position;
4002 if pos >= state.buffer.len() {
4003 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4004 return;
4005 }
4006
4007 let bytes = state.buffer.slice_bytes(pos..pos + 1);
4008 if bytes.is_empty() {
4009 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4010 return;
4011 }
4012
4013 let ch = bytes[0] as char;
4014 let (opening, closing, forward) = match ch {
4015 '(' => ('(', ')', true),
4016 ')' => ('(', ')', false),
4017 '[' => ('[', ']', true),
4018 ']' => ('[', ']', false),
4019 '{' => ('{', '}', true),
4020 '}' => ('{', '}', false),
4021 '<' => ('<', '>', true),
4022 '>' => ('<', '>', false),
4023 _ => {
4024 self.set_status_message(t!("diagnostics.bracket_none").to_string());
4025 return;
4026 }
4027 };
4028
4029 let buffer_len = state.buffer.len();
4031 let mut depth = 1;
4032 let matching_pos = if forward {
4033 let mut search_pos = pos + 1;
4034 let mut found = None;
4035 while search_pos < buffer_len && depth > 0 {
4036 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4037 if !b.is_empty() {
4038 let c = b[0] as char;
4039 if c == opening {
4040 depth += 1;
4041 } else if c == closing {
4042 depth -= 1;
4043 if depth == 0 {
4044 found = Some(search_pos);
4045 }
4046 }
4047 }
4048 search_pos += 1;
4049 }
4050 found
4051 } else {
4052 let mut search_pos = pos.saturating_sub(1);
4053 let mut found = None;
4054 loop {
4055 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
4056 if !b.is_empty() {
4057 let c = b[0] as char;
4058 if c == closing {
4059 depth += 1;
4060 } else if c == opening {
4061 depth -= 1;
4062 if depth == 0 {
4063 found = Some(search_pos);
4064 break;
4065 }
4066 }
4067 }
4068 if search_pos == 0 {
4069 break;
4070 }
4071 search_pos -= 1;
4072 }
4073 found
4074 };
4075
4076 if let Some(new_pos) = matching_pos {
4077 let event = Event::MoveCursor {
4078 cursor_id,
4079 old_position: cursor.position,
4080 new_position: new_pos,
4081 old_anchor: cursor.anchor,
4082 new_anchor: None,
4083 old_sticky_column: cursor.sticky_column,
4084 new_sticky_column: 0,
4085 };
4086 self.active_event_log_mut().append(event.clone());
4087 self.apply_event_to_active_buffer(&event);
4088 } else {
4089 self.set_status_message(t!("diagnostics.bracket_no_match").to_string());
4090 }
4091 }
4092
4093 pub(super) fn jump_to_next_error(&mut self) {
4095 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
4096 let cursor_pos = self.active_cursors().primary().position;
4097 let cursor_id = self.active_cursors().primary_id();
4098 let cursor = *self.active_cursors().primary();
4099 let state = self.active_state_mut();
4100
4101 let mut diagnostic_positions: Vec<usize> = state
4103 .overlays
4104 .all()
4105 .iter()
4106 .filter_map(|overlay| {
4107 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4109 Some(overlay.range(&state.marker_list).start)
4110 } else {
4111 None
4112 }
4113 })
4114 .collect();
4115
4116 if diagnostic_positions.is_empty() {
4117 self.set_status_message(t!("diagnostics.none").to_string());
4118 return;
4119 }
4120
4121 diagnostic_positions.sort_unstable();
4123 diagnostic_positions.dedup();
4124
4125 let next_pos = diagnostic_positions
4127 .iter()
4128 .find(|&&pos| pos > cursor_pos)
4129 .or_else(|| diagnostic_positions.first()) .copied();
4131
4132 if let Some(new_pos) = next_pos {
4133 let event = Event::MoveCursor {
4134 cursor_id,
4135 old_position: cursor.position,
4136 new_position: new_pos,
4137 old_anchor: cursor.anchor,
4138 new_anchor: None,
4139 old_sticky_column: cursor.sticky_column,
4140 new_sticky_column: 0,
4141 };
4142 self.active_event_log_mut().append(event.clone());
4143 self.apply_event_to_active_buffer(&event);
4144
4145 let state = self.active_state();
4147 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
4148 let range = overlay.range(&state.marker_list);
4149 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4150 overlay.message.clone()
4151 } else {
4152 None
4153 }
4154 }) {
4155 self.set_status_message(msg);
4156 }
4157 }
4158 }
4159
4160 pub(super) fn jump_to_previous_error(&mut self) {
4162 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
4163 let cursor_pos = self.active_cursors().primary().position;
4164 let cursor_id = self.active_cursors().primary_id();
4165 let cursor = *self.active_cursors().primary();
4166 let state = self.active_state_mut();
4167
4168 let mut diagnostic_positions: Vec<usize> = state
4170 .overlays
4171 .all()
4172 .iter()
4173 .filter_map(|overlay| {
4174 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4176 Some(overlay.range(&state.marker_list).start)
4177 } else {
4178 None
4179 }
4180 })
4181 .collect();
4182
4183 if diagnostic_positions.is_empty() {
4184 self.set_status_message(t!("diagnostics.none").to_string());
4185 return;
4186 }
4187
4188 diagnostic_positions.sort_unstable();
4190 diagnostic_positions.dedup();
4191
4192 let prev_pos = diagnostic_positions
4194 .iter()
4195 .rev()
4196 .find(|&&pos| pos < cursor_pos)
4197 .or_else(|| diagnostic_positions.last()) .copied();
4199
4200 if let Some(new_pos) = prev_pos {
4201 let event = Event::MoveCursor {
4202 cursor_id,
4203 old_position: cursor.position,
4204 new_position: new_pos,
4205 old_anchor: cursor.anchor,
4206 new_anchor: None,
4207 old_sticky_column: cursor.sticky_column,
4208 new_sticky_column: 0,
4209 };
4210 self.active_event_log_mut().append(event.clone());
4211 self.apply_event_to_active_buffer(&event);
4212
4213 let state = self.active_state();
4215 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
4216 let range = overlay.range(&state.marker_list);
4217 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4218 overlay.message.clone()
4219 } else {
4220 None
4221 }
4222 }) {
4223 self.set_status_message(msg);
4224 }
4225 }
4226 }
4227
4228 pub(super) fn toggle_macro_recording(&mut self, key: char) {
4230 if let Some(state) = &self.macro_recording {
4231 if state.key == key {
4232 self.stop_macro_recording();
4234 } else {
4235 self.stop_macro_recording();
4237 self.start_macro_recording(key);
4238 }
4239 } else {
4240 self.start_macro_recording(key);
4242 }
4243 }
4244
4245 pub(super) fn start_macro_recording(&mut self, key: char) {
4247 self.macro_recording = Some(MacroRecordingState {
4248 key,
4249 actions: Vec::new(),
4250 });
4251
4252 let stop_hint = self.build_macro_stop_hint(key);
4254 self.set_status_message(
4255 t!(
4256 "macro.recording_with_hint",
4257 key = key,
4258 stop_hint = stop_hint
4259 )
4260 .to_string(),
4261 );
4262 }
4263
4264 fn build_macro_stop_hint(&self, _key: char) -> String {
4266 let mut hints = Vec::new();
4267
4268 if let Some(stop_key) = self.get_keybinding_for_action("stop_macro_recording") {
4270 hints.push(stop_key);
4271 }
4272
4273 let palette_key = self
4275 .get_keybinding_for_action("command_palette")
4276 .unwrap_or_else(|| "Ctrl+P".to_string());
4277
4278 if hints.is_empty() {
4279 format!("{} → Stop Recording Macro", palette_key)
4281 } else {
4282 format!("{} or {} → Stop Recording", hints.join("/"), palette_key)
4284 }
4285 }
4286
4287 pub(super) fn stop_macro_recording(&mut self) {
4289 if let Some(state) = self.macro_recording.take() {
4290 let action_count = state.actions.len();
4291 let key = state.key;
4292 self.macros.insert(key, state.actions);
4293 self.last_macro_register = Some(key);
4294
4295 let play_hint = self.build_macro_play_hint();
4297 self.set_status_message(
4298 t!(
4299 "macro.saved",
4300 key = key,
4301 count = action_count,
4302 play_hint = play_hint
4303 )
4304 .to_string(),
4305 );
4306 } else {
4307 self.set_status_message(t!("macro.not_recording").to_string());
4308 }
4309 }
4310
4311 fn build_macro_play_hint(&self) -> String {
4313 if let Some(play_key) = self.get_keybinding_for_action("play_last_macro") {
4315 return format!("{} → Play Last Macro", play_key);
4316 }
4317
4318 let palette_key = self
4320 .get_keybinding_for_action("command_palette")
4321 .unwrap_or_else(|| "Ctrl+P".to_string());
4322
4323 format!("{} → Play Macro", palette_key)
4324 }
4325
4326 pub fn recompute_layout_cached(&mut self) {
4330 let w = self.cached_layout.last_frame_width;
4331 let h = self.cached_layout.last_frame_height;
4332 self.recompute_layout(w, h);
4333 }
4334
4335 pub fn recompute_layout(&mut self, width: u16, height: u16) {
4340 let size = ratatui::layout::Rect::new(0, 0, width, height);
4341
4342 let active_split = self.split_manager.active_split();
4344 self.pre_sync_ensure_visible(active_split);
4345 self.sync_scroll_groups();
4346
4347 let constraints = vec![
4350 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
4351 Constraint::Min(0),
4352 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(1), ];
4356 let main_chunks = Layout::default()
4357 .direction(Direction::Vertical)
4358 .constraints(constraints)
4359 .split(size);
4360 let main_content_area = main_chunks[1];
4361
4362 let file_explorer_should_show = self.file_explorer_visible
4364 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
4365 let editor_content_area = if file_explorer_should_show {
4366 let explorer_percent = (self.file_explorer_width_percent * 100.0) as u16;
4367 let editor_percent = 100 - explorer_percent;
4368 let horizontal_chunks = Layout::default()
4369 .direction(Direction::Horizontal)
4370 .constraints([
4371 Constraint::Percentage(explorer_percent),
4372 Constraint::Percentage(editor_percent),
4373 ])
4374 .split(main_content_area);
4375 horizontal_chunks[1]
4376 } else {
4377 main_content_area
4378 };
4379
4380 let view_line_mappings = SplitRenderer::compute_content_layout(
4382 editor_content_area,
4383 &self.split_manager,
4384 &mut self.buffers,
4385 &mut self.split_view_states,
4386 &self.theme,
4387 false, self.config.editor.estimated_line_length,
4389 self.config.editor.highlight_context_bytes,
4390 self.config.editor.relative_line_numbers,
4391 self.config.editor.use_terminal_bg,
4392 self.session_mode || !self.config.editor.cursor_style.is_block(),
4393 self.software_cursor_only,
4394 self.tab_bar_visible,
4395 self.config.editor.show_vertical_scrollbar,
4396 self.config.editor.show_horizontal_scrollbar,
4397 self.config.editor.diagnostics_inline_text,
4398 );
4399
4400 self.cached_layout.view_line_mappings = view_line_mappings;
4401 }
4402
4403 pub(super) fn play_macro(&mut self, key: char) {
4410 if self.macro_playing {
4412 return;
4413 }
4414
4415 if let Some(actions) = self.macros.get(&key).cloned() {
4416 if actions.is_empty() {
4417 self.set_status_message(t!("macro.empty", key = key).to_string());
4418 return;
4419 }
4420
4421 self.macro_playing = true;
4422 let action_count = actions.len();
4423 let width = self.cached_layout.last_frame_width;
4424 let height = self.cached_layout.last_frame_height;
4425 for action in actions {
4426 if let Err(e) = self.handle_action(action) {
4427 tracing::warn!("Macro action failed: {}", e);
4428 }
4429 self.recompute_layout(width, height);
4430 }
4431 self.macro_playing = false;
4432
4433 self.set_status_message(
4434 t!("macro.played", key = key, count = action_count).to_string(),
4435 );
4436 } else {
4437 self.set_status_message(t!("macro.not_found", key = key).to_string());
4438 }
4439 }
4440
4441 pub(super) fn record_macro_action(&mut self, action: &Action) {
4443 if self.macro_playing {
4445 return;
4446 }
4447 if let Some(state) = &mut self.macro_recording {
4448 match action {
4450 Action::StartMacroRecording
4451 | Action::StopMacroRecording
4452 | Action::PlayMacro(_)
4453 | Action::ToggleMacroRecording(_)
4454 | Action::ShowMacro(_)
4455 | Action::ListMacros
4456 | Action::PromptRecordMacro
4457 | Action::PromptPlayMacro
4458 | Action::PlayLastMacro => {}
4459 Action::PromptConfirm => {
4462 if let Some(prompt) = &self.prompt {
4463 let text = prompt.get_text().to_string();
4464 state.actions.push(Action::PromptConfirmWithText(text));
4465 } else {
4466 state.actions.push(action.clone());
4467 }
4468 }
4469 _ => {
4470 state.actions.push(action.clone());
4471 }
4472 }
4473 }
4474 }
4475
4476 pub(super) fn show_macro_in_buffer(&mut self, key: char) {
4478 let (json, actions_len) = match self.macros.get(&key) {
4480 Some(actions) => {
4481 let json = match serde_json::to_string_pretty(actions) {
4482 Ok(json) => json,
4483 Err(e) => {
4484 self.set_status_message(
4485 t!("macro.serialize_failed", error = e.to_string()).to_string(),
4486 );
4487 return;
4488 }
4489 };
4490 (json, actions.len())
4491 }
4492 None => {
4493 self.set_status_message(t!("macro.not_found", key = key).to_string());
4494 return;
4495 }
4496 };
4497
4498 let content = format!(
4500 "// Macro '{}' ({} actions)\n// This buffer can be saved as a .json file for persistence\n\n{}",
4501 key,
4502 actions_len,
4503 json
4504 );
4505
4506 let buffer_id = BufferId(self.next_buffer_id);
4508 self.next_buffer_id += 1;
4509
4510 let mut state = EditorState::new(
4511 self.terminal_width,
4512 self.terminal_height,
4513 self.config.editor.large_file_threshold_bytes as usize,
4514 std::sync::Arc::clone(&self.filesystem),
4515 );
4516 state
4517 .margins
4518 .configure_for_line_numbers(self.config.editor.line_numbers);
4519
4520 self.buffers.insert(buffer_id, state);
4521 self.event_logs.insert(buffer_id, EventLog::new());
4522
4523 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4525 state.buffer = crate::model::buffer::Buffer::from_str(
4526 &content,
4527 self.config.editor.large_file_threshold_bytes as usize,
4528 std::sync::Arc::clone(&self.filesystem),
4529 );
4530 }
4531
4532 let metadata = BufferMetadata {
4534 kind: BufferKind::Virtual {
4535 mode: "macro-view".to_string(),
4536 },
4537 display_name: format!("*Macro {}*", key),
4538 lsp_enabled: false,
4539 lsp_disabled_reason: Some("Virtual macro buffer".to_string()),
4540 read_only: false, binary: false,
4542 lsp_opened_with: std::collections::HashSet::new(),
4543 hidden_from_tabs: false,
4544 recovery_id: None,
4545 };
4546 self.buffer_metadata.insert(buffer_id, metadata);
4547
4548 self.set_active_buffer(buffer_id);
4550 self.set_status_message(
4551 t!("macro.shown_buffer", key = key, count = actions_len).to_string(),
4552 );
4553 }
4554
4555 pub(super) fn list_macros_in_buffer(&mut self) {
4557 if self.macros.is_empty() {
4558 self.set_status_message(t!("macro.none_recorded").to_string());
4559 return;
4560 }
4561
4562 let mut content =
4564 String::from("// Recorded Macros\n// Use ShowMacro(key) to see details\n\n");
4565
4566 let mut keys: Vec<char> = self.macros.keys().copied().collect();
4567 keys.sort();
4568
4569 for key in keys {
4570 if let Some(actions) = self.macros.get(&key) {
4571 content.push_str(&format!("Macro '{}': {} actions\n", key, actions.len()));
4572
4573 for (i, action) in actions.iter().enumerate() {
4575 content.push_str(&format!(" {}. {:?}\n", i + 1, action));
4576 }
4577 content.push('\n');
4578 }
4579 }
4580
4581 let buffer_id = BufferId(self.next_buffer_id);
4583 self.next_buffer_id += 1;
4584
4585 let mut state = EditorState::new(
4586 self.terminal_width,
4587 self.terminal_height,
4588 self.config.editor.large_file_threshold_bytes as usize,
4589 std::sync::Arc::clone(&self.filesystem),
4590 );
4591 state
4592 .margins
4593 .configure_for_line_numbers(self.config.editor.line_numbers);
4594
4595 self.buffers.insert(buffer_id, state);
4596 self.event_logs.insert(buffer_id, EventLog::new());
4597
4598 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4600 state.buffer = crate::model::buffer::Buffer::from_str(
4601 &content,
4602 self.config.editor.large_file_threshold_bytes as usize,
4603 std::sync::Arc::clone(&self.filesystem),
4604 );
4605 }
4606
4607 let metadata = BufferMetadata {
4609 kind: BufferKind::Virtual {
4610 mode: "macro-list".to_string(),
4611 },
4612 display_name: "*Macros*".to_string(),
4613 lsp_enabled: false,
4614 lsp_disabled_reason: Some("Virtual macro list buffer".to_string()),
4615 read_only: true,
4616 binary: false,
4617 lsp_opened_with: std::collections::HashSet::new(),
4618 hidden_from_tabs: false,
4619 recovery_id: None,
4620 };
4621 self.buffer_metadata.insert(buffer_id, metadata);
4622
4623 self.set_active_buffer(buffer_id);
4625 self.set_status_message(t!("macro.showing", count = self.macros.len()).to_string());
4626 }
4627
4628 pub(super) fn set_bookmark(&mut self, key: char) {
4630 let buffer_id = self.active_buffer();
4631 let position = self.active_cursors().primary().position;
4632 self.bookmarks.insert(
4633 key,
4634 Bookmark {
4635 buffer_id,
4636 position,
4637 },
4638 );
4639 self.set_status_message(t!("bookmark.set", key = key).to_string());
4640 }
4641
4642 pub(super) fn jump_to_bookmark(&mut self, key: char) {
4644 if let Some(bookmark) = self.bookmarks.get(&key).cloned() {
4645 if bookmark.buffer_id != self.active_buffer() {
4647 if self.buffers.contains_key(&bookmark.buffer_id) {
4648 self.set_active_buffer(bookmark.buffer_id);
4649 } else {
4650 self.set_status_message(t!("bookmark.buffer_gone", key = key).to_string());
4651 self.bookmarks.remove(&key);
4652 return;
4653 }
4654 }
4655
4656 let cursor = *self.active_cursors().primary();
4658 let cursor_id = self.active_cursors().primary_id();
4659 let state = self.active_state_mut();
4660 let new_pos = bookmark.position.min(state.buffer.len());
4661
4662 let event = Event::MoveCursor {
4663 cursor_id,
4664 old_position: cursor.position,
4665 new_position: new_pos,
4666 old_anchor: cursor.anchor,
4667 new_anchor: None,
4668 old_sticky_column: cursor.sticky_column,
4669 new_sticky_column: 0,
4670 };
4671
4672 self.active_event_log_mut().append(event.clone());
4673 self.apply_event_to_active_buffer(&event);
4674 self.set_status_message(t!("bookmark.jumped", key = key).to_string());
4675 } else {
4676 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
4677 }
4678 }
4679
4680 pub(super) fn clear_bookmark(&mut self, key: char) {
4682 if self.bookmarks.remove(&key).is_some() {
4683 self.set_status_message(t!("bookmark.cleared", key = key).to_string());
4684 } else {
4685 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
4686 }
4687 }
4688
4689 pub(super) fn list_bookmarks(&mut self) {
4691 if self.bookmarks.is_empty() {
4692 self.set_status_message(t!("bookmark.none_set").to_string());
4693 return;
4694 }
4695
4696 let mut bookmark_list: Vec<_> = self.bookmarks.iter().collect();
4697 bookmark_list.sort_by_key(|(k, _)| *k);
4698
4699 let list_str: String = bookmark_list
4700 .iter()
4701 .map(|(k, bm)| {
4702 let buffer_name = self
4703 .buffer_metadata
4704 .get(&bm.buffer_id)
4705 .map(|m| m.display_name.as_str())
4706 .unwrap_or("unknown");
4707 format!("'{}': {} @ {}", k, buffer_name, bm.position)
4708 })
4709 .collect::<Vec<_>>()
4710 .join(", ");
4711
4712 self.set_status_message(t!("bookmark.list", list = list_str).to_string());
4713 }
4714
4715 pub fn clear_search_history(&mut self) {
4718 if let Some(history) = self.prompt_histories.get_mut("search") {
4719 history.clear();
4720 }
4721 }
4722
4723 pub fn save_histories(&self) {
4726 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.data_dir) {
4728 tracing::warn!("Failed to create data directory: {}", e);
4729 return;
4730 }
4731
4732 for (key, history) in &self.prompt_histories {
4734 let path = self.dir_context.prompt_history_path(key);
4735 if let Err(e) = history.save_to_file(&path) {
4736 tracing::warn!("Failed to save {} history: {}", key, e);
4737 } else {
4738 tracing::debug!("Saved {} history to {:?}", key, path);
4739 }
4740 }
4741 }
4742
4743 pub(super) fn ensure_active_tab_visible(
4747 &mut self,
4748 split_id: LeafId,
4749 active_buffer: BufferId,
4750 available_width: u16,
4751 ) {
4752 tracing::debug!(
4753 "ensure_active_tab_visible called: split={:?}, buffer={:?}, width={}",
4754 split_id,
4755 active_buffer,
4756 available_width
4757 );
4758 let Some(view_state) = self.split_view_states.get_mut(&split_id) else {
4759 tracing::debug!(" -> no view_state for split");
4760 return;
4761 };
4762
4763 let split_buffers = view_state.open_buffers.clone();
4764
4765 let (tab_widths, rendered_buffer_ids) = crate::view::ui::tabs::calculate_tab_widths(
4767 &split_buffers,
4768 &self.buffers,
4769 &self.buffer_metadata,
4770 &self.composite_buffers,
4771 );
4772
4773 let total_tabs_width: usize = tab_widths.iter().sum();
4774 let max_visible_width = available_width as usize;
4775
4776 let active_tab_index = rendered_buffer_ids
4779 .iter()
4780 .position(|id| *id == active_buffer);
4781
4782 let active_width_index = active_tab_index.map(|buf_idx| {
4786 if buf_idx == 0 {
4787 0
4788 } else {
4789 buf_idx * 2
4794 }
4795 });
4796
4797 let old_offset = view_state.tab_scroll_offset;
4799 let new_scroll_offset = if let Some(idx) = active_width_index {
4800 crate::view::ui::tabs::scroll_to_show_tab(
4801 &tab_widths,
4802 idx,
4803 view_state.tab_scroll_offset,
4804 max_visible_width,
4805 )
4806 } else {
4807 view_state
4808 .tab_scroll_offset
4809 .min(total_tabs_width.saturating_sub(max_visible_width))
4810 };
4811
4812 tracing::debug!(
4813 " -> offset: {} -> {} (idx={:?}, max_width={}, total={})",
4814 old_offset,
4815 new_scroll_offset,
4816 active_width_index,
4817 max_visible_width,
4818 total_tabs_width
4819 );
4820 view_state.tab_scroll_offset = new_scroll_offset;
4821 }
4822
4823 fn sync_scroll_groups(&mut self) {
4829 let active_split = self.split_manager.active_split();
4830 let group_count = self.scroll_sync_manager.groups().len();
4831
4832 if group_count > 0 {
4833 tracing::debug!(
4834 "sync_scroll_groups: active_split={:?}, {} groups",
4835 active_split,
4836 group_count
4837 );
4838 }
4839
4840 let sync_info: Vec<_> = self
4843 .scroll_sync_manager
4844 .groups()
4845 .iter()
4846 .filter_map(|group| {
4847 tracing::debug!(
4848 "sync_scroll_groups: checking group {}, left={:?}, right={:?}",
4849 group.id,
4850 group.left_split,
4851 group.right_split
4852 );
4853
4854 if !group.contains_split(active_split.into()) {
4855 tracing::debug!(
4856 "sync_scroll_groups: active split {:?} not in group",
4857 active_split
4858 );
4859 return None;
4860 }
4861
4862 let active_top_byte = self
4864 .split_view_states
4865 .get(&active_split)?
4866 .viewport
4867 .top_byte;
4868
4869 let active_buffer_id = self.split_manager.buffer_for_split(active_split)?;
4871 let buffer_state = self.buffers.get(&active_buffer_id)?;
4872 let buffer_len = buffer_state.buffer.len();
4873 let active_line = buffer_state.buffer.get_line_number(active_top_byte);
4874
4875 tracing::debug!(
4876 "sync_scroll_groups: active_split={:?}, buffer_id={:?}, top_byte={}, buffer_len={}, active_line={}",
4877 active_split,
4878 active_buffer_id,
4879 active_top_byte,
4880 buffer_len,
4881 active_line
4882 );
4883
4884 let (other_split, other_line) = if group.is_left_split(active_split.into()) {
4886 (group.right_split, group.left_to_right_line(active_line))
4888 } else {
4889 (group.left_split, group.right_to_left_line(active_line))
4891 };
4892
4893 tracing::debug!(
4894 "sync_scroll_groups: syncing other_split={:?} to line {}",
4895 other_split,
4896 other_line
4897 );
4898
4899 Some((other_split, other_line))
4900 })
4901 .collect();
4902
4903 for (other_split, target_line) in sync_info {
4905 let other_leaf = LeafId(other_split);
4906 if let Some(buffer_id) = self.split_manager.buffer_for_split(other_leaf) {
4907 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4908 let buffer = &mut state.buffer;
4909 if let Some(view_state) = self.split_view_states.get_mut(&other_leaf) {
4910 view_state.viewport.scroll_to(buffer, target_line);
4911 }
4912 }
4913 }
4914 }
4915
4916 let active_buffer_id = if self.same_buffer_scroll_sync {
4926 self.split_manager.buffer_for_split(active_split)
4927 } else {
4928 None
4929 };
4930 if let Some(active_buf_id) = active_buffer_id {
4931 let active_top_byte = self
4932 .split_view_states
4933 .get(&active_split)
4934 .map(|vs| vs.viewport.top_byte);
4935 let active_viewport_height = self
4936 .split_view_states
4937 .get(&active_split)
4938 .map(|vs| vs.viewport.visible_line_count())
4939 .unwrap_or(0);
4940
4941 if let Some(top_byte) = active_top_byte {
4942 let other_splits: Vec<_> = self
4944 .split_view_states
4945 .keys()
4946 .filter(|&&s| {
4947 s != active_split
4948 && self.split_manager.buffer_for_split(s) == Some(active_buf_id)
4949 && !self.scroll_sync_manager.is_split_synced(s.into())
4950 })
4951 .copied()
4952 .collect();
4953
4954 if !other_splits.is_empty() {
4955 let at_bottom = if let Some(state) = self.buffers.get_mut(&active_buf_id) {
4958 let mut iter = state.buffer.line_iterator(top_byte, 80);
4959 let mut lines_remaining = 0;
4960 while iter.next_line().is_some() {
4961 lines_remaining += 1;
4962 if lines_remaining > active_viewport_height {
4963 break;
4964 }
4965 }
4966 lines_remaining <= active_viewport_height
4967 } else {
4968 false
4969 };
4970
4971 for other_split in other_splits {
4972 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
4973 view_state.viewport.top_byte = top_byte;
4974 view_state.viewport.sync_scroll_to_end = at_bottom;
4977 }
4978 }
4979 }
4980 }
4981 }
4982 }
4983
4984 fn pre_sync_ensure_visible(&mut self, active_split: LeafId) {
4993 let group_info = self
4995 .scroll_sync_manager
4996 .find_group_for_split(active_split.into())
4997 .map(|g| (g.left_split, g.right_split));
4998
4999 if let Some((left_split, right_split)) = group_info {
5000 if let Some(buffer_id) = self.split_manager.buffer_for_split(active_split) {
5002 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5003 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
5004 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
5006
5007 tracing::debug!(
5008 "pre_sync_ensure_visible: updated active split {:?} viewport, top_byte={}",
5009 active_split,
5010 view_state.viewport.top_byte
5011 );
5012 }
5013 }
5014 }
5015
5016 let active_sid: SplitId = active_split.into();
5018 let other_split: SplitId = if active_sid == left_split {
5019 right_split
5020 } else {
5021 left_split
5022 };
5023
5024 if let Some(view_state) = self.split_view_states.get_mut(&LeafId(other_split)) {
5025 view_state.viewport.set_skip_ensure_visible();
5026 tracing::debug!(
5027 "pre_sync_ensure_visible: marked other split {:?} to skip ensure_visible",
5028 other_split
5029 );
5030 }
5031 }
5032
5033 if !self.same_buffer_scroll_sync {
5036 } else if let Some(active_buf_id) = self.split_manager.buffer_for_split(active_split) {
5038 let other_same_buffer_splits: Vec<_> = self
5039 .split_view_states
5040 .keys()
5041 .filter(|&&s| {
5042 s != active_split
5043 && self.split_manager.buffer_for_split(s) == Some(active_buf_id)
5044 && !self.scroll_sync_manager.is_split_synced(s.into())
5045 })
5046 .copied()
5047 .collect();
5048
5049 for other_split in other_same_buffer_splits {
5050 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
5051 view_state.viewport.set_skip_ensure_visible();
5052 }
5053 }
5054 }
5055 }
5056}