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