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::trace_span!("render").entered();
9 let size = frame.area();
10
11 let active_split = self.split_manager.active_split();
16 self.pre_sync_ensure_visible(active_split);
17
18 self.sync_scroll_groups();
21
22 let mut semantic_ranges: std::collections::HashMap<BufferId, (usize, usize)> =
28 std::collections::HashMap::new();
29 for (split_id, view_state) in &self.split_view_states {
30 if let Some(buffer_id) = self.split_manager.get_buffer_id(*split_id) {
31 if let Some(state) = self.buffers.get(&buffer_id) {
32 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
33 let visible_lines = view_state.viewport.visible_line_count().saturating_sub(1);
34 let end_line = start_line.saturating_add(visible_lines);
35 semantic_ranges
36 .entry(buffer_id)
37 .and_modify(|(min_start, max_end)| {
38 *min_start = (*min_start).min(start_line);
39 *max_end = (*max_end).max(end_line);
40 })
41 .or_insert((start_line, end_line));
42 }
43 }
44 }
45 for (buffer_id, (start_line, end_line)) in semantic_ranges {
46 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
47 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
48 }
49
50 for (split_id, view_state) in &self.split_view_states {
51 if let Some(buffer_id) = self.split_manager.get_buffer_id(*split_id) {
52 if let Some(state) = self.buffers.get_mut(&buffer_id) {
53 let top_byte = view_state.viewport.top_byte;
54 let height = view_state.viewport.height;
55 if let Err(e) = state.prepare_for_render(top_byte, height) {
56 tracing::error!("Failed to prepare buffer for render: {}", e);
57 }
59 }
60 }
61 }
62
63 let is_search_prompt_active = self.prompt.as_ref().is_some_and(|p| {
66 matches!(
67 p.prompt_type,
68 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
69 )
70 });
71 if is_search_prompt_active {
72 if let Some(ref search_state) = self.search_state {
73 let query = search_state.query.clone();
74 self.update_search_highlights(&query);
75 }
76 }
77
78 let show_search_options = self.prompt.as_ref().is_some_and(|p| {
80 matches!(
81 p.prompt_type,
82 PromptType::Search
83 | PromptType::ReplaceSearch
84 | PromptType::Replace { .. }
85 | PromptType::QueryReplaceSearch
86 | PromptType::QueryReplace { .. }
87 )
88 });
89
90 let has_suggestions = self
92 .prompt
93 .as_ref()
94 .is_some_and(|p| !p.suggestions.is_empty());
95 let has_file_browser = self.prompt.as_ref().is_some_and(|p| {
96 matches!(
97 p.prompt_type,
98 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
99 )
100 }) && self.file_open_state.is_some();
101
102 let constraints = vec![
106 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }), Constraint::Min(0), Constraint::Length(if has_suggestions || has_file_browser {
109 0
110 } else {
111 1
112 }), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(1), ];
116
117 let main_chunks = Layout::default()
118 .direction(Direction::Vertical)
119 .constraints(constraints)
120 .split(size);
121
122 let menu_bar_area = main_chunks[0];
123 let main_content_area = main_chunks[1];
124 let status_bar_idx = 2;
125 let search_options_idx = 3;
126 let prompt_line_idx = 4;
127
128 let editor_content_area;
131 let file_explorer_should_show = self.file_explorer_visible
132 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
133
134 if file_explorer_should_show {
135 tracing::trace!(
137 "render: file explorer layout active (present={}, sync_in_progress={})",
138 self.file_explorer.is_some(),
139 self.file_explorer_sync_in_progress
140 );
141 let explorer_percent = (self.file_explorer_width_percent * 100.0) as u16;
143 let editor_percent = 100 - explorer_percent;
144 let horizontal_chunks = Layout::default()
145 .direction(Direction::Horizontal)
146 .constraints([
147 Constraint::Percentage(explorer_percent), Constraint::Percentage(editor_percent), ])
150 .split(main_content_area);
151
152 self.cached_layout.file_explorer_area = Some(horizontal_chunks[0]);
153 editor_content_area = horizontal_chunks[1];
154
155 let remote_connection = self.remote_connection_info().map(|s| s.to_string());
157
158 if let Some(ref mut explorer) = self.file_explorer {
160 let is_focused = self.key_context == KeyContext::FileExplorer;
161
162 let mut files_with_unsaved_changes = std::collections::HashSet::new();
164 for (buffer_id, state) in &self.buffers {
165 if state.buffer.is_modified() {
166 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
167 if let Some(file_path) = metadata.file_path() {
168 files_with_unsaved_changes.insert(file_path.clone());
169 }
170 }
171 }
172 }
173
174 let close_button_hovered = matches!(
175 &self.mouse_state.hover_target,
176 Some(HoverTarget::FileExplorerCloseButton)
177 );
178 FileExplorerRenderer::render(
179 explorer,
180 frame,
181 horizontal_chunks[0],
182 is_focused,
183 &files_with_unsaved_changes,
184 &self.file_explorer_decoration_cache,
185 &self.keybindings,
186 self.key_context,
187 &self.theme,
188 close_button_hovered,
189 remote_connection.as_deref(),
190 );
191 }
192 } else {
195 self.cached_layout.file_explorer_area = None;
197 editor_content_area = main_content_area;
198 }
199
200 if self.plugin_manager.is_active() {
207 let hooks_start = std::time::Instant::now();
208 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
210
211 let mut total_new_lines = 0usize;
212 for (split_id, buffer_id, split_area) in visible_buffers {
213 let viewport_top_byte = self
215 .split_view_states
216 .get(&split_id)
217 .map(|vs| vs.viewport.top_byte)
218 .unwrap_or(0);
219
220 if let Some(state) = self.buffers.get_mut(&buffer_id) {
221 self.plugin_manager.run_hook(
223 "render_start",
224 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
225 );
226
227 let visible_count = split_area.height as usize;
230 let is_binary = state.buffer.is_binary();
231 let line_ending = state.buffer.line_ending();
232 let base_tokens =
233 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
234 &mut state.buffer,
235 viewport_top_byte,
236 self.config.editor.estimated_line_length,
237 visible_count,
238 is_binary,
239 line_ending,
240 );
241 let viewport_start = viewport_top_byte;
242 let viewport_end = base_tokens
243 .last()
244 .and_then(|t| t.source_offset)
245 .unwrap_or(viewport_start);
246 self.plugin_manager.run_hook(
247 "view_transform_request",
248 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
249 buffer_id,
250 split_id,
251 viewport_start,
252 viewport_end,
253 tokens: base_tokens,
254 },
255 );
256
257 let visible_count = split_area.height as usize;
259 let top_byte = viewport_top_byte;
260
261 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
263
264 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
266 let mut line_number = state.buffer.get_line_number(top_byte);
267 let mut iter = state
268 .buffer
269 .line_iterator(top_byte, self.config.editor.estimated_line_length);
270
271 for _ in 0..visible_count {
272 if let Some((line_start, line_content)) = iter.next_line() {
273 let byte_end = line_start + line_content.len();
274 let byte_range = (line_start, byte_end);
275
276 if !seen_byte_ranges.contains(&byte_range) {
278 new_lines.push(crate::services::plugins::hooks::LineInfo {
279 line_number,
280 byte_start: line_start,
281 byte_end,
282 content: line_content,
283 });
284 seen_byte_ranges.insert(byte_range);
285 }
286 line_number += 1;
287 } else {
288 break;
289 }
290 }
291
292 if !new_lines.is_empty() {
294 total_new_lines += new_lines.len();
295 self.plugin_manager.run_hook(
296 "lines_changed",
297 crate::services::plugins::hooks::HookArgs::LinesChanged {
298 buffer_id,
299 lines: new_lines,
300 },
301 );
302 }
303 }
304 }
305 let hooks_elapsed = hooks_start.elapsed();
306 tracing::trace!(
307 new_lines = total_new_lines,
308 elapsed_ms = hooks_elapsed.as_millis(),
309 elapsed_us = hooks_elapsed.as_micros(),
310 "lines_changed hooks total"
311 );
312
313 let commands = self.plugin_manager.process_commands();
315 for command in commands {
316 if let Err(e) = self.handle_plugin_command(command) {
317 tracing::error!("Error handling plugin command: {}", e);
318 }
319 }
320 }
321
322 let lsp_waiting = self.pending_completion_request.is_some()
324 || self.pending_goto_definition_request.is_some();
325
326 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
333 let hide_cursor = self.menu_state.active_menu.is_some()
334 || self.key_context == KeyContext::FileExplorer
335 || self.terminal_mode
336 || settings_visible;
337
338 let hovered_tab = match &self.mouse_state.hover_target {
340 Some(HoverTarget::TabName(buffer_id, split_id)) => Some((*buffer_id, *split_id, false)),
341 Some(HoverTarget::TabCloseButton(buffer_id, split_id)) => {
342 Some((*buffer_id, *split_id, true))
343 }
344 _ => None,
345 };
346
347 let hovered_close_split = match &self.mouse_state.hover_target {
349 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
350 _ => None,
351 };
352
353 let hovered_maximize_split = match &self.mouse_state.hover_target {
355 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
356 _ => None,
357 };
358
359 let is_maximized = self.split_manager.is_maximized();
360
361 let (split_areas, tab_layouts, close_split_areas, maximize_split_areas, view_line_mappings) =
362 SplitRenderer::render_content(
363 frame,
364 editor_content_area,
365 &self.split_manager,
366 &mut self.buffers,
367 &self.buffer_metadata,
368 &mut self.event_logs,
369 &self.composite_buffers,
370 &mut self.composite_view_states,
371 &self.theme,
372 self.ansi_background.as_ref(),
373 self.background_fade,
374 lsp_waiting,
375 self.config.editor.large_file_threshold_bytes,
376 self.config.editor.line_wrap,
377 self.config.editor.estimated_line_length,
378 self.config.editor.highlight_context_bytes,
379 Some(&mut self.split_view_states),
380 hide_cursor,
381 hovered_tab,
382 hovered_close_split,
383 hovered_maximize_split,
384 is_maximized,
385 self.config.editor.relative_line_numbers,
386 self.tab_bar_visible,
387 self.config.editor.use_terminal_bg,
388 );
389
390 if self.plugin_manager.is_active() {
394 for (split_id, view_state) in &self.split_view_states {
395 let current = (
396 view_state.viewport.top_byte,
397 view_state.viewport.width,
398 view_state.viewport.height,
399 );
400 let (changed, previous) = match self.previous_viewports.get(split_id) {
405 Some(previous) => (*previous != current, Some(*previous)),
406 None => (false, None), };
408 tracing::trace!(
409 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
410 split_id,
411 current,
412 previous,
413 changed
414 );
415 if changed {
416 if let Some(buffer_id) = self.split_manager.get_buffer_id(*split_id) {
417 tracing::debug!(
418 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={}",
419 split_id,
420 buffer_id,
421 view_state.viewport.top_byte
422 );
423 self.plugin_manager.run_hook(
424 "viewport_changed",
425 crate::services::plugins::hooks::HookArgs::ViewportChanged {
426 split_id: *split_id,
427 buffer_id,
428 top_byte: view_state.viewport.top_byte,
429 width: view_state.viewport.width,
430 height: view_state.viewport.height,
431 },
432 );
433 }
434 }
435 }
436 }
437
438 self.previous_viewports.clear();
440 for (split_id, view_state) in &self.split_view_states {
441 self.previous_viewports.insert(
442 *split_id,
443 (
444 view_state.viewport.top_byte,
445 view_state.viewport.width,
446 view_state.viewport.height,
447 ),
448 );
449 }
450
451 self.render_terminal_splits(frame, &split_areas);
453
454 self.cached_layout.split_areas = split_areas;
455 self.cached_layout.tab_layouts = tab_layouts;
456 self.cached_layout.close_split_areas = close_split_areas;
457 self.cached_layout.maximize_split_areas = maximize_split_areas;
458 self.cached_layout.view_line_mappings = view_line_mappings;
459 self.cached_layout.separator_areas = self
460 .split_manager
461 .get_separators_with_ids(editor_content_area);
462 self.cached_layout.editor_content_area = Some(editor_content_area);
463
464 self.render_hover_highlights(frame);
466
467 self.cached_layout.suggestions_area = None;
469 self.file_browser_layout = None;
470 if let Some(prompt) = &self.prompt {
471 if matches!(
473 prompt.prompt_type,
474 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
475 ) {
476 if let Some(file_open_state) = &self.file_open_state {
477 let max_height = main_chunks[prompt_line_idx].y.saturating_sub(1).min(20);
479 let popup_area = ratatui::layout::Rect {
480 x: 0,
481 y: main_chunks[prompt_line_idx].y.saturating_sub(max_height),
482 width: size.width,
483 height: max_height,
484 };
485
486 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
487 frame,
488 popup_area,
489 file_open_state,
490 &self.theme,
491 &self.mouse_state.hover_target,
492 Some(&self.keybindings),
493 );
494 }
495 } else if !prompt.suggestions.is_empty() {
496 let suggestion_count = prompt.suggestions.len().min(10);
499 let is_quick_open =
500 prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
501 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
502 let height = suggestion_count as u16 + 2 + hints_height; let suggestions_area = ratatui::layout::Rect {
507 x: 0,
508 y: main_chunks[prompt_line_idx].y.saturating_sub(height),
509 width: size.width,
510 height: height - hints_height,
511 };
512
513 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
515
516 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
517 frame,
518 suggestions_area,
519 prompt,
520 &self.theme,
521 self.mouse_state.hover_target.as_ref(),
522 );
523
524 if is_quick_open {
526 let hints_area = ratatui::layout::Rect {
527 x: 0,
528 y: main_chunks[prompt_line_idx].y.saturating_sub(hints_height),
529 width: size.width,
530 height: hints_height,
531 };
532 frame.render_widget(ratatui::widgets::Clear, hints_area);
533 Self::render_quick_open_hints(frame, hints_area, &self.theme);
534 }
535 }
536 }
537
538 let display_name = self
540 .buffer_metadata
541 .get(&self.active_buffer())
542 .map(|m| m.display_name.clone())
543 .unwrap_or_else(|| "[No Name]".to_string());
544 let status_message = self.status_message.clone();
545 let plugin_status_message = self.plugin_status_message.clone();
546 let prompt = self.prompt.clone();
547 let lsp_status = self.lsp_status.clone();
548 let theme = self.theme.clone();
549 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());
554
555 if !has_suggestions && !has_file_browser {
557 let (warning_level, general_warning_count) =
559 if self.config.warnings.show_status_indicator {
560 (
561 self.get_effective_warning_level(),
562 self.get_general_warning_count(),
563 )
564 } else {
565 (WarningLevel::None, 0)
566 };
567
568 use crate::view::ui::status_bar::StatusBarHover;
570 let status_bar_hover = match &self.mouse_state.hover_target {
571 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
572 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
573 Some(HoverTarget::StatusBarLineEndingIndicator) => {
574 StatusBarHover::LineEndingIndicator
575 }
576 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
577 _ => StatusBarHover::None,
578 };
579
580 let remote_connection = self.remote_connection_info().map(|s| s.to_string());
582
583 let status_bar_layout = StatusBarRenderer::render_status_bar(
584 frame,
585 main_chunks[status_bar_idx],
586 self.active_state_mut(), &status_message,
588 &plugin_status_message,
589 &lsp_status,
590 &theme,
591 &display_name,
592 &keybindings_cloned, &chord_state_cloned, update_available.as_deref(), warning_level, general_warning_count, status_bar_hover, remote_connection.as_deref(), );
600
601 let status_bar_area = main_chunks[status_bar_idx];
603 self.cached_layout.status_bar_area =
604 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
605 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
606 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
607 self.cached_layout.status_bar_line_ending_area =
608 status_bar_layout.line_ending_indicator;
609 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
610 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
611 }
612
613 if show_search_options {
615 let confirm_each = self.prompt.as_ref().and_then(|p| {
617 if matches!(
618 p.prompt_type,
619 PromptType::ReplaceSearch
620 | PromptType::Replace { .. }
621 | PromptType::QueryReplaceSearch
622 | PromptType::QueryReplace { .. }
623 ) {
624 Some(self.search_confirm_each)
625 } else {
626 None
627 }
628 });
629
630 use crate::view::ui::status_bar::SearchOptionsHover;
632 let search_options_hover = match &self.mouse_state.hover_target {
633 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
634 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
635 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
636 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
637 _ => SearchOptionsHover::None,
638 };
639
640 let search_options_layout = StatusBarRenderer::render_search_options(
641 frame,
642 main_chunks[search_options_idx],
643 self.search_case_sensitive,
644 self.search_whole_word,
645 self.search_use_regex,
646 confirm_each,
647 &theme,
648 &keybindings_cloned,
649 search_options_hover,
650 );
651 self.cached_layout.search_options_layout = Some(search_options_layout);
652 } else {
653 self.cached_layout.search_options_layout = None;
654 }
655
656 if let Some(prompt) = &prompt {
658 if matches!(
660 prompt.prompt_type,
661 crate::view::prompt::PromptType::OpenFile
662 | crate::view::prompt::PromptType::SwitchProject
663 ) {
664 if let Some(file_open_state) = &self.file_open_state {
665 StatusBarRenderer::render_file_open_prompt(
666 frame,
667 main_chunks[prompt_line_idx],
668 prompt,
669 file_open_state,
670 &theme,
671 );
672 } else {
673 StatusBarRenderer::render_prompt(
674 frame,
675 main_chunks[prompt_line_idx],
676 prompt,
677 &theme,
678 );
679 }
680 } else {
681 StatusBarRenderer::render_prompt(
682 frame,
683 main_chunks[prompt_line_idx],
684 prompt,
685 &theme,
686 );
687 }
688 }
689
690 let theme_clone = self.theme.clone();
693 let hover_target = self.mouse_state.hover_target.clone();
694
695 self.cached_layout.popup_areas.clear();
697
698 let popup_info: Vec<_> = {
700 let active_split = self.split_manager.active_split();
702 let viewport = self
703 .split_view_states
704 .get(&active_split)
705 .map(|vs| vs.viewport.clone());
706
707 let state = self.active_state_mut();
708 if state.popups.is_visible() {
709 let primary_cursor = state.cursors.primary();
711 let cursor_screen_pos = viewport
712 .as_ref()
713 .map(|vp| vp.cursor_screen_position(&mut state.buffer, primary_cursor))
714 .unwrap_or((0, 0));
715
716 let cursor_screen_pos = (cursor_screen_pos.0, cursor_screen_pos.1 + 1);
718
719 state
721 .popups
722 .all()
723 .iter()
724 .enumerate()
725 .map(|(popup_idx, popup)| {
726 let popup_area = popup.calculate_area(size, Some(cursor_screen_pos));
727
728 let desc_height = popup.description_height();
731 let inner_area = if popup.bordered {
732 ratatui::layout::Rect {
733 x: popup_area.x + 1,
734 y: popup_area.y + 1 + desc_height,
735 width: popup_area.width.saturating_sub(2),
736 height: popup_area.height.saturating_sub(2 + desc_height),
737 }
738 } else {
739 ratatui::layout::Rect {
740 x: popup_area.x,
741 y: popup_area.y + desc_height,
742 width: popup_area.width,
743 height: popup_area.height.saturating_sub(desc_height),
744 }
745 };
746
747 let num_items = match &popup.content {
748 crate::view::popup::PopupContent::List { items, .. } => items.len(),
749 _ => 0,
750 };
751
752 let total_lines = popup.item_count();
754 let visible_lines = inner_area.height as usize;
755 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
756 {
757 Some(ratatui::layout::Rect {
758 x: inner_area.x + inner_area.width - 1,
759 y: inner_area.y,
760 width: 1,
761 height: inner_area.height,
762 })
763 } else {
764 None
765 };
766
767 (
768 popup_idx,
769 popup_area,
770 inner_area,
771 popup.scroll_offset,
772 num_items,
773 scrollbar_rect,
774 total_lines,
775 )
776 })
777 .collect()
778 } else {
779 Vec::new()
780 }
781 };
782
783 self.cached_layout.popup_areas = popup_info.clone();
785
786 let state = self.active_state_mut();
788 if state.popups.is_visible() {
789 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
790 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
791 popup.render_with_hover(
792 frame,
793 *popup_area,
794 &theme_clone,
795 hover_target.as_ref(),
796 );
797 }
798 }
799 }
800
801 self.update_menu_context();
804
805 let settings_visible = self
808 .settings_state
809 .as_ref()
810 .map(|s| s.visible)
811 .unwrap_or(false);
812 if settings_visible {
813 crate::view::dimming::apply_dimming(frame, size);
815 }
816 if let Some(ref mut settings_state) = self.settings_state {
817 if settings_state.visible {
818 settings_state.update_focus_states();
819 let settings_layout = crate::view::settings::render_settings(
820 frame,
821 size,
822 settings_state,
823 &self.theme,
824 );
825 self.cached_layout.settings_layout = Some(settings_layout);
826 }
827 }
828
829 if let Some(ref wizard) = self.calibration_wizard {
831 crate::view::dimming::apply_dimming(frame, size);
833 crate::view::calibration_wizard::render_calibration_wizard(
834 frame,
835 size,
836 wizard,
837 &self.theme,
838 );
839 }
840
841 if let Some(ref debug) = self.event_debug {
843 crate::view::dimming::apply_dimming(frame, size);
845 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
846 }
847
848 if self.menu_bar_visible {
849 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
850 frame,
851 menu_bar_area,
852 &self.menus,
853 &self.menu_state,
854 &self.keybindings,
855 &self.theme,
856 self.mouse_state.hover_target.as_ref(),
857 ));
858 } else {
859 self.cached_layout.menu_layout = None;
860 }
861
862 if let Some(ref menu) = self.tab_context_menu {
864 self.render_tab_context_menu(frame, menu);
865 }
866
867 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
869 if drag_state.is_dragging() {
870 self.render_tab_drop_zone(frame, drag_state);
871 }
872 }
873
874 if self.gpm_active {
880 if let Some((col, row)) = self.mouse_cursor_position {
881 use ratatui::style::Modifier;
882
883 if col < size.width && row < size.height {
885 let buf = frame.buffer_mut();
887 if let Some(cell) = buf.cell_mut((col, row)) {
888 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
889 }
890 }
891 }
892 }
893
894 if self.keyboard_capture && self.terminal_mode {
897 let active_split = self.split_manager.active_split();
899 let active_split_area = self
900 .cached_layout
901 .split_areas
902 .iter()
903 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
904 .map(|(_, _, content_rect, _, _, _)| *content_rect);
905
906 if let Some(terminal_area) = active_split_area {
907 self.apply_keyboard_capture_dimming(frame, terminal_area);
908 }
909 }
910
911 crate::view::color_support::convert_buffer_colors(
913 frame.buffer_mut(),
914 self.color_capability,
915 );
916 }
917
918 fn render_quick_open_hints(
920 frame: &mut Frame,
921 area: ratatui::layout::Rect,
922 theme: &crate::view::theme::Theme,
923 ) {
924 use ratatui::style::{Modifier, Style};
925 use ratatui::text::{Line, Span};
926 use ratatui::widgets::Paragraph;
927 use rust_i18n::t;
928
929 let hints_style = Style::default()
930 .fg(theme.line_number_fg)
931 .bg(theme.suggestion_selected_bg)
932 .add_modifier(Modifier::DIM);
933 let hints_text = t!("quick_open.mode_hints");
934 let left_margin = 2;
936 let hints_width = crate::primitives::display_width::str_width(&hints_text);
937 let mut spans = Vec::new();
938 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
939 spans.push(Span::styled(hints_text.to_string(), hints_style));
940 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
941 spans.push(Span::styled(" ".repeat(remaining), hints_style));
942
943 let paragraph = Paragraph::new(Line::from(spans));
944 frame.render_widget(paragraph, area);
945 }
946
947 fn apply_keyboard_capture_dimming(
950 &self,
951 frame: &mut Frame,
952 terminal_area: ratatui::layout::Rect,
953 ) {
954 let size = frame.area();
955 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
956 }
957
958 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
960 use ratatui::style::Style;
961 use ratatui::text::Span;
962 use ratatui::widgets::Paragraph;
963
964 match &self.mouse_state.hover_target {
965 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
966 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
968 if sid == split_id && dir == direction {
969 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
970 match dir {
971 SplitDirection::Horizontal => {
972 let line_text = "─".repeat(*length as usize);
973 let paragraph =
974 Paragraph::new(Span::styled(line_text, hover_style));
975 frame.render_widget(
976 paragraph,
977 ratatui::layout::Rect::new(*x, *y, *length, 1),
978 );
979 }
980 SplitDirection::Vertical => {
981 for offset in 0..*length {
982 let paragraph = Paragraph::new(Span::styled("│", hover_style));
983 frame.render_widget(
984 paragraph,
985 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
986 );
987 }
988 }
989 }
990 }
991 }
992 }
993 Some(HoverTarget::ScrollbarThumb(split_id)) => {
994 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
996 &self.cached_layout.split_areas
997 {
998 if sid == split_id {
999 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
1000 for row_offset in *thumb_start..*thumb_end {
1001 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
1002 frame.render_widget(
1003 paragraph,
1004 ratatui::layout::Rect::new(
1005 scrollbar_rect.x,
1006 scrollbar_rect.y + row_offset as u16,
1007 1,
1008 1,
1009 ),
1010 );
1011 }
1012 }
1013 }
1014 }
1015 Some(HoverTarget::ScrollbarTrack(split_id)) => {
1016 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1018 &self.cached_layout.split_areas
1019 {
1020 if sid == split_id {
1021 let track_hover_style =
1022 Style::default().bg(self.theme.scrollbar_track_hover_fg);
1023 let thumb_style = Style::default().bg(self.theme.scrollbar_thumb_fg);
1024 for row_offset in 0..scrollbar_rect.height {
1025 let is_thumb = (row_offset as usize) >= *thumb_start
1026 && (row_offset as usize) < *thumb_end;
1027 let style = if is_thumb {
1028 thumb_style
1029 } else {
1030 track_hover_style
1031 };
1032 let paragraph = Paragraph::new(Span::styled(" ", style));
1033 frame.render_widget(
1034 paragraph,
1035 ratatui::layout::Rect::new(
1036 scrollbar_rect.x,
1037 scrollbar_rect.y + row_offset,
1038 1,
1039 1,
1040 ),
1041 );
1042 }
1043 }
1044 }
1045 }
1046 Some(HoverTarget::FileExplorerBorder) => {
1047 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1049 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1050 let border_x = explorer_area.x + explorer_area.width;
1051 for row_offset in 0..explorer_area.height {
1052 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1053 frame.render_widget(
1054 paragraph,
1055 ratatui::layout::Rect::new(
1056 border_x,
1057 explorer_area.y + row_offset,
1058 1,
1059 1,
1060 ),
1061 );
1062 }
1063 }
1064 }
1065 _ => {}
1067 }
1068 }
1069
1070 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
1072 use ratatui::style::Style;
1073 use ratatui::text::{Line, Span};
1074 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1075
1076 let items = super::types::TabContextMenuItem::all();
1077 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
1082 let screen_height = frame.area().height;
1083
1084 let menu_x = if menu.position.0 + menu_width > screen_width {
1085 screen_width.saturating_sub(menu_width)
1086 } else {
1087 menu.position.0
1088 };
1089
1090 let menu_y = if menu.position.1 + menu_height > screen_height {
1091 screen_height.saturating_sub(menu_height)
1092 } else {
1093 menu.position.1
1094 };
1095
1096 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1097
1098 frame.render_widget(Clear, area);
1100
1101 let mut lines = Vec::new();
1103 for (idx, item) in items.iter().enumerate() {
1104 let is_highlighted = idx == menu.highlighted;
1105
1106 let style = if is_highlighted {
1107 Style::default()
1108 .fg(self.theme.menu_highlight_fg)
1109 .bg(self.theme.menu_highlight_bg)
1110 } else {
1111 Style::default()
1112 .fg(self.theme.menu_dropdown_fg)
1113 .bg(self.theme.menu_dropdown_bg)
1114 };
1115
1116 let label = item.label();
1118 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1120
1121 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1122 }
1123
1124 let block = Block::default()
1125 .borders(Borders::ALL)
1126 .border_style(Style::default().fg(self.theme.menu_border_fg))
1127 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1128
1129 let paragraph = Paragraph::new(lines).block(block);
1130 frame.render_widget(paragraph, area);
1131 }
1132
1133 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
1135 use ratatui::style::Modifier;
1136
1137 let Some(ref drop_zone) = drag_state.drop_zone else {
1138 return;
1139 };
1140
1141 let split_id = drop_zone.split_id();
1142
1143 let split_area = self
1145 .cached_layout
1146 .split_areas
1147 .iter()
1148 .find(|(sid, _, _, _, _, _)| *sid == split_id)
1149 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1150
1151 let Some(content_rect) = split_area else {
1152 return;
1153 };
1154
1155 use super::types::TabDropZone;
1157
1158 let highlight_area = match drop_zone {
1159 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
1160 content_rect
1163 }
1164 TabDropZone::SplitLeft(_) => {
1165 let width = (content_rect.width / 2).max(3);
1167 ratatui::layout::Rect::new(
1168 content_rect.x,
1169 content_rect.y,
1170 width,
1171 content_rect.height,
1172 )
1173 }
1174 TabDropZone::SplitRight(_) => {
1175 let width = (content_rect.width / 2).max(3);
1177 let x = content_rect.x + content_rect.width - width;
1178 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
1179 }
1180 TabDropZone::SplitTop(_) => {
1181 let height = (content_rect.height / 2).max(2);
1183 ratatui::layout::Rect::new(
1184 content_rect.x,
1185 content_rect.y,
1186 content_rect.width,
1187 height,
1188 )
1189 }
1190 TabDropZone::SplitBottom(_) => {
1191 let height = (content_rect.height / 2).max(2);
1193 let y = content_rect.y + content_rect.height - height;
1194 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
1195 }
1196 };
1197
1198 let buf = frame.buffer_mut();
1201 let drop_zone_bg = self.theme.tab_drop_zone_bg;
1202 let drop_zone_border = self.theme.tab_drop_zone_border;
1203
1204 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1206 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1207 if let Some(cell) = buf.cell_mut((x, y)) {
1208 cell.set_bg(drop_zone_bg);
1211
1212 let is_border = x == highlight_area.x
1214 || x == highlight_area.x + highlight_area.width - 1
1215 || y == highlight_area.y
1216 || y == highlight_area.y + highlight_area.height - 1;
1217
1218 if is_border {
1219 cell.set_fg(drop_zone_border);
1220 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
1221 }
1222 }
1223 }
1224 }
1225
1226 match drop_zone {
1228 TabDropZone::SplitLeft(_) => {
1229 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1231 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
1232 cell.set_symbol("▌");
1233 cell.set_fg(drop_zone_border);
1234 }
1235 }
1236 }
1237 TabDropZone::SplitRight(_) => {
1238 let x = highlight_area.x + highlight_area.width - 1;
1240 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1241 if let Some(cell) = buf.cell_mut((x, y)) {
1242 cell.set_symbol("▐");
1243 cell.set_fg(drop_zone_border);
1244 }
1245 }
1246 }
1247 TabDropZone::SplitTop(_) => {
1248 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1250 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
1251 cell.set_symbol("▀");
1252 cell.set_fg(drop_zone_border);
1253 }
1254 }
1255 }
1256 TabDropZone::SplitBottom(_) => {
1257 let y = highlight_area.y + highlight_area.height - 1;
1259 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1260 if let Some(cell) = buf.cell_mut((x, y)) {
1261 cell.set_symbol("▄");
1262 cell.set_fg(drop_zone_border);
1263 }
1264 }
1265 }
1266 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
1267 }
1269 }
1270 }
1271
1272 pub fn add_overlay(
1276 &mut self,
1277 namespace: Option<crate::view::overlay::OverlayNamespace>,
1278 range: Range<usize>,
1279 face: crate::model::event::OverlayFace,
1280 priority: i32,
1281 message: Option<String>,
1282 ) -> crate::view::overlay::OverlayHandle {
1283 let event = Event::AddOverlay {
1284 namespace,
1285 range,
1286 face,
1287 priority,
1288 message,
1289 extend_to_line_end: false,
1290 };
1291 self.apply_event_to_active_buffer(&event);
1292 let state = self.active_state();
1294 state
1295 .overlays
1296 .all()
1297 .last()
1298 .map(|o| o.handle.clone())
1299 .unwrap_or_default()
1300 }
1301
1302 pub fn remove_overlay(&mut self, handle: crate::view::overlay::OverlayHandle) {
1304 let event = Event::RemoveOverlay { handle };
1305 self.apply_event_to_active_buffer(&event);
1306 }
1307
1308 pub fn remove_overlays_in_range(&mut self, range: Range<usize>) {
1310 let event = Event::RemoveOverlaysInRange { range };
1311 self.active_event_log_mut().append(event.clone());
1312 self.apply_event_to_active_buffer(&event);
1313 }
1314
1315 pub fn clear_overlays(&mut self) {
1317 let event = Event::ClearOverlays;
1318 self.active_event_log_mut().append(event.clone());
1319 self.apply_event_to_active_buffer(&event);
1320 }
1321
1322 pub fn show_popup(&mut self, popup: crate::model::event::PopupData) {
1326 let event = Event::ShowPopup { popup };
1327 self.active_event_log_mut().append(event.clone());
1328 self.apply_event_to_active_buffer(&event);
1329 }
1330
1331 pub fn hide_popup(&mut self) {
1333 let event = Event::HidePopup;
1334 self.active_event_log_mut().append(event.clone());
1335 self.apply_event_to_active_buffer(&event);
1336
1337 if let Some(handle) = self.hover_symbol_overlay.take() {
1339 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1340 self.apply_event_to_active_buffer(&remove_overlay_event);
1341 }
1342 self.hover_symbol_range = None;
1343 }
1344
1345 pub(super) fn dismiss_transient_popups(&mut self) {
1348 let is_transient_popup = self
1349 .active_state()
1350 .popups
1351 .top()
1352 .is_some_and(|p| p.transient);
1353
1354 if is_transient_popup {
1355 self.hide_popup();
1356 tracing::trace!("Dismissed transient popup");
1357 }
1358 }
1359
1360 pub(super) fn scroll_popup(&mut self, delta: i32) {
1363 if let Some(popup) = self.active_state_mut().popups.top_mut() {
1364 popup.scroll_by(delta);
1365 tracing::debug!(
1366 "Scrolled popup by {}, new offset: {}",
1367 delta,
1368 popup.scroll_offset
1369 );
1370 }
1371 }
1372
1373 pub(super) fn on_editor_focus_lost(&mut self) {
1381 self.active_state_mut().on_focus_lost();
1383
1384 self.mouse_state.lsp_hover_state = None;
1386 self.mouse_state.lsp_hover_request_sent = false;
1387 self.pending_hover_request = None;
1388
1389 if let Some(handle) = self.hover_symbol_overlay.take() {
1391 let remove_overlay_event = crate::model::event::Event::RemoveOverlay { handle };
1392 self.apply_event_to_active_buffer(&remove_overlay_event);
1393 }
1394 self.hover_symbol_range = None;
1395 }
1396
1397 pub fn clear_popups(&mut self) {
1399 let event = Event::ClearPopups;
1400 self.active_event_log_mut().append(event.clone());
1401 self.apply_event_to_active_buffer(&event);
1402 }
1403
1404 pub fn show_lsp_confirmation_popup(&mut self, language: &str) {
1411 use crate::model::event::{
1412 PopupContentData, PopupData, PopupListItemData, PopupPositionData,
1413 };
1414
1415 self.pending_lsp_confirmation = Some(language.to_string());
1417
1418 let server_info = if let Some(lsp) = &self.lsp {
1420 if let Some(config) = lsp.get_config(language) {
1421 if !config.command.is_empty() {
1422 format!("{} ({})", language, config.command)
1423 } else {
1424 language.to_string()
1425 }
1426 } else {
1427 language.to_string()
1428 }
1429 } else {
1430 language.to_string()
1431 };
1432
1433 let popup = PopupData {
1434 title: Some(format!("Start LSP Server: {}?", server_info)),
1435 description: None,
1436 transient: false,
1437 content: PopupContentData::List {
1438 items: vec![
1439 PopupListItemData {
1440 text: "Allow this time".to_string(),
1441 detail: Some("Start the LSP server for this session".to_string()),
1442 icon: None,
1443 data: Some("allow_once".to_string()),
1444 },
1445 PopupListItemData {
1446 text: "Always allow".to_string(),
1447 detail: Some("Always start this LSP server automatically".to_string()),
1448 icon: None,
1449 data: Some("allow_always".to_string()),
1450 },
1451 PopupListItemData {
1452 text: "Don't start".to_string(),
1453 detail: Some("Cancel LSP server startup".to_string()),
1454 icon: None,
1455 data: Some("deny".to_string()),
1456 },
1457 ],
1458 selected: 0,
1459 },
1460 position: PopupPositionData::Centered,
1461 width: 50,
1462 max_height: 8,
1463 bordered: true,
1464 };
1465
1466 self.show_popup(popup);
1467 }
1468
1469 pub fn handle_lsp_confirmation_response(&mut self, action: &str) -> bool {
1477 let Some(language) = self.pending_lsp_confirmation.take() else {
1478 return false;
1479 };
1480
1481 match action {
1482 "allow_once" => {
1483 if let Some(lsp) = &mut self.lsp {
1485 lsp.allow_language(&language);
1487 if lsp.force_spawn(&language).is_some() {
1489 tracing::info!("LSP server for {} started (allowed once)", language);
1490 self.set_status_message(
1491 t!("lsp.server_started", language = language).to_string(),
1492 );
1493 } else {
1494 self.set_status_message(
1495 t!("lsp.failed_to_start", language = language).to_string(),
1496 );
1497 }
1498 }
1499 self.notify_lsp_current_file_opened(&language);
1501 }
1502 "allow_always" => {
1503 if let Some(lsp) = &mut self.lsp {
1505 lsp.allow_language(&language);
1506 if lsp.force_spawn(&language).is_some() {
1508 tracing::info!("LSP server for {} started (always allowed)", language);
1509 self.set_status_message(
1510 t!("lsp.server_started_auto", language = language).to_string(),
1511 );
1512 } else {
1513 self.set_status_message(
1514 t!("lsp.failed_to_start", language = language).to_string(),
1515 );
1516 }
1517 }
1518 self.notify_lsp_current_file_opened(&language);
1520 }
1521 _ => {
1522 tracing::info!("LSP server for {} startup declined by user", language);
1524 self.set_status_message(
1525 t!("lsp.startup_cancelled", language = language).to_string(),
1526 );
1527 }
1528 }
1529
1530 true
1531 }
1532
1533 fn notify_lsp_current_file_opened(&mut self, language: &str) {
1538 let metadata = match self.buffer_metadata.get(&self.active_buffer()) {
1540 Some(m) => m,
1541 None => {
1542 tracing::debug!(
1543 "notify_lsp_current_file_opened: no metadata for buffer {:?}",
1544 self.active_buffer()
1545 );
1546 return;
1547 }
1548 };
1549
1550 if !metadata.lsp_enabled {
1551 tracing::debug!("notify_lsp_current_file_opened: LSP disabled for this buffer");
1552 return;
1553 }
1554
1555 let uri = match metadata.file_uri() {
1557 Some(u) => u.clone(),
1558 None => {
1559 tracing::debug!(
1560 "notify_lsp_current_file_opened: no URI for buffer (not a file or URI creation failed)"
1561 );
1562 return;
1563 }
1564 };
1565
1566 let path = match metadata.file_path() {
1568 Some(p) => p,
1569 None => {
1570 tracing::debug!("notify_lsp_current_file_opened: no file path for buffer");
1571 return;
1572 }
1573 };
1574
1575 let file_language = match detect_language(path, &self.config.languages) {
1576 Some(l) => l,
1577 None => {
1578 tracing::debug!(
1579 "notify_lsp_current_file_opened: no language detected for {:?}",
1580 path
1581 );
1582 return;
1583 }
1584 };
1585
1586 if file_language != language {
1588 tracing::debug!(
1589 "notify_lsp_current_file_opened: file language {} doesn't match server {}",
1590 file_language,
1591 language
1592 );
1593 return;
1594 }
1595
1596 let active_buffer = self.active_buffer();
1598 let (text, line_count) = if let Some(state) = self.buffers.get(&active_buffer) {
1599 let text = match state.buffer.to_string() {
1600 Some(t) => t,
1601 None => {
1602 tracing::debug!("notify_lsp_current_file_opened: buffer not fully loaded");
1603 return;
1604 }
1605 };
1606 let line_count = state.buffer.line_count().unwrap_or(1000);
1607 (text, line_count)
1608 } else {
1609 tracing::debug!("notify_lsp_current_file_opened: no buffer state");
1610 return;
1611 };
1612
1613 if let Some(lsp) = &mut self.lsp {
1615 if let Some(client) = lsp.force_spawn(language) {
1616 tracing::info!("Sending didOpen to newly started LSP for: {}", uri.as_str());
1617 if let Err(e) = client.did_open(uri.clone(), text, file_language) {
1618 tracing::warn!("Failed to send didOpen to LSP: {}", e);
1619 } else {
1620 tracing::info!("Successfully sent didOpen to LSP after confirmation");
1621
1622 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
1624 let request_id = self.next_lsp_request_id;
1625 self.next_lsp_request_id += 1;
1626
1627 if let Err(e) =
1628 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
1629 {
1630 tracing::debug!(
1631 "Failed to request pull diagnostics (server may not support): {}",
1632 e
1633 );
1634 }
1635
1636 if self.config.editor.enable_inlay_hints {
1638 let request_id = self.next_lsp_request_id;
1639 self.next_lsp_request_id += 1;
1640 self.pending_inlay_hints_request = Some(request_id);
1641
1642 let last_line = line_count.saturating_sub(1) as u32;
1643 let last_char = 10000u32;
1644
1645 if let Err(e) =
1646 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
1647 {
1648 tracing::debug!(
1649 "Failed to request inlay hints (server may not support): {}",
1650 e
1651 );
1652 self.pending_inlay_hints_request = None;
1653 }
1654 }
1655 }
1656 }
1657 }
1658 }
1659
1660 pub fn has_pending_lsp_confirmation(&self) -> bool {
1662 self.pending_lsp_confirmation.is_some()
1663 }
1664
1665 pub fn try_get_lsp_with_confirmation(&mut self, language: &str) -> Option<bool> {
1675 use crate::services::lsp::manager::LspSpawnResult;
1676
1677 let result = {
1678 let lsp = self.lsp.as_mut()?;
1679 lsp.try_spawn(language)
1680 };
1681
1682 match result {
1683 LspSpawnResult::Spawned => Some(true),
1684 LspSpawnResult::NotAutoStart => None, LspSpawnResult::Failed => None,
1686 }
1687 }
1688
1689 pub fn popup_select_next(&mut self) {
1691 let event = Event::PopupSelectNext;
1692 self.active_event_log_mut().append(event.clone());
1693 self.apply_event_to_active_buffer(&event);
1694 }
1695
1696 pub fn popup_select_prev(&mut self) {
1698 let event = Event::PopupSelectPrev;
1699 self.active_event_log_mut().append(event.clone());
1700 self.apply_event_to_active_buffer(&event);
1701 }
1702
1703 pub fn popup_page_down(&mut self) {
1705 let event = Event::PopupPageDown;
1706 self.active_event_log_mut().append(event.clone());
1707 self.apply_event_to_active_buffer(&event);
1708 }
1709
1710 pub fn popup_page_up(&mut self) {
1712 let event = Event::PopupPageUp;
1713 self.active_event_log_mut().append(event.clone());
1714 self.apply_event_to_active_buffer(&event);
1715 }
1716
1717 pub(super) fn collect_lsp_changes(&self, event: &Event) -> Vec<TextDocumentContentChangeEvent> {
1723 match event {
1724 Event::Insert { position, text, .. } => {
1725 tracing::trace!(
1726 "collect_lsp_changes: processing Insert at position {}",
1727 position
1728 );
1729 let (line, character) = self
1731 .active_state()
1732 .buffer
1733 .position_to_lsp_position(*position);
1734 let lsp_pos = Position::new(line as u32, character as u32);
1735 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
1736 vec![TextDocumentContentChangeEvent {
1737 range: Some(lsp_range),
1738 range_length: None,
1739 text: text.clone(),
1740 }]
1741 }
1742 Event::Delete { range, .. } => {
1743 tracing::trace!("collect_lsp_changes: processing Delete range {:?}", range);
1744 let (start_line, start_char) = self
1746 .active_state()
1747 .buffer
1748 .position_to_lsp_position(range.start);
1749 let (end_line, end_char) = self
1750 .active_state()
1751 .buffer
1752 .position_to_lsp_position(range.end);
1753 let lsp_range = LspRange::new(
1754 Position::new(start_line as u32, start_char as u32),
1755 Position::new(end_line as u32, end_char as u32),
1756 );
1757 vec![TextDocumentContentChangeEvent {
1758 range: Some(lsp_range),
1759 range_length: None,
1760 text: String::new(),
1761 }]
1762 }
1763 Event::Batch { events, .. } => {
1764 tracing::trace!(
1767 "collect_lsp_changes: processing Batch with {} events",
1768 events.len()
1769 );
1770 let mut all_changes = Vec::new();
1771 for sub_event in events {
1772 all_changes.extend(self.collect_lsp_changes(sub_event));
1773 }
1774 all_changes
1775 }
1776 _ => Vec::new(), }
1778 }
1779
1780 pub(super) fn calculate_event_line_info(&self, event: &Event) -> super::types::EventLineInfo {
1802 match event {
1803 Event::Insert { position, text, .. } => {
1804 let start_line = self.active_state().buffer.get_line_number(*position);
1806
1807 let lines_added = text.matches('\n').count();
1809 let end_line = start_line + lines_added;
1810
1811 super::types::EventLineInfo {
1812 start_line,
1813 end_line,
1814 line_delta: lines_added as i32,
1815 }
1816 }
1817 Event::Delete {
1818 range,
1819 deleted_text,
1820 ..
1821 } => {
1822 let start_line = self.active_state().buffer.get_line_number(range.start);
1824 let end_line = self.active_state().buffer.get_line_number(range.end);
1825
1826 let lines_removed = deleted_text.matches('\n').count();
1828
1829 super::types::EventLineInfo {
1830 start_line,
1831 end_line,
1832 line_delta: -(lines_removed as i32),
1833 }
1834 }
1835 Event::Batch { events, .. } => {
1836 let mut min_line = usize::MAX;
1839 let mut max_line = 0usize;
1840 let mut total_delta = 0i32;
1841
1842 for sub_event in events {
1843 let info = self.calculate_event_line_info(sub_event);
1844 min_line = min_line.min(info.start_line);
1845 max_line = max_line.max(info.end_line);
1846 total_delta += info.line_delta;
1847 }
1848
1849 if min_line == usize::MAX {
1850 min_line = 0;
1851 }
1852
1853 super::types::EventLineInfo {
1854 start_line: min_line,
1855 end_line: max_line,
1856 line_delta: total_delta,
1857 }
1858 }
1859 _ => super::types::EventLineInfo::default(),
1860 }
1861 }
1862
1863 pub(super) fn notify_lsp_save(&mut self) {
1865 let metadata = match self.buffer_metadata.get(&self.active_buffer()) {
1867 Some(m) => m,
1868 None => {
1869 tracing::debug!(
1870 "notify_lsp_save: no metadata for buffer {:?}",
1871 self.active_buffer()
1872 );
1873 return;
1874 }
1875 };
1876
1877 if !metadata.lsp_enabled {
1878 tracing::debug!("notify_lsp_save: LSP disabled for this buffer");
1879 return;
1880 }
1881
1882 let uri = match metadata.file_uri() {
1884 Some(u) => u.clone(),
1885 None => {
1886 tracing::debug!("notify_lsp_save: no URI for buffer");
1887 return;
1888 }
1889 };
1890
1891 let path = match metadata.file_path() {
1893 Some(p) => p,
1894 None => {
1895 tracing::debug!("notify_lsp_save: no file path for buffer");
1896 return;
1897 }
1898 };
1899
1900 let language = match detect_language(path, &self.config.languages) {
1901 Some(l) => l,
1902 None => {
1903 tracing::debug!("notify_lsp_save: no language detected for {:?}", path);
1904 return;
1905 }
1906 };
1907
1908 let full_text = match self.active_state().buffer.to_string() {
1910 Some(t) => t,
1911 None => {
1912 tracing::debug!("notify_lsp_save: buffer not fully loaded");
1913 return;
1914 }
1915 };
1916 tracing::debug!(
1917 "notify_lsp_save: sending didSave to {} (text length: {} bytes)",
1918 uri.as_str(),
1919 full_text.len()
1920 );
1921
1922 if let Some(lsp) = &mut self.lsp {
1924 use crate::services::lsp::manager::LspSpawnResult;
1925 if lsp.try_spawn(&language) != LspSpawnResult::Spawned {
1926 tracing::debug!(
1927 "notify_lsp_save: LSP not running for {} (auto_start disabled)",
1928 language
1929 );
1930 return;
1931 }
1932 if let Some(client) = lsp.get_handle_mut(&language) {
1933 if let Err(e) = client.did_save(uri, Some(full_text)) {
1935 tracing::warn!("Failed to send didSave to LSP: {}", e);
1936 } else {
1937 tracing::info!("Successfully sent didSave to LSP");
1938 }
1939 } else {
1940 tracing::warn!("notify_lsp_save: failed to get LSP client for {}", language);
1941 }
1942 } else {
1943 tracing::debug!("notify_lsp_save: no LSP manager available");
1944 }
1945 }
1946
1947 pub fn action_to_events(&mut self, action: Action) -> Option<Vec<Event>> {
1950 let tab_size = self.config.editor.tab_size;
1951 let auto_indent = self.config.editor.auto_indent;
1952 let estimated_line_length = self.config.editor.estimated_line_length;
1953
1954 let active_split = self.split_manager.active_split();
1956 let viewport_height = self
1957 .split_view_states
1958 .get(&active_split)
1959 .map(|vs| vs.viewport.height)
1960 .unwrap_or(24);
1961
1962 convert_action_to_events(
1963 self.active_state_mut(),
1964 action,
1965 tab_size,
1966 auto_indent,
1967 estimated_line_length,
1968 viewport_height,
1969 )
1970 }
1971
1972 pub(super) fn clear_search_highlights(&mut self) {
1976 self.clear_search_overlays();
1977 self.search_state = None;
1979 }
1980
1981 pub(super) fn clear_search_overlays(&mut self) {
1984 let ns = self.search_namespace.clone();
1985 let state = self.active_state_mut();
1986 state.overlays.clear_namespace(&ns, &mut state.marker_list);
1987 }
1988
1989 pub(super) fn update_search_highlights(&mut self, query: &str) {
1992 if query.is_empty() {
1994 self.clear_search_highlights();
1995 return;
1996 }
1997
1998 let search_bg = self.theme.search_match_bg;
2000 let search_fg = self.theme.search_match_fg;
2001 let case_sensitive = self.search_case_sensitive;
2002 let whole_word = self.search_whole_word;
2003 let use_regex = self.search_use_regex;
2004 let ns = self.search_namespace.clone();
2005
2006 let regex_pattern = if use_regex {
2008 if whole_word {
2009 format!(r"\b{}\b", query)
2010 } else {
2011 query.to_string()
2012 }
2013 } else {
2014 let escaped = regex::escape(query);
2015 if whole_word {
2016 format!(r"\b{}\b", escaped)
2017 } else {
2018 escaped
2019 }
2020 };
2021
2022 let regex = regex::RegexBuilder::new(®ex_pattern)
2024 .case_insensitive(!case_sensitive)
2025 .build();
2026
2027 let regex = match regex {
2028 Ok(r) => r,
2029 Err(_) => {
2030 self.clear_search_highlights();
2032 return;
2033 }
2034 };
2035
2036 let active_split = self.split_manager.active_split();
2038 let (top_byte, visible_height) = self
2039 .split_view_states
2040 .get(&active_split)
2041 .map(|vs| (vs.viewport.top_byte, vs.viewport.height.saturating_sub(2)))
2042 .unwrap_or((0, 20));
2043
2044 let state = self.active_state_mut();
2045
2046 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2048
2049 let visible_start = top_byte;
2051 let mut visible_end = top_byte;
2052
2053 {
2054 let mut line_iter = state.buffer.line_iterator(top_byte, 80);
2055 for _ in 0..visible_height {
2056 if let Some((line_start, line_content)) = line_iter.next_line() {
2057 visible_end = line_start + line_content.len();
2058 } else {
2059 break;
2060 }
2061 }
2062 }
2063
2064 visible_end = visible_end.min(state.buffer.len());
2066
2067 let visible_text = state.get_text_range(visible_start, visible_end);
2069
2070 for mat in regex.find_iter(&visible_text) {
2072 let absolute_pos = visible_start + mat.start();
2073 let match_len = mat.end() - mat.start();
2074
2075 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2077 let overlay = crate::view::overlay::Overlay::with_namespace(
2078 &mut state.marker_list,
2079 absolute_pos..(absolute_pos + match_len),
2080 crate::view::overlay::OverlayFace::Style {
2081 style: search_style,
2082 },
2083 ns.clone(),
2084 )
2085 .with_priority_value(10); state.overlays.add(overlay);
2088 }
2089 }
2090
2091 pub(super) fn perform_search(&mut self, query: &str) {
2093 if query.is_empty() {
2100 self.search_state = None;
2101 self.set_status_message(t!("search.cancelled").to_string());
2102 return;
2103 }
2104
2105 let search_range = self.pending_search_range.take();
2106
2107 let buffer_content = {
2111 let state = self.active_state_mut();
2112 let total_bytes = state.buffer.len();
2113
2114 match state.buffer.get_text_range_mut(0, total_bytes) {
2117 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2118 Err(e) => {
2119 tracing::warn!("Failed to load buffer for search: {}", e);
2120 self.set_status_message(t!("error.buffer_not_loaded").to_string());
2121 return;
2122 }
2123 }
2124 };
2125
2126 let case_sensitive = self.search_case_sensitive;
2128 let whole_word = self.search_whole_word;
2129 let use_regex = self.search_use_regex;
2130
2131 let (search_start, search_end) = if let Some(ref range) = search_range {
2133 (range.start, range.end)
2134 } else {
2135 (0, buffer_content.len())
2136 };
2137
2138 let regex_pattern = if use_regex {
2140 if whole_word {
2141 format!(r"\b{}\b", query)
2142 } else {
2143 query.to_string()
2144 }
2145 } else {
2146 let escaped = regex::escape(query);
2147 if whole_word {
2148 format!(r"\b{}\b", escaped)
2149 } else {
2150 escaped
2151 }
2152 };
2153
2154 let regex = match regex::RegexBuilder::new(®ex_pattern)
2156 .case_insensitive(!case_sensitive)
2157 .build()
2158 {
2159 Ok(r) => r,
2160 Err(e) => {
2161 self.search_state = None;
2162 self.set_status_message(
2163 t!("error.invalid_regex", error = e.to_string()).to_string(),
2164 );
2165 return;
2166 }
2167 };
2168
2169 let search_slice = &buffer_content[search_start..search_end];
2171 let match_ranges: Vec<(usize, usize)> = regex
2172 .find_iter(search_slice)
2173 .map(|m| (search_start + m.start(), m.end() - m.start()))
2174 .collect();
2175
2176 if match_ranges.is_empty() {
2177 self.search_state = None;
2178 let msg = if search_range.is_some() {
2179 format!("No matches found for '{}' in selection", query)
2180 } else {
2181 format!("No matches found for '{}'", query)
2182 };
2183 self.set_status_message(msg);
2184 return;
2185 }
2186
2187 let matches: Vec<usize> = match_ranges.iter().map(|(pos, _)| *pos).collect();
2189
2190 {
2193 let search_bg = self.theme.search_match_bg;
2194 let search_fg = self.theme.search_match_fg;
2195 let ns = self.search_namespace.clone();
2196 let state = self.active_state_mut();
2197
2198 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2200
2201 for &(match_pos, match_len) in &match_ranges {
2203 let search_style = ratatui::style::Style::default().fg(search_fg).bg(search_bg);
2204 let overlay = crate::view::overlay::Overlay::with_namespace(
2205 &mut state.marker_list,
2206 match_pos..(match_pos + match_len),
2207 crate::view::overlay::OverlayFace::Style {
2208 style: search_style,
2209 },
2210 ns.clone(),
2211 )
2212 .with_priority_value(10);
2213 state.overlays.add(overlay);
2214 }
2215 }
2216
2217 let cursor_pos = {
2219 let state = self.active_state();
2220 state.cursors.primary().position
2221 };
2222 let current_match_index = matches
2223 .iter()
2224 .position(|&pos| pos >= cursor_pos)
2225 .unwrap_or(0);
2226
2227 let match_pos = matches[current_match_index];
2229 {
2230 let active_split = self.split_manager.active_split();
2231 let active_buffer = self.active_buffer();
2232 let state = self.active_state_mut();
2233 state.cursors.primary_mut().position = match_pos;
2234 state.cursors.primary_mut().anchor = None;
2235 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2237 let state = self.buffers.get_mut(&active_buffer).unwrap();
2238 view_state
2239 .viewport
2240 .ensure_visible(&mut state.buffer, state.cursors.primary());
2241 }
2242 }
2243
2244 let num_matches = matches.len();
2245
2246 self.search_state = Some(SearchState {
2248 query: query.to_string(),
2249 matches,
2250 current_match_index: Some(current_match_index),
2251 wrap_search: search_range.is_none(), search_range,
2253 });
2254
2255 let msg = if self.search_state.as_ref().unwrap().search_range.is_some() {
2256 format!(
2257 "Found {} match{} for '{}' in selection",
2258 num_matches,
2259 if num_matches == 1 { "" } else { "es" },
2260 query
2261 )
2262 } else {
2263 format!(
2264 "Found {} match{} for '{}'",
2265 num_matches,
2266 if num_matches == 1 { "" } else { "es" },
2267 query
2268 )
2269 };
2270 self.set_status_message(msg);
2271 }
2272
2273 fn get_search_match_positions(&self) -> Vec<usize> {
2276 let ns = &self.search_namespace;
2277 let state = self.active_state();
2278
2279 let mut positions: Vec<usize> = state
2281 .overlays
2282 .all()
2283 .iter()
2284 .filter(|o| o.namespace.as_ref() == Some(ns))
2285 .filter_map(|o| state.marker_list.get_position(o.start_marker))
2286 .collect();
2287
2288 positions.sort_unstable();
2290 positions.dedup(); positions
2293 }
2294
2295 pub(super) fn find_next(&mut self) {
2297 let overlay_positions = self.get_search_match_positions();
2300
2301 if let Some(ref mut search_state) = self.search_state {
2302 let match_positions =
2305 if !overlay_positions.is_empty() && search_state.search_range.is_none() {
2306 overlay_positions
2307 } else {
2308 search_state.matches.clone()
2309 };
2310
2311 if match_positions.is_empty() {
2312 return;
2313 }
2314
2315 let current_index = search_state.current_match_index.unwrap_or(0);
2316 let next_index = if current_index + 1 < match_positions.len() {
2317 current_index + 1
2318 } else if search_state.wrap_search {
2319 0 } else {
2321 self.set_status_message(t!("search.no_matches").to_string());
2322 return;
2323 };
2324
2325 search_state.current_match_index = Some(next_index);
2326 let match_pos = match_positions[next_index];
2327 let matches_len = match_positions.len();
2328
2329 {
2330 let active_split = self.split_manager.active_split();
2331 let active_buffer = self.active_buffer();
2332 let state = self.active_state_mut();
2333 state.cursors.primary_mut().position = match_pos;
2334 state.cursors.primary_mut().anchor = None;
2335 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2337 let state = self.buffers.get_mut(&active_buffer).unwrap();
2338 view_state
2339 .viewport
2340 .ensure_visible(&mut state.buffer, state.cursors.primary());
2341 }
2342 }
2343
2344 self.set_status_message(
2345 t!(
2346 "search.match_of",
2347 current = next_index + 1,
2348 total = matches_len
2349 )
2350 .to_string(),
2351 );
2352 } else {
2353 let find_key = self
2354 .get_keybinding_for_action("find")
2355 .unwrap_or_else(|| "Ctrl+F".to_string());
2356 self.set_status_message(t!("search.no_active", find_key = find_key).to_string());
2357 }
2358 }
2359
2360 pub(super) fn find_previous(&mut self) {
2362 let overlay_positions = self.get_search_match_positions();
2365
2366 if let Some(ref mut search_state) = self.search_state {
2367 let match_positions =
2371 if !overlay_positions.is_empty() && search_state.search_range.is_none() {
2372 overlay_positions
2373 } else {
2374 search_state.matches.clone()
2375 };
2376
2377 if match_positions.is_empty() {
2378 return;
2379 }
2380
2381 let current_index = search_state.current_match_index.unwrap_or(0);
2382 let prev_index = if current_index > 0 {
2383 current_index - 1
2384 } else if search_state.wrap_search {
2385 match_positions.len() - 1 } else {
2387 self.set_status_message(t!("search.no_matches").to_string());
2388 return;
2389 };
2390
2391 search_state.current_match_index = Some(prev_index);
2392 let match_pos = match_positions[prev_index];
2393 let matches_len = match_positions.len();
2394
2395 {
2396 let active_split = self.split_manager.active_split();
2397 let active_buffer = self.active_buffer();
2398 let state = self.active_state_mut();
2399 state.cursors.primary_mut().position = match_pos;
2400 state.cursors.primary_mut().anchor = None;
2401 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2403 let state = self.buffers.get_mut(&active_buffer).unwrap();
2404 view_state
2405 .viewport
2406 .ensure_visible(&mut state.buffer, state.cursors.primary());
2407 }
2408 }
2409
2410 self.set_status_message(
2411 t!(
2412 "search.match_of",
2413 current = prev_index + 1,
2414 total = matches_len
2415 )
2416 .to_string(),
2417 );
2418 } else {
2419 let find_key = self
2420 .get_keybinding_for_action("find")
2421 .unwrap_or_else(|| "Ctrl+F".to_string());
2422 self.set_status_message(t!("search.no_active", find_key = find_key).to_string());
2423 }
2424 }
2425
2426 pub(super) fn find_selection_next(&mut self) {
2433 if let Some(ref search_state) = self.search_state {
2436 let cursor_pos = self.active_state().cursors.primary().position;
2437 if search_state.matches.contains(&cursor_pos) {
2438 self.find_next();
2439 return;
2440 }
2441 }
2443 self.search_state = None;
2444
2445 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
2447
2448 match search_text {
2449 Some(text) if !text.is_empty() => {
2450 let cursor_before = self.active_state().cursors.primary().position;
2452
2453 self.perform_search(&text);
2455
2456 if let Some(ref search_state) = self.search_state {
2458 let cursor_after = self.active_state().cursors.primary().position;
2459
2460 let started_at_match = selection_start
2464 .map(|start| search_state.matches.contains(&start))
2465 .unwrap_or(false);
2466
2467 let landed_at_start = selection_start
2468 .map(|start| cursor_after == start)
2469 .unwrap_or(false);
2470
2471 if ((started_at_match && landed_at_start) || cursor_before == cursor_after)
2475 && search_state.matches.len() > 1
2476 {
2477 self.find_next();
2478 }
2479 }
2480 }
2481 _ => {
2482 self.set_status_message(t!("search.no_text").to_string());
2483 }
2484 }
2485 }
2486
2487 pub(super) fn find_selection_previous(&mut self) {
2493 if let Some(ref search_state) = self.search_state {
2496 let cursor_pos = self.active_state().cursors.primary().position;
2497 if search_state.matches.contains(&cursor_pos) {
2498 self.find_previous();
2499 return;
2500 }
2501 }
2503 self.search_state = None;
2504
2505 let (search_text, selection_start) = self.get_selection_or_word_for_search_with_pos();
2507
2508 match search_text {
2509 Some(text) if !text.is_empty() => {
2510 let cursor_before = self.active_state().cursors.primary().position;
2512
2513 self.perform_search(&text);
2515
2516 if let Some(ref search_state) = self.search_state {
2518 let cursor_after = self.active_state().cursors.primary().position;
2519
2520 let started_at_match = selection_start
2522 .map(|start| search_state.matches.contains(&start))
2523 .unwrap_or(false);
2524
2525 let landed_at_start = selection_start
2526 .map(|start| cursor_after == start)
2527 .unwrap_or(false);
2528
2529 if started_at_match && landed_at_start {
2534 self.find_previous();
2536 } else if cursor_before != cursor_after {
2537 self.find_previous();
2540 } else {
2541 self.find_previous();
2543 }
2544 }
2545 }
2546 _ => {
2547 self.set_status_message(t!("search.no_text").to_string());
2548 }
2549 }
2550 }
2551
2552 fn get_selection_or_word_for_search_with_pos(&mut self) -> (Option<String>, Option<usize>) {
2555 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2556
2557 let (selection_range, cursor_pos) = {
2559 let state = self.active_state();
2560 let primary = state.cursors.primary();
2561 (primary.selection_range(), primary.position)
2562 };
2563
2564 if let Some(range) = selection_range {
2566 let state = self.active_state_mut();
2567 let text = state.get_text_range(range.start, range.end);
2568 if !text.is_empty() {
2569 return (Some(text), Some(range.start));
2570 }
2571 }
2572
2573 let (word_start, word_end) = {
2575 let state = self.active_state();
2576 let word_start = find_word_start(&state.buffer, cursor_pos);
2577 let word_end = find_word_end(&state.buffer, cursor_pos);
2578 (word_start, word_end)
2579 };
2580
2581 if word_start < word_end {
2582 let state = self.active_state_mut();
2583 (
2584 Some(state.get_text_range(word_start, word_end)),
2585 Some(word_start),
2586 )
2587 } else {
2588 (None, None)
2589 }
2590 }
2591
2592 pub(super) fn perform_replace(&mut self, search: &str, replacement: &str) {
2598 if search.is_empty() {
2599 self.set_status_message(t!("replace.empty_query").to_string());
2600 return;
2601 }
2602
2603 let matches = {
2605 let state = self.active_state();
2606 let buffer_len = state.buffer.len();
2607 let mut matches = Vec::new();
2608 let mut current_pos = 0;
2609
2610 while current_pos < buffer_len {
2611 if let Some(offset) = state.buffer.find_next_in_range(
2612 search,
2613 current_pos,
2614 Some(current_pos..buffer_len),
2615 ) {
2616 matches.push(offset);
2617 current_pos = offset + search.len();
2618 } else {
2619 break;
2620 }
2621 }
2622 matches
2623 };
2624
2625 let count = matches.len();
2626
2627 if count == 0 {
2628 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
2629 return;
2630 }
2631
2632 let cursor_id = self.active_state().cursors.primary_id();
2634
2635 let mut events = Vec::with_capacity(count * 2);
2638 for &match_pos in &matches {
2639 events.push(Event::Delete {
2641 range: match_pos..match_pos + search.len(),
2642 deleted_text: search.to_string(), cursor_id,
2644 });
2645 events.push(Event::Insert {
2647 position: match_pos,
2648 text: replacement.to_string(),
2649 cursor_id,
2650 });
2651 }
2652
2653 let description = format!("Replace all '{}' with '{}'", search, replacement);
2655 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
2656 self.active_event_log_mut().append(bulk_edit);
2657 }
2658
2659 self.search_state = None;
2661
2662 let ns = self.search_namespace.clone();
2664 let state = self.active_state_mut();
2665 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2666
2667 self.set_status_message(
2669 t!(
2670 "search.replaced",
2671 count = count,
2672 search = search,
2673 replace = replacement
2674 )
2675 .to_string(),
2676 );
2677 }
2678
2679 pub(super) fn start_interactive_replace(&mut self, search: &str, replacement: &str) {
2681 if search.is_empty() {
2682 self.set_status_message(t!("replace.query_empty").to_string());
2683 return;
2684 }
2685
2686 let state = self.active_state();
2688 let start_pos = state.cursors.primary().position;
2689 let first_match = state.buffer.find_next(search, start_pos);
2690
2691 let Some(first_match_pos) = first_match else {
2692 self.set_status_message(t!("search.no_occurrences", search = search).to_string());
2693 return;
2694 };
2695
2696 self.interactive_replace_state = Some(InteractiveReplaceState {
2698 search: search.to_string(),
2699 replacement: replacement.to_string(),
2700 current_match_pos: first_match_pos,
2701 start_pos: first_match_pos,
2702 has_wrapped: false,
2703 replacements_made: 0,
2704 });
2705
2706 let active_split = self.split_manager.active_split();
2708 let active_buffer = self.active_buffer();
2709 {
2710 let state = self.active_state_mut();
2711 state.cursors.primary_mut().position = first_match_pos;
2712 state.cursors.primary_mut().anchor = None;
2713 }
2714 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2716 let state = self.buffers.get_mut(&active_buffer).unwrap();
2717 view_state
2718 .viewport
2719 .ensure_visible(&mut state.buffer, state.cursors.primary());
2720 }
2721
2722 self.prompt = Some(Prompt::new(
2724 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string(),
2725 PromptType::QueryReplaceConfirm,
2726 ));
2727 }
2728
2729 pub(super) fn handle_interactive_replace_key(&mut self, c: char) -> AnyhowResult<()> {
2731 let state = self.interactive_replace_state.clone();
2732 let Some(mut ir_state) = state else {
2733 return Ok(());
2734 };
2735
2736 match c {
2737 'y' | 'Y' => {
2738 self.replace_current_match(&ir_state)?;
2740 ir_state.replacements_made += 1;
2741
2742 let search_pos = ir_state.current_match_pos + ir_state.replacement.len();
2744 if let Some((next_match, wrapped)) =
2745 self.find_next_match_for_replace(&ir_state, search_pos)
2746 {
2747 ir_state.current_match_pos = next_match;
2748 if wrapped {
2749 ir_state.has_wrapped = true;
2750 }
2751 self.interactive_replace_state = Some(ir_state.clone());
2752 self.move_to_current_match(&ir_state);
2753 } else {
2754 self.finish_interactive_replace(ir_state.replacements_made);
2755 }
2756 }
2757 'n' | 'N' => {
2758 let search_pos = ir_state.current_match_pos + ir_state.search.len();
2760 if let Some((next_match, wrapped)) =
2761 self.find_next_match_for_replace(&ir_state, search_pos)
2762 {
2763 ir_state.current_match_pos = next_match;
2764 if wrapped {
2765 ir_state.has_wrapped = true;
2766 }
2767 self.interactive_replace_state = Some(ir_state.clone());
2768 self.move_to_current_match(&ir_state);
2769 } else {
2770 self.finish_interactive_replace(ir_state.replacements_made);
2771 }
2772 }
2773 'a' | 'A' | '!' => {
2774 let all_matches = {
2783 let mut matches = Vec::new();
2784 let mut temp_state = ir_state.clone();
2785 temp_state.has_wrapped = false; matches.push(ir_state.current_match_pos);
2789 let mut current_pos = ir_state.current_match_pos + ir_state.search.len();
2790
2791 while let Some((next_match, wrapped)) =
2793 self.find_next_match_for_replace(&temp_state, current_pos)
2794 {
2795 matches.push(next_match);
2796 current_pos = next_match + temp_state.search.len();
2797 if wrapped {
2798 temp_state.has_wrapped = true;
2799 }
2800 }
2801 matches
2802 };
2803
2804 let total_count = all_matches.len();
2805
2806 if total_count > 0 {
2807 let cursor_id = self.active_state().cursors.primary_id();
2809
2810 let mut events = Vec::with_capacity(total_count * 2);
2812 for &match_pos in &all_matches {
2813 events.push(Event::Delete {
2814 range: match_pos..match_pos + ir_state.search.len(),
2815 deleted_text: ir_state.search.clone(),
2816 cursor_id,
2817 });
2818 events.push(Event::Insert {
2819 position: match_pos,
2820 text: ir_state.replacement.clone(),
2821 cursor_id,
2822 });
2823 }
2824
2825 let description = format!(
2827 "Replace all {} occurrences of '{}' with '{}'",
2828 total_count, ir_state.search, ir_state.replacement
2829 );
2830 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
2831 self.active_event_log_mut().append(bulk_edit);
2832 }
2833
2834 ir_state.replacements_made += total_count;
2835 }
2836
2837 self.finish_interactive_replace(ir_state.replacements_made);
2838 }
2839 'c' | 'C' | 'q' | 'Q' | '\x1b' => {
2840 self.finish_interactive_replace(ir_state.replacements_made);
2842 }
2843 _ => {
2844 }
2846 }
2847
2848 Ok(())
2849 }
2850
2851 pub(super) fn find_next_match_for_replace(
2853 &self,
2854 ir_state: &InteractiveReplaceState,
2855 start_pos: usize,
2856 ) -> Option<(usize, bool)> {
2857 let state = self.active_state();
2858
2859 if ir_state.has_wrapped {
2860 let search_range = Some(start_pos..ir_state.start_pos);
2863 if let Some(match_pos) =
2864 state
2865 .buffer
2866 .find_next_in_range(&ir_state.search, start_pos, search_range)
2867 {
2868 return Some((match_pos, true));
2869 }
2870 None } else {
2872 let buffer_len = state.buffer.len();
2875 let search_range = Some(start_pos..buffer_len);
2876 if let Some(match_pos) =
2877 state
2878 .buffer
2879 .find_next_in_range(&ir_state.search, start_pos, search_range)
2880 {
2881 return Some((match_pos, false));
2882 }
2883
2884 let wrap_range = Some(0..ir_state.start_pos);
2887 if let Some(match_pos) =
2888 state
2889 .buffer
2890 .find_next_in_range(&ir_state.search, 0, wrap_range)
2891 {
2892 return Some((match_pos, true)); }
2894
2895 None }
2897 }
2898
2899 pub(super) fn replace_current_match(
2901 &mut self,
2902 ir_state: &InteractiveReplaceState,
2903 ) -> AnyhowResult<()> {
2904 let match_pos = ir_state.current_match_pos;
2905 let search_len = ir_state.search.len();
2906 let range = match_pos..(match_pos + search_len);
2907
2908 let deleted_text = self
2910 .active_state_mut()
2911 .get_text_range(range.start, range.end);
2912
2913 let cursor_id = self.active_state().cursors.primary_id();
2915 let cursor = *self.active_state().cursors.get(cursor_id).unwrap();
2916 let old_position = cursor.position;
2917 let old_anchor = cursor.anchor;
2918 let old_sticky_column = cursor.sticky_column;
2919
2920 let events = vec![
2923 Event::MoveCursor {
2924 cursor_id,
2925 old_position,
2926 new_position: match_pos,
2927 old_anchor,
2928 new_anchor: None,
2929 old_sticky_column,
2930 new_sticky_column: 0,
2931 },
2932 Event::Delete {
2933 range: range.clone(),
2934 deleted_text,
2935 cursor_id,
2936 },
2937 Event::Insert {
2938 position: match_pos,
2939 text: ir_state.replacement.clone(),
2940 cursor_id,
2941 },
2942 ];
2943
2944 let batch = Event::Batch {
2946 events,
2947 description: format!(
2948 "Query replace '{}' with '{}'",
2949 ir_state.search, ir_state.replacement
2950 ),
2951 };
2952
2953 self.active_event_log_mut().append(batch.clone());
2955 self.apply_event_to_active_buffer(&batch);
2956
2957 Ok(())
2958 }
2959
2960 pub(super) fn move_to_current_match(&mut self, ir_state: &InteractiveReplaceState) {
2962 let match_pos = ir_state.current_match_pos;
2963 let active_split = self.split_manager.active_split();
2964 let active_buffer = self.active_buffer();
2965 {
2966 let state = self.active_state_mut();
2967 state.cursors.primary_mut().position = match_pos;
2968 state.cursors.primary_mut().anchor = None;
2969 }
2970 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2972 let state = self.buffers.get_mut(&active_buffer).unwrap();
2973 view_state
2974 .viewport
2975 .ensure_visible(&mut state.buffer, state.cursors.primary());
2976 }
2977
2978 let msg = if ir_state.has_wrapped {
2980 "[Wrapped] Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
2981 } else {
2982 "Replace? (y)es (n)o (a)ll (c)ancel: ".to_string()
2983 };
2984 if let Some(ref mut prompt) = self.prompt {
2985 if prompt.prompt_type == PromptType::QueryReplaceConfirm {
2986 prompt.message = msg;
2987 prompt.input.clear();
2988 prompt.cursor_pos = 0;
2989 }
2990 }
2991 }
2992
2993 pub(super) fn finish_interactive_replace(&mut self, replacements_made: usize) {
2995 self.interactive_replace_state = None;
2996 self.prompt = None; let ns = self.search_namespace.clone();
3000 let state = self.active_state_mut();
3001 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3002
3003 self.set_status_message(t!("search.replaced_count", count = replacements_made).to_string());
3004 }
3005
3006 pub(super) fn smart_home(&mut self) {
3008 let estimated_line_length = self.config.editor.estimated_line_length;
3009 let state = self.active_state_mut();
3010 let cursor = *state.cursors.primary();
3011 let cursor_id = state.cursors.primary_id();
3012
3013 let mut iter = state
3015 .buffer
3016 .line_iterator(cursor.position, estimated_line_length);
3017 if let Some((line_start, line_content)) = iter.next_line() {
3018 let first_non_ws = line_content
3020 .chars()
3021 .take_while(|c| *c != '\n')
3022 .position(|c| !c.is_whitespace())
3023 .map(|offset| line_start + offset)
3024 .unwrap_or(line_start);
3025
3026 let new_pos = if cursor.position == first_non_ws {
3028 line_start
3029 } else {
3030 first_non_ws
3031 };
3032
3033 let event = Event::MoveCursor {
3034 cursor_id,
3035 old_position: cursor.position,
3036 new_position: new_pos,
3037 old_anchor: cursor.anchor,
3038 new_anchor: None,
3039 old_sticky_column: cursor.sticky_column,
3040 new_sticky_column: 0,
3041 };
3042
3043 self.active_event_log_mut().append(event.clone());
3044 self.apply_event_to_active_buffer(&event);
3045 }
3046 }
3047
3048 pub(super) fn toggle_comment(&mut self) {
3050 let language = &self.active_state().language;
3053 let comment_prefix = self
3054 .config
3055 .languages
3056 .get(language)
3057 .and_then(|lang_config| lang_config.comment_prefix.clone());
3058
3059 let comment_prefix: String = match comment_prefix {
3060 Some(prefix) => {
3061 if prefix.ends_with(' ') {
3063 prefix
3064 } else {
3065 format!("{} ", prefix)
3066 }
3067 }
3068 None => return, };
3070
3071 let estimated_line_length = self.config.editor.estimated_line_length;
3072
3073 let state = self.active_state_mut();
3074 let cursor = *state.cursors.primary();
3075 let cursor_id = state.cursors.primary_id();
3076
3077 let original_anchor = cursor.anchor;
3079 let original_position = cursor.position;
3080 let had_selection = original_anchor.is_some();
3081
3082 let (start_pos, end_pos) = if let Some(range) = cursor.selection_range() {
3083 (range.start, range.end)
3084 } else {
3085 let iter = state
3086 .buffer
3087 .line_iterator(cursor.position, estimated_line_length);
3088 let line_start = iter.current_position();
3089 (line_start, cursor.position)
3090 };
3091
3092 let buffer_len = state.buffer.len();
3094 let mut line_starts = Vec::new();
3095 let mut iter = state.buffer.line_iterator(start_pos, estimated_line_length);
3096 let mut current_pos = iter.current_position();
3097 line_starts.push(current_pos);
3098
3099 while let Some((_, content)) = iter.next_line() {
3100 current_pos += content.len();
3101 if current_pos >= end_pos || current_pos >= buffer_len {
3102 break;
3103 }
3104 let next_iter = state
3105 .buffer
3106 .line_iterator(current_pos, estimated_line_length);
3107 let next_start = next_iter.current_position();
3108 if next_start != *line_starts.last().unwrap() {
3109 line_starts.push(next_start);
3110 }
3111 iter = state
3112 .buffer
3113 .line_iterator(current_pos, estimated_line_length);
3114 }
3115
3116 let all_commented = line_starts.iter().all(|&line_start| {
3119 let line_bytes = state
3120 .buffer
3121 .slice_bytes(line_start..buffer_len.min(line_start + comment_prefix.len() + 10));
3122 let line_str = String::from_utf8_lossy(&line_bytes);
3123 let trimmed = line_str.trim_start();
3124 trimmed.starts_with(comment_prefix.trim())
3125 });
3126
3127 let mut events = Vec::new();
3128 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
3131
3132 if all_commented {
3133 for &line_start in line_starts.iter().rev() {
3135 let line_bytes = state
3136 .buffer
3137 .slice_bytes(line_start..buffer_len.min(line_start + 100));
3138 let line_str = String::from_utf8_lossy(&line_bytes);
3139
3140 let leading_ws: usize = line_str
3142 .chars()
3143 .take_while(|c| c.is_whitespace() && *c != '\n')
3144 .map(|c| c.len_utf8())
3145 .sum();
3146 let rest = &line_str[leading_ws..];
3147
3148 if rest.starts_with(comment_prefix.trim()) {
3149 let remove_len = if rest.starts_with(&comment_prefix) {
3150 comment_prefix.len()
3151 } else {
3152 comment_prefix.trim().len()
3153 };
3154 let deleted_text = String::from_utf8_lossy(&state.buffer.slice_bytes(
3155 line_start + leading_ws..line_start + leading_ws + remove_len,
3156 ))
3157 .to_string();
3158 events.push(Event::Delete {
3159 range: (line_start + leading_ws)..(line_start + leading_ws + remove_len),
3160 deleted_text,
3161 cursor_id,
3162 });
3163 position_deltas.push((line_start, -(remove_len as isize)));
3164 }
3165 }
3166 } else {
3167 let prefix_len = comment_prefix.len();
3169 for &line_start in line_starts.iter().rev() {
3170 events.push(Event::Insert {
3171 position: line_start,
3172 text: comment_prefix.to_string(),
3173 cursor_id,
3174 });
3175 position_deltas.push((line_start, prefix_len as isize));
3176 }
3177 }
3178
3179 if events.is_empty() {
3180 return;
3181 }
3182
3183 let action_desc = if all_commented {
3184 "Uncomment"
3185 } else {
3186 "Comment"
3187 };
3188
3189 if had_selection {
3191 position_deltas.sort_by_key(|(pos, _)| *pos);
3193
3194 let calc_shift = |original_pos: usize| -> isize {
3196 let mut shift: isize = 0;
3197 for (edit_pos, delta) in &position_deltas {
3198 if *edit_pos < original_pos {
3199 shift += delta;
3200 }
3201 }
3202 shift
3203 };
3204
3205 let anchor_shift = calc_shift(original_anchor.unwrap_or(0));
3206 let position_shift = calc_shift(original_position);
3207
3208 let new_anchor = (original_anchor.unwrap_or(0) as isize + anchor_shift).max(0) as usize;
3209 let new_position = (original_position as isize + position_shift).max(0) as usize;
3210
3211 events.push(Event::MoveCursor {
3212 cursor_id,
3213 old_position: original_position,
3214 new_position,
3215 old_anchor: original_anchor,
3216 new_anchor: Some(new_anchor),
3217 old_sticky_column: 0,
3218 new_sticky_column: 0,
3219 });
3220 }
3221
3222 let description = format!("{} lines", action_desc);
3224 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
3225 self.active_event_log_mut().append(bulk_edit);
3226 }
3227
3228 self.set_status_message(
3229 t!(
3230 "lines.action",
3231 action = action_desc,
3232 count = line_starts.len()
3233 )
3234 .to_string(),
3235 );
3236 }
3237
3238 pub(super) fn goto_matching_bracket(&mut self) {
3240 let state = self.active_state_mut();
3241 let cursor = *state.cursors.primary();
3242 let cursor_id = state.cursors.primary_id();
3243
3244 let pos = cursor.position;
3245 if pos >= state.buffer.len() {
3246 self.set_status_message(t!("diagnostics.bracket_none").to_string());
3247 return;
3248 }
3249
3250 let bytes = state.buffer.slice_bytes(pos..pos + 1);
3251 if bytes.is_empty() {
3252 self.set_status_message(t!("diagnostics.bracket_none").to_string());
3253 return;
3254 }
3255
3256 let ch = bytes[0] as char;
3257 let (opening, closing, forward) = match ch {
3258 '(' => ('(', ')', true),
3259 ')' => ('(', ')', false),
3260 '[' => ('[', ']', true),
3261 ']' => ('[', ']', false),
3262 '{' => ('{', '}', true),
3263 '}' => ('{', '}', false),
3264 '<' => ('<', '>', true),
3265 '>' => ('<', '>', false),
3266 _ => {
3267 self.set_status_message(t!("diagnostics.bracket_none").to_string());
3268 return;
3269 }
3270 };
3271
3272 let buffer_len = state.buffer.len();
3274 let mut depth = 1;
3275 let matching_pos = if forward {
3276 let mut search_pos = pos + 1;
3277 let mut found = None;
3278 while search_pos < buffer_len && depth > 0 {
3279 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
3280 if !b.is_empty() {
3281 let c = b[0] as char;
3282 if c == opening {
3283 depth += 1;
3284 } else if c == closing {
3285 depth -= 1;
3286 if depth == 0 {
3287 found = Some(search_pos);
3288 }
3289 }
3290 }
3291 search_pos += 1;
3292 }
3293 found
3294 } else {
3295 let mut search_pos = pos.saturating_sub(1);
3296 let mut found = None;
3297 loop {
3298 let b = state.buffer.slice_bytes(search_pos..search_pos + 1);
3299 if !b.is_empty() {
3300 let c = b[0] as char;
3301 if c == closing {
3302 depth += 1;
3303 } else if c == opening {
3304 depth -= 1;
3305 if depth == 0 {
3306 found = Some(search_pos);
3307 break;
3308 }
3309 }
3310 }
3311 if search_pos == 0 {
3312 break;
3313 }
3314 search_pos -= 1;
3315 }
3316 found
3317 };
3318
3319 if let Some(new_pos) = matching_pos {
3320 let event = Event::MoveCursor {
3321 cursor_id,
3322 old_position: cursor.position,
3323 new_position: new_pos,
3324 old_anchor: cursor.anchor,
3325 new_anchor: None,
3326 old_sticky_column: cursor.sticky_column,
3327 new_sticky_column: 0,
3328 };
3329 self.active_event_log_mut().append(event.clone());
3330 self.apply_event_to_active_buffer(&event);
3331 } else {
3332 self.set_status_message(t!("diagnostics.bracket_no_match").to_string());
3333 }
3334 }
3335
3336 pub(super) fn jump_to_next_error(&mut self) {
3338 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
3339 let state = self.active_state_mut();
3340 let cursor_pos = state.cursors.primary().position;
3341 let cursor_id = state.cursors.primary_id();
3342 let cursor = *state.cursors.primary();
3343
3344 let mut diagnostic_positions: Vec<usize> = state
3346 .overlays
3347 .all()
3348 .iter()
3349 .filter_map(|overlay| {
3350 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
3352 Some(overlay.range(&state.marker_list).start)
3353 } else {
3354 None
3355 }
3356 })
3357 .collect();
3358
3359 if diagnostic_positions.is_empty() {
3360 self.set_status_message(t!("diagnostics.none").to_string());
3361 return;
3362 }
3363
3364 diagnostic_positions.sort_unstable();
3366 diagnostic_positions.dedup();
3367
3368 let next_pos = diagnostic_positions
3370 .iter()
3371 .find(|&&pos| pos > cursor_pos)
3372 .or_else(|| diagnostic_positions.first()) .copied();
3374
3375 if let Some(new_pos) = next_pos {
3376 let event = Event::MoveCursor {
3377 cursor_id,
3378 old_position: cursor.position,
3379 new_position: new_pos,
3380 old_anchor: cursor.anchor,
3381 new_anchor: None,
3382 old_sticky_column: cursor.sticky_column,
3383 new_sticky_column: 0,
3384 };
3385 self.active_event_log_mut().append(event.clone());
3386 self.apply_event_to_active_buffer(&event);
3387
3388 let state = self.active_state();
3390 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
3391 let range = overlay.range(&state.marker_list);
3392 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
3393 overlay.message.clone()
3394 } else {
3395 None
3396 }
3397 }) {
3398 self.set_status_message(msg);
3399 }
3400 }
3401 }
3402
3403 pub(super) fn jump_to_previous_error(&mut self) {
3405 let diagnostic_ns = self.lsp_diagnostic_namespace.clone();
3406 let state = self.active_state_mut();
3407 let cursor_pos = state.cursors.primary().position;
3408 let cursor_id = state.cursors.primary_id();
3409 let cursor = *state.cursors.primary();
3410
3411 let mut diagnostic_positions: Vec<usize> = state
3413 .overlays
3414 .all()
3415 .iter()
3416 .filter_map(|overlay| {
3417 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
3419 Some(overlay.range(&state.marker_list).start)
3420 } else {
3421 None
3422 }
3423 })
3424 .collect();
3425
3426 if diagnostic_positions.is_empty() {
3427 self.set_status_message(t!("diagnostics.none").to_string());
3428 return;
3429 }
3430
3431 diagnostic_positions.sort_unstable();
3433 diagnostic_positions.dedup();
3434
3435 let prev_pos = diagnostic_positions
3437 .iter()
3438 .rev()
3439 .find(|&&pos| pos < cursor_pos)
3440 .or_else(|| diagnostic_positions.last()) .copied();
3442
3443 if let Some(new_pos) = prev_pos {
3444 let event = Event::MoveCursor {
3445 cursor_id,
3446 old_position: cursor.position,
3447 new_position: new_pos,
3448 old_anchor: cursor.anchor,
3449 new_anchor: None,
3450 old_sticky_column: cursor.sticky_column,
3451 new_sticky_column: 0,
3452 };
3453 self.active_event_log_mut().append(event.clone());
3454 self.apply_event_to_active_buffer(&event);
3455
3456 let state = self.active_state();
3458 if let Some(msg) = state.overlays.all().iter().find_map(|overlay| {
3459 let range = overlay.range(&state.marker_list);
3460 if range.start == new_pos && overlay.namespace.as_ref() == Some(&diagnostic_ns) {
3461 overlay.message.clone()
3462 } else {
3463 None
3464 }
3465 }) {
3466 self.set_status_message(msg);
3467 }
3468 }
3469 }
3470
3471 pub(super) fn toggle_macro_recording(&mut self, key: char) {
3473 if let Some(state) = &self.macro_recording {
3474 if state.key == key {
3475 self.stop_macro_recording();
3477 } else {
3478 self.stop_macro_recording();
3480 self.start_macro_recording(key);
3481 }
3482 } else {
3483 self.start_macro_recording(key);
3485 }
3486 }
3487
3488 pub(super) fn start_macro_recording(&mut self, key: char) {
3490 self.macro_recording = Some(MacroRecordingState {
3491 key,
3492 actions: Vec::new(),
3493 });
3494
3495 let stop_hint = self.build_macro_stop_hint(key);
3497 self.set_status_message(
3498 t!(
3499 "macro.recording_with_hint",
3500 key = key,
3501 stop_hint = stop_hint
3502 )
3503 .to_string(),
3504 );
3505 }
3506
3507 fn build_macro_stop_hint(&self, _key: char) -> String {
3509 let mut hints = Vec::new();
3510
3511 if let Some(stop_key) = self.get_keybinding_for_action("stop_macro_recording") {
3513 hints.push(stop_key);
3514 }
3515
3516 let palette_key = self
3518 .get_keybinding_for_action("command_palette")
3519 .unwrap_or_else(|| "Ctrl+P".to_string());
3520
3521 if hints.is_empty() {
3522 format!("{} → Stop Recording Macro", palette_key)
3524 } else {
3525 format!("{} or {} → Stop Recording", hints.join("/"), palette_key)
3527 }
3528 }
3529
3530 pub(super) fn stop_macro_recording(&mut self) {
3532 if let Some(state) = self.macro_recording.take() {
3533 let action_count = state.actions.len();
3534 let key = state.key;
3535 self.macros.insert(key, state.actions);
3536 self.last_macro_register = Some(key);
3537
3538 let play_hint = self.build_macro_play_hint();
3540 self.set_status_message(
3541 t!(
3542 "macro.saved",
3543 key = key,
3544 count = action_count,
3545 play_hint = play_hint
3546 )
3547 .to_string(),
3548 );
3549 } else {
3550 self.set_status_message(t!("macro.not_recording").to_string());
3551 }
3552 }
3553
3554 fn build_macro_play_hint(&self) -> String {
3556 let palette_key = self
3558 .get_keybinding_for_action("command_palette")
3559 .unwrap_or_else(|| "Ctrl+P".to_string());
3560
3561 format!("{} → Play Macro", palette_key)
3562 }
3563
3564 pub(super) fn play_macro(&mut self, key: char) {
3566 if self.macro_playing {
3568 return;
3569 }
3570
3571 if let Some(actions) = self.macros.get(&key).cloned() {
3572 if actions.is_empty() {
3573 self.set_status_message(t!("macro.empty", key = key).to_string());
3574 return;
3575 }
3576
3577 let was_recording = self.macro_recording.take();
3579 self.macro_playing = true;
3580
3581 let action_count = actions.len();
3582 for action in actions {
3583 let _ = self.handle_action(action);
3584 }
3585
3586 self.macro_recording = was_recording;
3588 self.macro_playing = false;
3589
3590 self.set_status_message(
3591 t!("macro.played", key = key, count = action_count).to_string(),
3592 );
3593 } else {
3594 self.set_status_message(t!("macro.not_found", key = key).to_string());
3595 }
3596 }
3597
3598 pub(super) fn record_macro_action(&mut self, action: &Action) {
3600 if let Some(state) = &mut self.macro_recording {
3601 match action {
3603 Action::StartMacroRecording
3604 | Action::StopMacroRecording
3605 | Action::PlayMacro(_)
3606 | Action::ToggleMacroRecording(_)
3607 | Action::ShowMacro(_)
3608 | Action::ListMacros
3609 | Action::PromptRecordMacro
3610 | Action::PromptPlayMacro
3611 | Action::PlayLastMacro => {}
3612 Action::PromptConfirm => {
3615 if let Some(prompt) = &self.prompt {
3616 let text = prompt.get_text().to_string();
3617 state.actions.push(Action::PromptConfirmWithText(text));
3618 } else {
3619 state.actions.push(action.clone());
3620 }
3621 }
3622 _ => {
3623 state.actions.push(action.clone());
3624 }
3625 }
3626 }
3627 }
3628
3629 pub(super) fn show_macro_in_buffer(&mut self, key: char) {
3631 let (json, actions_len) = match self.macros.get(&key) {
3633 Some(actions) => {
3634 let json = match serde_json::to_string_pretty(actions) {
3635 Ok(json) => json,
3636 Err(e) => {
3637 self.set_status_message(
3638 t!("macro.serialize_failed", error = e.to_string()).to_string(),
3639 );
3640 return;
3641 }
3642 };
3643 (json, actions.len())
3644 }
3645 None => {
3646 self.set_status_message(t!("macro.not_found", key = key).to_string());
3647 return;
3648 }
3649 };
3650
3651 let content = format!(
3653 "// Macro '{}' ({} actions)\n// This buffer can be saved as a .json file for persistence\n\n{}",
3654 key,
3655 actions_len,
3656 json
3657 );
3658
3659 let buffer_id = BufferId(self.next_buffer_id);
3661 self.next_buffer_id += 1;
3662
3663 let mut state = EditorState::new(
3664 self.terminal_width,
3665 self.terminal_height,
3666 self.config.editor.large_file_threshold_bytes as usize,
3667 std::sync::Arc::clone(&self.filesystem),
3668 );
3669 state
3670 .margins
3671 .set_line_numbers(self.config.editor.line_numbers);
3672
3673 self.buffers.insert(buffer_id, state);
3674 self.event_logs.insert(buffer_id, EventLog::new());
3675
3676 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3678 state.buffer = crate::model::buffer::Buffer::from_str(
3679 &content,
3680 self.config.editor.large_file_threshold_bytes as usize,
3681 std::sync::Arc::clone(&self.filesystem),
3682 );
3683 }
3684
3685 let metadata = BufferMetadata {
3687 kind: BufferKind::Virtual {
3688 mode: "macro-view".to_string(),
3689 },
3690 display_name: format!("*Macro {}*", key),
3691 lsp_enabled: false,
3692 lsp_disabled_reason: Some("Virtual macro buffer".to_string()),
3693 read_only: false, binary: false,
3695 lsp_opened_with: std::collections::HashSet::new(),
3696 hidden_from_tabs: false,
3697 recovery_id: None,
3698 };
3699 self.buffer_metadata.insert(buffer_id, metadata);
3700
3701 self.set_active_buffer(buffer_id);
3703 self.set_status_message(
3704 t!("macro.shown_buffer", key = key, count = actions_len).to_string(),
3705 );
3706 }
3707
3708 pub(super) fn list_macros_in_buffer(&mut self) {
3710 if self.macros.is_empty() {
3711 self.set_status_message(t!("macro.none_recorded").to_string());
3712 return;
3713 }
3714
3715 let mut content =
3717 String::from("// Recorded Macros\n// Use ShowMacro(key) to see details\n\n");
3718
3719 let mut keys: Vec<char> = self.macros.keys().copied().collect();
3720 keys.sort();
3721
3722 for key in keys {
3723 if let Some(actions) = self.macros.get(&key) {
3724 content.push_str(&format!("Macro '{}': {} actions\n", key, actions.len()));
3725
3726 for (i, action) in actions.iter().enumerate() {
3728 content.push_str(&format!(" {}. {:?}\n", i + 1, action));
3729 }
3730 content.push('\n');
3731 }
3732 }
3733
3734 let buffer_id = BufferId(self.next_buffer_id);
3736 self.next_buffer_id += 1;
3737
3738 let mut state = EditorState::new(
3739 self.terminal_width,
3740 self.terminal_height,
3741 self.config.editor.large_file_threshold_bytes as usize,
3742 std::sync::Arc::clone(&self.filesystem),
3743 );
3744 state
3745 .margins
3746 .set_line_numbers(self.config.editor.line_numbers);
3747
3748 self.buffers.insert(buffer_id, state);
3749 self.event_logs.insert(buffer_id, EventLog::new());
3750
3751 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3753 state.buffer = crate::model::buffer::Buffer::from_str(
3754 &content,
3755 self.config.editor.large_file_threshold_bytes as usize,
3756 std::sync::Arc::clone(&self.filesystem),
3757 );
3758 }
3759
3760 let metadata = BufferMetadata {
3762 kind: BufferKind::Virtual {
3763 mode: "macro-list".to_string(),
3764 },
3765 display_name: "*Macros*".to_string(),
3766 lsp_enabled: false,
3767 lsp_disabled_reason: Some("Virtual macro list buffer".to_string()),
3768 read_only: true,
3769 binary: false,
3770 lsp_opened_with: std::collections::HashSet::new(),
3771 hidden_from_tabs: false,
3772 recovery_id: None,
3773 };
3774 self.buffer_metadata.insert(buffer_id, metadata);
3775
3776 self.set_active_buffer(buffer_id);
3778 self.set_status_message(t!("macro.showing", count = self.macros.len()).to_string());
3779 }
3780
3781 pub(super) fn set_bookmark(&mut self, key: char) {
3783 let buffer_id = self.active_buffer();
3784 let position = self.active_state().cursors.primary().position;
3785 self.bookmarks.insert(
3786 key,
3787 Bookmark {
3788 buffer_id,
3789 position,
3790 },
3791 );
3792 self.set_status_message(t!("bookmark.set", key = key).to_string());
3793 }
3794
3795 pub(super) fn jump_to_bookmark(&mut self, key: char) {
3797 if let Some(bookmark) = self.bookmarks.get(&key).cloned() {
3798 if bookmark.buffer_id != self.active_buffer() {
3800 if self.buffers.contains_key(&bookmark.buffer_id) {
3801 self.set_active_buffer(bookmark.buffer_id);
3802 } else {
3803 self.set_status_message(t!("bookmark.buffer_gone", key = key).to_string());
3804 self.bookmarks.remove(&key);
3805 return;
3806 }
3807 }
3808
3809 let state = self.active_state_mut();
3811 let cursor_id = state.cursors.primary_id();
3812 let old_pos = state.cursors.primary().position;
3813 let new_pos = bookmark.position.min(state.buffer.len());
3814
3815 let event = Event::MoveCursor {
3816 cursor_id,
3817 old_position: old_pos,
3818 new_position: new_pos,
3819 old_anchor: state.cursors.primary().anchor,
3820 new_anchor: None,
3821 old_sticky_column: state.cursors.primary().sticky_column,
3822 new_sticky_column: 0,
3823 };
3824
3825 self.active_event_log_mut().append(event.clone());
3826 self.apply_event_to_active_buffer(&event);
3827 self.set_status_message(t!("bookmark.jumped", key = key).to_string());
3828 } else {
3829 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
3830 }
3831 }
3832
3833 pub(super) fn clear_bookmark(&mut self, key: char) {
3835 if self.bookmarks.remove(&key).is_some() {
3836 self.set_status_message(t!("bookmark.cleared", key = key).to_string());
3837 } else {
3838 self.set_status_message(t!("bookmark.not_set", key = key).to_string());
3839 }
3840 }
3841
3842 pub(super) fn list_bookmarks(&mut self) {
3844 if self.bookmarks.is_empty() {
3845 self.set_status_message(t!("bookmark.none_set").to_string());
3846 return;
3847 }
3848
3849 let mut bookmark_list: Vec<_> = self.bookmarks.iter().collect();
3850 bookmark_list.sort_by_key(|(k, _)| *k);
3851
3852 let list_str: String = bookmark_list
3853 .iter()
3854 .map(|(k, bm)| {
3855 let buffer_name = self
3856 .buffer_metadata
3857 .get(&bm.buffer_id)
3858 .map(|m| m.display_name.as_str())
3859 .unwrap_or("unknown");
3860 format!("'{}': {} @ {}", k, buffer_name, bm.position)
3861 })
3862 .collect::<Vec<_>>()
3863 .join(", ");
3864
3865 self.set_status_message(t!("bookmark.list", list = list_str).to_string());
3866 }
3867
3868 pub fn clear_search_history(&mut self) {
3871 if let Some(history) = self.prompt_histories.get_mut("search") {
3872 history.clear();
3873 }
3874 }
3875
3876 pub fn save_histories(&self) {
3879 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.data_dir) {
3881 tracing::warn!("Failed to create data directory: {}", e);
3882 return;
3883 }
3884
3885 for (key, history) in &self.prompt_histories {
3887 let path = self.dir_context.prompt_history_path(key);
3888 if let Err(e) = history.save_to_file(&path) {
3889 tracing::warn!("Failed to save {} history: {}", key, e);
3890 } else {
3891 tracing::debug!("Saved {} history to {:?}", key, path);
3892 }
3893 }
3894 }
3895
3896 pub(super) fn ensure_active_tab_visible(
3900 &mut self,
3901 split_id: SplitId,
3902 active_buffer: BufferId,
3903 available_width: u16,
3904 ) {
3905 tracing::debug!(
3906 "ensure_active_tab_visible called: split={:?}, buffer={:?}, width={}",
3907 split_id,
3908 active_buffer,
3909 available_width
3910 );
3911 let Some(view_state) = self.split_view_states.get_mut(&split_id) else {
3912 tracing::debug!(" -> no view_state for split");
3913 return;
3914 };
3915
3916 let split_buffers = view_state.open_buffers.clone();
3917
3918 let (tab_widths, rendered_buffer_ids) = crate::view::ui::tabs::calculate_tab_widths(
3920 &split_buffers,
3921 &self.buffers,
3922 &self.buffer_metadata,
3923 &self.composite_buffers,
3924 );
3925
3926 let total_tabs_width: usize = tab_widths.iter().sum();
3927 let max_visible_width = available_width as usize;
3928
3929 let active_tab_index = rendered_buffer_ids
3932 .iter()
3933 .position(|id| *id == active_buffer);
3934
3935 let active_width_index = active_tab_index.map(|buf_idx| {
3939 if buf_idx == 0 {
3940 0
3941 } else {
3942 buf_idx * 2
3947 }
3948 });
3949
3950 let old_offset = view_state.tab_scroll_offset;
3952 let new_scroll_offset = if let Some(idx) = active_width_index {
3953 crate::view::ui::tabs::scroll_to_show_tab(
3954 &tab_widths,
3955 idx,
3956 view_state.tab_scroll_offset,
3957 max_visible_width,
3958 )
3959 } else {
3960 view_state
3961 .tab_scroll_offset
3962 .min(total_tabs_width.saturating_sub(max_visible_width))
3963 };
3964
3965 tracing::debug!(
3966 " -> offset: {} -> {} (idx={:?}, max_width={}, total={})",
3967 old_offset,
3968 new_scroll_offset,
3969 active_width_index,
3970 max_visible_width,
3971 total_tabs_width
3972 );
3973 view_state.tab_scroll_offset = new_scroll_offset;
3974 }
3975
3976 fn sync_scroll_groups(&mut self) {
3982 let active_split = self.split_manager.active_split();
3983 let group_count = self.scroll_sync_manager.groups().len();
3984
3985 if group_count > 0 {
3986 tracing::debug!(
3987 "sync_scroll_groups: active_split={:?}, {} groups",
3988 active_split,
3989 group_count
3990 );
3991 }
3992
3993 let sync_info: Vec<_> = self
3996 .scroll_sync_manager
3997 .groups()
3998 .iter()
3999 .filter_map(|group| {
4000 tracing::debug!(
4001 "sync_scroll_groups: checking group {}, left={:?}, right={:?}",
4002 group.id,
4003 group.left_split,
4004 group.right_split
4005 );
4006
4007 if !group.contains_split(active_split) {
4008 tracing::debug!(
4009 "sync_scroll_groups: active split {:?} not in group",
4010 active_split
4011 );
4012 return None;
4013 }
4014
4015 let active_top_byte = self
4017 .split_view_states
4018 .get(&active_split)?
4019 .viewport
4020 .top_byte;
4021
4022 let active_buffer_id = self.split_manager.buffer_for_split(active_split)?;
4024 let buffer_state = self.buffers.get(&active_buffer_id)?;
4025 let buffer_len = buffer_state.buffer.len();
4026 let active_line = buffer_state.buffer.get_line_number(active_top_byte);
4027
4028 tracing::debug!(
4029 "sync_scroll_groups: active_split={:?}, buffer_id={:?}, top_byte={}, buffer_len={}, active_line={}",
4030 active_split,
4031 active_buffer_id,
4032 active_top_byte,
4033 buffer_len,
4034 active_line
4035 );
4036
4037 let (other_split, other_line) = if group.is_left_split(active_split) {
4039 (group.right_split, group.left_to_right_line(active_line))
4041 } else {
4042 (group.left_split, group.right_to_left_line(active_line))
4044 };
4045
4046 tracing::debug!(
4047 "sync_scroll_groups: syncing other_split={:?} to line {}",
4048 other_split,
4049 other_line
4050 );
4051
4052 Some((other_split, other_line))
4053 })
4054 .collect();
4055
4056 for (other_split, target_line) in sync_info {
4058 if let Some(buffer_id) = self.split_manager.buffer_for_split(other_split) {
4059 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4060 let buffer = &mut state.buffer;
4061 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
4062 view_state.viewport.scroll_to(buffer, target_line);
4063 }
4064 }
4065 }
4066 }
4067 }
4068
4069 fn pre_sync_ensure_visible(&mut self, active_split: SplitId) {
4078 let group_info = self
4080 .scroll_sync_manager
4081 .find_group_for_split(active_split)
4082 .map(|g| (g.left_split, g.right_split));
4083
4084 let Some((left_split, right_split)) = group_info else {
4085 return;
4086 };
4087
4088 if let Some(buffer_id) = self.split_manager.buffer_for_split(active_split) {
4090 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4091 let buffer = &mut state.buffer;
4092 let cursor = *state.cursors.primary();
4093
4094 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
4095 view_state.viewport.ensure_visible(buffer, &cursor);
4097
4098 tracing::debug!(
4099 "pre_sync_ensure_visible: updated active split {:?} viewport, top_byte={}",
4100 active_split,
4101 view_state.viewport.top_byte
4102 );
4103 }
4104 }
4105 }
4106
4107 let other_split = if active_split == left_split {
4109 right_split
4110 } else {
4111 left_split
4112 };
4113
4114 if let Some(view_state) = self.split_view_states.get_mut(&other_split) {
4115 view_state.viewport.set_skip_ensure_visible();
4116 tracing::debug!(
4117 "pre_sync_ensure_visible: marked other split {:?} to skip ensure_visible",
4118 other_split
4119 );
4120 }
4121 }
4122}