1use super::lsp_status::compose_lsp_status;
2use super::*;
3use crate::config::FileExplorerSide;
4
5impl Editor {
6 pub fn render(&mut self, frame: &mut Frame) {
8 let _span = tracing::info_span!("render").entered();
9 let size = frame.area();
10
11 self.animations.capture_before_all();
16
17 self.cached_layout.last_frame_width = size.width;
19 self.cached_layout.last_frame_height = size.height;
20
21 self.cached_layout.reset_cell_theme_map();
23
24 let active_split = self.split_manager.active_split();
29 {
30 let _span = tracing::info_span!("pre_sync_ensure_visible").entered();
31 self.pre_sync_ensure_visible(active_split);
32 }
33
34 {
37 let _span = tracing::info_span!("sync_scroll_groups").entered();
38 self.sync_scroll_groups();
39 }
40
41 let mut semantic_ranges: std::collections::HashMap<BufferId, (usize, usize)> =
47 std::collections::HashMap::new();
48 {
49 let _span = tracing::info_span!("compute_semantic_ranges").entered();
50 for (split_id, view_state) in &self.split_view_states {
51 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
52 if let Some(state) = self.buffers.get(&buffer_id) {
53 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
54 let visible_lines =
55 view_state.viewport.visible_line_count().saturating_sub(1);
56 let end_line = start_line.saturating_add(visible_lines);
57 semantic_ranges
58 .entry(buffer_id)
59 .and_modify(|(min_start, max_end)| {
60 *min_start = (*min_start).min(start_line);
61 *max_end = (*max_end).max(end_line);
62 })
63 .or_insert((start_line, end_line));
64 }
65 }
66 }
67 }
68 for (buffer_id, (start_line, end_line)) in semantic_ranges {
69 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
70 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
71 self.maybe_request_folding_ranges_debounced(buffer_id);
72 }
73
74 {
75 let _span = tracing::info_span!("prepare_for_render").entered();
76 for (split_id, view_state) in &self.split_view_states {
77 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
78 if let Some(state) = self.buffers.get_mut(&buffer_id) {
79 let top_byte = view_state.viewport.top_byte;
80 let height = view_state.viewport.height;
81 if let Err(e) = state.prepare_for_render(top_byte, height) {
82 tracing::error!("Failed to prepare buffer for render: {}", e);
83 }
85 }
86 }
87 }
88 }
89
90 let is_search_prompt_active = self.prompt.as_ref().is_some_and(|p| {
93 matches!(
94 p.prompt_type,
95 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
96 )
97 });
98 if is_search_prompt_active {
99 if let Some(ref search_state) = self.search_state {
100 let query = search_state.query.clone();
101 self.update_search_highlights(&query);
102 }
103 }
104
105 let mut show_search_options = self.prompt.as_ref().is_some_and(|p| {
115 matches!(
116 p.prompt_type,
117 PromptType::Search
118 | PromptType::ReplaceSearch
119 | PromptType::Replace { .. }
120 | PromptType::QueryReplaceSearch
121 | PromptType::QueryReplace { .. }
122 )
123 });
124
125 let mut prompt_is_overlay = self.prompt.as_ref().is_some_and(|p| p.overlay);
132 let mut has_suggestions = self
133 .prompt
134 .as_ref()
135 .is_some_and(|p| !p.suggestions.is_empty())
136 && !prompt_is_overlay;
137 let mut has_file_browser = self.prompt.as_ref().is_some_and(|p| {
138 matches!(
139 p.prompt_type,
140 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
141 )
142 }) && self.file_open_state.is_some();
143
144 let mut main_chunks = Layout::default()
148 .direction(Direction::Vertical)
149 .constraints(vec![
150 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }), Constraint::Min(0), Constraint::Length(
153 if !self.status_bar_visible || has_suggestions || has_file_browser {
154 0
155 } else {
156 1
157 },
158 ), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(
161 if (self.prompt_line_visible || self.prompt.is_some()) && !prompt_is_overlay {
167 1
168 } else {
169 0
170 },
171 ), ])
173 .split(size);
174
175 let menu_bar_area = main_chunks[0];
176 let main_content_area = main_chunks[1];
177 let status_bar_idx = 2;
178 let search_options_idx = 3;
179 let prompt_line_idx = 4;
180
181 let editor_content_area;
184 let file_explorer_should_show = self.file_explorer_visible
185 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
186
187 if file_explorer_should_show {
188 tracing::trace!(
190 "render: file explorer layout active (present={}, sync_in_progress={}, side={:?})",
191 self.file_explorer.is_some(),
192 self.file_explorer_sync_in_progress,
193 self.file_explorer_side
194 );
195 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
196
197 let (explorer_area, editor_area) = match self.file_explorer_side {
198 FileExplorerSide::Left => {
199 let chunks = Layout::default()
200 .direction(Direction::Horizontal)
201 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
202 .split(main_content_area);
203 (chunks[0], chunks[1])
204 }
205 FileExplorerSide::Right => {
206 let chunks = Layout::default()
207 .direction(Direction::Horizontal)
208 .constraints([Constraint::Min(0), Constraint::Length(explorer_cols)])
209 .split(main_content_area);
210 (chunks[1], chunks[0])
211 }
212 };
213
214 self.cached_layout.file_explorer_area = Some(explorer_area);
215 editor_content_area = editor_area;
216
217 let remote_connection = self.connection_display_string();
219
220 if let Some(ref mut explorer) = self.file_explorer {
222 let is_focused = self.key_context == KeyContext::FileExplorer;
223
224 let mut files_with_unsaved_changes = std::collections::HashSet::new();
226 for (buffer_id, state) in &self.buffers {
227 if state.buffer.is_modified() {
228 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
229 if let Some(file_path) = metadata.file_path() {
230 files_with_unsaved_changes.insert(file_path.clone());
231 }
232 }
233 }
234 }
235
236 let close_button_hovered = matches!(
237 &self.mouse_state.hover_target,
238 Some(HoverTarget::FileExplorerCloseButton)
239 );
240 let keybindings = self.keybindings.read().unwrap();
241 let empty: Vec<std::path::PathBuf> = Vec::new();
242 let cut_paths = self
243 .file_explorer_clipboard
244 .as_ref()
245 .filter(|cb| cb.is_cut)
246 .map(|cb| cb.paths.as_slice())
247 .unwrap_or(empty.as_slice());
248 FileExplorerRenderer::render(
249 explorer,
250 frame,
251 explorer_area,
252 is_focused,
253 &files_with_unsaved_changes,
254 &self.file_explorer_decoration_cache,
255 &keybindings,
256 self.key_context.clone(),
257 &self.theme,
258 close_button_hovered,
259 remote_connection.as_deref(),
260 cut_paths,
261 );
262 }
263 } else {
266 self.cached_layout.file_explorer_area = None;
268 editor_content_area = main_content_area;
269 }
270
271 if self.plugin_manager.is_active() {
278 let hooks_start = std::time::Instant::now();
279 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
281
282 let mut total_new_lines = 0usize;
283 for (split_id, buffer_id, split_area) in visible_buffers {
284 let viewport_top_byte = self
286 .split_view_states
287 .get(&split_id)
288 .map(|vs| vs.viewport.top_byte)
289 .unwrap_or(0);
290
291 if let Some(state) = self.buffers.get_mut(&buffer_id) {
292 self.plugin_manager.run_hook(
294 "render_start",
295 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
296 );
297
298 let visible_count = split_area.height as usize;
301 let is_binary = state.buffer.is_binary();
302 let line_ending = state.buffer.line_ending();
303 let base_tokens =
304 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
305 &mut state.buffer,
306 viewport_top_byte,
307 self.config.editor.estimated_line_length,
308 visible_count,
309 is_binary,
310 line_ending,
311 );
312 let viewport_start = viewport_top_byte;
313 let viewport_end = base_tokens
314 .last()
315 .and_then(|t| t.source_offset)
316 .unwrap_or(viewport_start);
317 let cursor_positions: Vec<usize> = self
318 .split_view_states
319 .get(&split_id)
320 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
321 .unwrap_or_default();
322 self.plugin_manager.run_hook(
323 "view_transform_request",
324 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
325 buffer_id,
326 split_id: split_id.into(),
327 viewport_start,
328 viewport_end,
329 tokens: base_tokens,
330 cursor_positions,
331 },
332 );
333
334 if let Some(vs) = self.split_view_states.get_mut(&split_id) {
338 vs.view_transform_stale = false;
339 }
340
341 let visible_count = split_area.height as usize;
343 let top_byte = viewport_top_byte;
344
345 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
347
348 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
350 let mut line_number = state.buffer.get_line_number(top_byte);
351 let mut iter = state
352 .buffer
353 .line_iterator(top_byte, self.config.editor.estimated_line_length);
354
355 for _ in 0..visible_count {
356 if let Some((line_start, line_content)) = iter.next_line() {
357 let byte_end = line_start + line_content.len();
358 let byte_range = (line_start, byte_end);
359
360 if !seen_byte_ranges.contains(&byte_range) {
362 new_lines.push(crate::services::plugins::hooks::LineInfo {
363 line_number,
364 byte_start: line_start,
365 byte_end,
366 content: line_content,
367 });
368 seen_byte_ranges.insert(byte_range);
369 }
370 line_number += 1;
371 } else {
372 break;
373 }
374 }
375
376 if !new_lines.is_empty() {
378 total_new_lines += new_lines.len();
379 self.plugin_manager.run_hook(
380 "lines_changed",
381 crate::services::plugins::hooks::HookArgs::LinesChanged {
382 buffer_id,
383 lines: new_lines,
384 },
385 );
386 }
387 }
388 }
389 let hooks_elapsed = hooks_start.elapsed();
390 tracing::trace!(
391 new_lines = total_new_lines,
392 elapsed_ms = hooks_elapsed.as_millis(),
393 elapsed_us = hooks_elapsed.as_micros(),
394 "lines_changed hooks total"
395 );
396
397 let commands = self.plugin_manager.process_commands();
409 let dispatched_any = !commands.is_empty();
410 if dispatched_any {
411 let cmd_names: Vec<String> =
412 commands.iter().map(|c| c.debug_variant_name()).collect();
413 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
414 }
415 for command in commands {
416 if let Err(e) = self.handle_plugin_command(command) {
417 tracing::error!("Error handling plugin command: {}", e);
418 }
419 }
420
421 self.flush_pending_grammars();
423
424 if dispatched_any {
452 show_search_options = self.prompt.as_ref().is_some_and(|p| {
453 matches!(
454 p.prompt_type,
455 PromptType::Search
456 | PromptType::ReplaceSearch
457 | PromptType::Replace { .. }
458 | PromptType::QueryReplaceSearch
459 | PromptType::QueryReplace { .. }
460 )
461 });
462 prompt_is_overlay = self.prompt.as_ref().is_some_and(|p| p.overlay);
463 has_suggestions = self
464 .prompt
465 .as_ref()
466 .is_some_and(|p| !p.suggestions.is_empty())
467 && !prompt_is_overlay;
468 has_file_browser = self.prompt.as_ref().is_some_and(|p| {
469 matches!(
470 p.prompt_type,
471 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
472 )
473 }) && self.file_open_state.is_some();
474 main_chunks = Layout::default()
475 .direction(Direction::Vertical)
476 .constraints(vec![
477 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
478 Constraint::Min(0),
479 Constraint::Length(
480 if !self.status_bar_visible || has_suggestions || has_file_browser {
481 0
482 } else {
483 1
484 },
485 ),
486 Constraint::Length(if show_search_options { 1 } else { 0 }),
487 Constraint::Length(
488 if (self.prompt_line_visible || self.prompt.is_some())
489 && !prompt_is_overlay
490 {
491 1
492 } else {
493 0
494 },
495 ),
496 ])
497 .split(size);
498 }
499 }
500
501 let lsp_waiting = !self.pending_completion_requests.is_empty()
503 || self.pending_goto_definition_request.is_some();
504
505 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
512 let hide_cursor = self.menu_state.active_menu.is_some()
513 || self.key_context == KeyContext::FileExplorer
514 || self.terminal_mode
515 || settings_visible
516 || self.keybinding_editor.is_some();
517
518 let hovered_tab = match &self.mouse_state.hover_target {
520 Some(HoverTarget::TabName(target, split_id)) => Some((*target, *split_id, false)),
521 Some(HoverTarget::TabCloseButton(target, split_id)) => Some((*target, *split_id, true)),
522 _ => None,
523 };
524
525 let hovered_close_split = match &self.mouse_state.hover_target {
527 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
528 _ => None,
529 };
530
531 let hovered_maximize_split = match &self.mouse_state.hover_target {
533 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
534 _ => None,
535 };
536
537 let is_maximized = self.split_manager.is_maximized();
538
539 let mut pending_hardware_cursor: Option<(u16, u16)> = None;
546
547 let _content_span = tracing::info_span!("render_content").entered();
548 let (
549 split_areas,
550 tab_layouts,
551 close_split_areas,
552 maximize_split_areas,
553 view_line_mappings,
554 horizontal_scrollbar_areas,
555 grouped_separator_areas,
556 ) = SplitRenderer::render_content(
557 frame,
558 editor_content_area,
559 &self.split_manager,
560 &mut self.buffers,
561 &self.buffer_metadata,
562 &mut self.event_logs,
563 &mut self.composite_buffers,
564 &mut self.composite_view_states,
565 &self.theme,
566 self.ansi_background.as_ref(),
567 self.background_fade,
568 lsp_waiting,
569 self.config.editor.large_file_threshold_bytes,
570 self.config.editor.line_wrap,
571 self.config.editor.estimated_line_length,
572 self.config.editor.highlight_context_bytes,
573 Some(&mut self.split_view_states),
574 &self.grouped_subtrees,
575 hide_cursor,
576 hovered_tab,
577 hovered_close_split,
578 hovered_maximize_split,
579 is_maximized,
580 self.config.editor.relative_line_numbers,
581 self.tab_bar_visible,
582 self.config.editor.use_terminal_bg,
583 self.session_mode || !self.software_cursor_only,
584 self.software_cursor_only,
585 self.config.editor.show_vertical_scrollbar,
586 self.config.editor.show_horizontal_scrollbar,
587 self.config.editor.diagnostics_inline_text,
588 self.config.editor.show_tilde,
589 self.config.editor.highlight_current_column,
590 &mut self.cached_layout.cell_theme_map,
591 size.width,
592 &mut pending_hardware_cursor,
593 );
594
595 drop(_content_span);
596
597 self.maybe_start_cursor_jump_animation(pending_hardware_cursor, active_split);
603
604 if self.plugin_manager.is_active() {
608 for (split_id, view_state) in &self.split_view_states {
609 let current = (
610 view_state.viewport.top_byte,
611 view_state.viewport.width,
612 view_state.viewport.height,
613 );
614 let (changed, previous) = match self.previous_viewports.get(split_id) {
619 Some(previous) => (*previous != current, Some(*previous)),
620 None => (false, None), };
622 tracing::trace!(
623 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
624 split_id,
625 current,
626 previous,
627 changed
628 );
629 if changed {
630 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
631 let top_line = self.buffers.get(&buffer_id).and_then(|state| {
633 if state.buffer.line_count().is_some() {
634 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
635 } else {
636 None
637 }
638 });
639 tracing::debug!(
640 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
641 split_id,
642 buffer_id,
643 view_state.viewport.top_byte,
644 top_line
645 );
646 self.plugin_manager.run_hook(
647 "viewport_changed",
648 crate::services::plugins::hooks::HookArgs::ViewportChanged {
649 split_id: (*split_id).into(),
650 buffer_id,
651 top_byte: view_state.viewport.top_byte,
652 top_line,
653 width: view_state.viewport.width,
654 height: view_state.viewport.height,
655 },
656 );
657 }
658 }
659 }
660 }
661
662 self.previous_viewports.clear();
664 for (split_id, view_state) in &self.split_view_states {
665 self.previous_viewports.insert(
666 *split_id,
667 (
668 view_state.viewport.top_byte,
669 view_state.viewport.width,
670 view_state.viewport.height,
671 ),
672 );
673 }
674
675 self.render_terminal_splits(frame, &split_areas);
677
678 self.cached_layout.split_areas = split_areas;
679 self.cached_layout.horizontal_scrollbar_areas = horizontal_scrollbar_areas;
680 self.cached_layout.tab_layouts = tab_layouts;
681 self.cached_layout.close_split_areas = close_split_areas;
682 self.cached_layout.maximize_split_areas = maximize_split_areas;
683 self.cached_layout.view_line_mappings = view_line_mappings;
684
685 self.drain_pending_vb_animations();
690 let mut separator_areas = self
691 .split_manager
692 .get_separators_with_ids(editor_content_area);
693 separator_areas.extend(grouped_separator_areas);
698 self.cached_layout.separator_areas = separator_areas;
699 self.cached_layout.editor_content_area = Some(editor_content_area);
700
701 self.render_hover_highlights(frame);
703
704 self.cached_layout.suggestions_area = None;
706 self.cached_layout.suggestions_outer_area = None;
707 self.file_browser_layout = None;
708
709 let display_name = self
711 .buffer_metadata
712 .get(&self.active_buffer())
713 .map(|m| m.display_name.clone())
714 .unwrap_or_else(|| "[No Name]".to_string());
715
716 self.update_terminal_title(&display_name);
720
721 let status_message = self.status_message.clone();
722 let plugin_status_message = self.plugin_status_message.clone();
723 let prompt = self.prompt.clone();
724 let current_language = self
747 .buffers
748 .get(&self.active_buffer())
749 .map(|s| s.language.clone())
750 .unwrap_or_default();
751 let buffer_lsp_disabled_reason = self
752 .buffer_metadata
753 .get(&self.active_buffer())
754 .filter(|m| !m.lsp_enabled)
755 .and_then(|m| m.lsp_disabled_reason.as_deref());
756 let (lsp_status, lsp_indicator_state) = compose_lsp_status(
757 ¤t_language,
758 buffer_lsp_disabled_reason,
759 &self.lsp_progress,
760 &self.lsp_server_statuses,
761 &self.config.lsp,
762 &self.user_dismissed_lsp_languages,
763 );
764 let theme = self.theme.clone();
765 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());
770
771 if self.status_bar_visible && !has_suggestions && !has_file_browser {
773 let (warning_level, general_warning_count) =
776 if self.config.warnings.show_status_indicator {
777 let lsp_level = {
778 use crate::services::async_bridge::LspServerStatus;
779 let mut level = WarningLevel::None;
780 for ((lang, _), status) in &self.lsp_server_statuses {
781 if lang == ¤t_language {
782 match status {
783 LspServerStatus::Error => {
784 level = WarningLevel::Error;
785 break;
786 }
787 LspServerStatus::Starting | LspServerStatus::Initializing => {
788 if level != WarningLevel::Error {
789 level = WarningLevel::Warning;
790 }
791 }
792 _ => {}
793 }
794 }
795 }
796 level
797 };
798 (lsp_level, self.get_general_warning_count())
799 } else {
800 (WarningLevel::None, 0)
801 };
802
803 use crate::view::ui::status_bar::StatusBarHover;
805 let status_bar_hover = match &self.mouse_state.hover_target {
806 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
807 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
808 Some(HoverTarget::StatusBarLineEndingIndicator) => {
809 StatusBarHover::LineEndingIndicator
810 }
811 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
812 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
813 Some(HoverTarget::StatusBarRemoteIndicator) => StatusBarHover::RemoteIndicator,
814 _ => StatusBarHover::None,
815 };
816
817 let remote_connection = self.connection_display_string();
818
819 let session_name = self.session_name().map(|s| s.to_string());
821
822 let active_split = self.effective_active_split();
823 let active_buf = self.active_buffer();
824 let default_cursors = crate::model::cursor::Cursors::new();
825 let status_cursors = self
826 .split_view_states
827 .get(&active_split)
828 .map(|vs| &vs.cursors)
829 .unwrap_or(&default_cursors);
830 let is_read_only = self
831 .buffer_metadata
832 .get(&active_buf)
833 .map(|m| m.read_only)
834 .unwrap_or(false);
835 let is_synthetic_placeholder = self
836 .buffer_metadata
837 .get(&active_buf)
838 .map(|m| m.synthetic_placeholder)
839 .unwrap_or(false);
840 let mut status_ctx = crate::view::ui::status_bar::StatusBarContext {
841 state: self.buffers.get_mut(&active_buf).unwrap(),
842 cursors: status_cursors,
843 status_message: &status_message,
844 plugin_status_message: &plugin_status_message,
845 lsp_status: &lsp_status,
846 lsp_indicator_state,
847 theme: &theme,
848 display_name: &display_name,
849 keybindings: &keybindings_cloned,
850 chord_state: &chord_state_cloned,
851 update_available: update_available.as_deref(),
852 warning_level,
853 general_warning_count,
854 hover: status_bar_hover,
855 remote_connection: remote_connection.as_deref(),
856 session_name: session_name.as_deref(),
857 read_only: is_read_only,
858 remote_state_override: self.remote_indicator_override.as_ref(),
859 is_synthetic_placeholder,
860 remote_indicator_on_bar: false,
865 };
866 let status_bar_layout = StatusBarRenderer::render_status_bar(
867 frame,
868 main_chunks[status_bar_idx],
869 &mut status_ctx,
870 &self.config.editor.status_bar,
871 );
872
873 let status_bar_area = main_chunks[status_bar_idx];
875 self.cached_layout.status_bar_area =
876 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
877 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
878 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
879 self.cached_layout.status_bar_line_ending_area =
880 status_bar_layout.line_ending_indicator;
881 self.cached_layout.status_bar_encoding_area = status_bar_layout.encoding_indicator;
882 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
883 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
884 self.cached_layout.status_bar_remote_area = status_bar_layout.remote_indicator;
885 }
886
887 if show_search_options {
889 let confirm_each = self.prompt.as_ref().and_then(|p| {
891 if matches!(
892 p.prompt_type,
893 PromptType::ReplaceSearch
894 | PromptType::Replace { .. }
895 | PromptType::QueryReplaceSearch
896 | PromptType::QueryReplace { .. }
897 ) {
898 Some(self.search_confirm_each)
899 } else {
900 None
901 }
902 });
903
904 use crate::view::ui::status_bar::SearchOptionsHover;
906 let search_options_hover = match &self.mouse_state.hover_target {
907 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
908 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
909 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
910 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
911 _ => SearchOptionsHover::None,
912 };
913
914 let search_options_layout = StatusBarRenderer::render_search_options(
915 frame,
916 main_chunks[search_options_idx],
917 self.search_case_sensitive,
918 self.search_whole_word,
919 self.search_use_regex,
920 confirm_each,
921 &theme,
922 &keybindings_cloned,
923 search_options_hover,
924 );
925 self.cached_layout.search_options_layout = Some(search_options_layout);
926 } else {
927 self.cached_layout.search_options_layout = None;
928 }
929
930 if let Some(prompt) = &prompt {
935 if !prompt.overlay {
936 if matches!(
938 prompt.prompt_type,
939 crate::view::prompt::PromptType::OpenFile
940 | crate::view::prompt::PromptType::SwitchProject
941 ) {
942 if let Some(file_open_state) = &self.file_open_state {
943 StatusBarRenderer::render_file_open_prompt(
944 frame,
945 main_chunks[prompt_line_idx],
946 prompt,
947 file_open_state,
948 &theme,
949 );
950 } else {
951 StatusBarRenderer::render_prompt(
952 frame,
953 main_chunks[prompt_line_idx],
954 prompt,
955 &theme,
956 );
957 }
958 } else {
959 StatusBarRenderer::render_prompt(
960 frame,
961 main_chunks[prompt_line_idx],
962 prompt,
963 &theme,
964 );
965 }
966 }
967 }
968
969 if self.prompt.as_ref().is_some_and(|p| p.overlay) {
974 self.prepare_overlay_preview();
975 }
976
977 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
980
981 let theme_clone = self.theme.clone();
984 let hover_target = self.mouse_state.hover_target.clone();
985
986 self.cached_layout.popup_areas.clear();
988
989 let popup_info: Vec<_> = {
991 let active_split = self.split_manager.active_split();
993 let viewport = self
994 .split_view_states
995 .get(&active_split)
996 .map(|vs| vs.viewport.clone());
997
998 let content_rect = self
1003 .cached_layout
1004 .split_areas
1005 .iter()
1006 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1007 .map(|(_, _, rect, _, _, _)| *rect);
1008
1009 let primary_cursor = self
1010 .split_view_states
1011 .get(&active_split)
1012 .map(|vs| *vs.cursors.primary());
1013 let state = self.active_state_mut();
1014 if state.popups.is_visible() {
1015 let primary_cursor =
1017 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
1018
1019 let gutter_width = viewport
1021 .as_ref()
1022 .map(|vp| vp.gutter_width(&state.buffer) as u16)
1023 .unwrap_or(0);
1024
1025 let cursor_screen_pos = viewport
1026 .as_ref()
1027 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
1028 .unwrap_or((0, 0));
1029
1030 let word_start_screen_pos = {
1034 use crate::primitives::word_navigation::find_completion_word_start;
1035 let word_start =
1036 find_completion_word_start(&state.buffer, primary_cursor.position);
1037 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
1038 viewport
1039 .as_ref()
1040 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
1041 .unwrap_or((0, 0))
1042 };
1043
1044 let (base_x, base_y) = content_rect
1049 .map(|r| (r.x + gutter_width, r.y))
1050 .unwrap_or((gutter_width, 1));
1051
1052 let cursor_screen_pos =
1053 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
1054 let word_start_screen_pos = (
1055 word_start_screen_pos.0 + base_x,
1056 word_start_screen_pos.1 + base_y,
1057 );
1058
1059 state
1061 .popups
1062 .all()
1063 .iter()
1064 .enumerate()
1065 .map(|(popup_idx, popup)| {
1066 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
1068 (word_start_screen_pos.0, cursor_screen_pos.1)
1069 } else {
1070 cursor_screen_pos
1071 };
1072 let popup_area = popup.calculate_area(size, Some(popup_pos));
1073
1074 let desc_height = popup.description_height();
1077 let inner_area = if popup.bordered {
1078 ratatui::layout::Rect {
1079 x: popup_area.x + 1,
1080 y: popup_area.y + 1 + desc_height,
1081 width: popup_area.width.saturating_sub(2),
1082 height: popup_area.height.saturating_sub(2 + desc_height),
1083 }
1084 } else {
1085 ratatui::layout::Rect {
1086 x: popup_area.x,
1087 y: popup_area.y + desc_height,
1088 width: popup_area.width,
1089 height: popup_area.height.saturating_sub(desc_height),
1090 }
1091 };
1092
1093 let num_items = match &popup.content {
1094 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1095 _ => 0,
1096 };
1097
1098 let total_lines = popup.item_count();
1100 let visible_lines = inner_area.height as usize;
1101 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
1102 {
1103 Some(ratatui::layout::Rect {
1104 x: inner_area.x + inner_area.width - 1,
1105 y: inner_area.y,
1106 width: 1,
1107 height: inner_area.height,
1108 })
1109 } else {
1110 None
1111 };
1112
1113 (
1114 popup_idx,
1115 popup_area,
1116 inner_area,
1117 popup.scroll_offset,
1118 num_items,
1119 scrollbar_rect,
1120 total_lines,
1121 )
1122 })
1123 .collect()
1124 } else {
1125 Vec::new()
1126 }
1127 };
1128
1129 self.cached_layout.popup_areas = popup_info.clone();
1131
1132 let state = self.active_state_mut();
1134 if state.popups.is_visible() {
1135 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
1136 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
1137 popup.render_with_hover(
1138 frame,
1139 *popup_area,
1140 &theme_clone,
1141 hover_target.as_ref(),
1142 );
1143 }
1144 }
1145 }
1146
1147 self.cached_layout.global_popup_areas.clear();
1158 if let Some(popup) = self.global_popups.top() {
1159 let top_idx = self.global_popups.all().len() - 1;
1160 let popup_area = popup.calculate_area(size, None);
1161 let desc_height = popup.description_height();
1162 let inner_area = if popup.bordered {
1163 ratatui::layout::Rect {
1164 x: popup_area.x + 1,
1165 y: popup_area.y + 1 + desc_height,
1166 width: popup_area.width.saturating_sub(2),
1167 height: popup_area.height.saturating_sub(2 + desc_height),
1168 }
1169 } else {
1170 ratatui::layout::Rect {
1171 x: popup_area.x,
1172 y: popup_area.y + desc_height,
1173 width: popup_area.width,
1174 height: popup_area.height.saturating_sub(desc_height),
1175 }
1176 };
1177 let num_items = match &popup.content {
1178 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1179 _ => 0,
1180 };
1181 self.cached_layout.global_popup_areas.push((
1182 top_idx,
1183 popup_area,
1184 inner_area,
1185 popup.scroll_offset,
1186 num_items,
1187 ));
1188 popup.render_with_hover(frame, popup_area, &theme_clone, hover_target.as_ref());
1189 }
1190
1191 self.update_menu_context();
1194
1195 let settings_visible = self
1198 .settings_state
1199 .as_ref()
1200 .map(|s| s.visible)
1201 .unwrap_or(false);
1202 if settings_visible {
1203 crate::view::dimming::apply_dimming(frame, size);
1205 }
1206 if let Some(ref mut settings_state) = self.settings_state {
1207 if settings_state.visible {
1208 settings_state.update_focus_states();
1209 let settings_layout = crate::view::settings::render_settings(
1210 frame,
1211 size,
1212 settings_state,
1213 &self.theme,
1214 );
1215 self.cached_layout.settings_layout = Some(settings_layout);
1216 }
1217 }
1218
1219 if let Some(ref wizard) = self.calibration_wizard {
1221 crate::view::dimming::apply_dimming(frame, size);
1223 crate::view::calibration_wizard::render_calibration_wizard(
1224 frame,
1225 size,
1226 wizard,
1227 &self.theme,
1228 );
1229 }
1230
1231 if let Some(ref mut kb_editor) = self.keybinding_editor {
1233 crate::view::dimming::apply_dimming(frame, size);
1234 crate::view::keybinding_editor::render_keybinding_editor(
1235 frame,
1236 size,
1237 kb_editor,
1238 &self.theme,
1239 );
1240 }
1241
1242 if let Some(ref debug) = self.event_debug {
1244 crate::view::dimming::apply_dimming(frame, size);
1246 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
1247 }
1248
1249 if self.menu_bar_visible {
1250 self.expanded_menus_cache.update(
1254 &self.theme_registry,
1255 &self.menus,
1256 &self.menu_state.themes_dir,
1257 );
1258 let expanded = self.expanded_menus_cache.get().expect("just updated");
1259 let keybindings = self.keybindings.read().unwrap();
1260 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
1261 frame,
1262 menu_bar_area,
1263 expanded,
1264 &self.menu_state,
1265 &keybindings,
1266 &self.theme,
1267 self.mouse_state.hover_target.as_ref(),
1268 self.config.editor.menu_bar_mnemonics,
1269 ));
1270 } else {
1271 self.cached_layout.menu_layout = None;
1272 }
1273
1274 if let Some(ref menu) = self.tab_context_menu {
1276 self.render_tab_context_menu(frame, menu);
1277 }
1278
1279 if let Some(ref menu) = self.file_explorer_context_menu {
1280 self.render_file_explorer_context_menu(frame, menu);
1281 }
1282
1283 self.record_non_editor_theme_regions();
1285
1286 self.render_theme_info_popup(frame);
1288
1289 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
1291 if drag_state.is_dragging() {
1292 self.render_tab_drop_zone(frame, drag_state);
1293 }
1294 }
1295
1296 if self.gpm_active {
1302 if let Some((col, row)) = self.mouse_cursor_position {
1303 use ratatui::style::Modifier;
1304
1305 if col < size.width && row < size.height {
1307 let buf = frame.buffer_mut();
1309 if let Some(cell) = buf.cell_mut((col, row)) {
1310 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1311 }
1312 }
1313 }
1314 }
1315
1316 if self.keyboard_capture && self.terminal_mode {
1319 let active_split = self.split_manager.active_split();
1321 let active_split_area = self
1322 .cached_layout
1323 .split_areas
1324 .iter()
1325 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1326 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1327
1328 if let Some(terminal_area) = active_split_area {
1329 self.apply_keyboard_capture_dimming(frame, terminal_area);
1330 }
1331 }
1332
1333 if let Some((cx, cy)) = pending_hardware_cursor {
1344 if self.prompt.is_none() && !self.cursor_obscured_by_overlay(cx, cy) {
1345 frame.set_cursor_position((cx, cy));
1346 }
1347 }
1348
1349 crate::view::color_support::convert_buffer_colors(
1351 frame.buffer_mut(),
1352 self.color_capability,
1353 );
1354
1355 self.animations.apply_all(frame.buffer_mut());
1357 }
1358
1359 fn maybe_start_cursor_jump_animation(
1374 &mut self,
1375 current_pos: Option<(u16, u16)>,
1376 active_split: crate::model::event::LeafId,
1377 ) {
1378 if !self.config.editor.animations || !self.config.editor.cursor_jump_animation {
1386 self.previous_cursor_screen_pos = current_pos.map(|p| (p, active_split));
1387 return;
1388 }
1389
1390 let Some(current) = current_pos else {
1391 self.previous_cursor_screen_pos = None;
1395 return;
1396 };
1397
1398 let prev_entry = self.previous_cursor_screen_pos;
1399 self.previous_cursor_screen_pos = Some((current, active_split));
1401
1402 let Some((prev, prev_split)) = prev_entry else {
1403 return;
1404 };
1405 if prev == current && prev_split == active_split {
1406 return;
1407 }
1408
1409 let dx = (current.0 as i32 - prev.0 as i32).abs();
1410 let dy = (current.1 as i32 - prev.1 as i32).abs();
1411 let crossed_panes = prev_split != active_split;
1419 let row_jump = dy > 2;
1420 let col_jump = dx >= 80;
1421 if !crossed_panes && !row_jump && !col_jump {
1422 return;
1423 }
1424
1425 if let Some(prev_anim) = self.cursor_jump_animation.take() {
1427 self.animations.cancel(prev_anim);
1428 }
1429
1430 let id = self.animations.start(
1431 ratatui::layout::Rect {
1434 x: prev.0.min(current.0),
1435 y: prev.1.min(current.1),
1436 width: dx as u16 + 1,
1437 height: dy as u16 + 1,
1438 },
1439 crate::view::animation::AnimationKind::CursorJump {
1440 from: prev,
1441 to: current,
1442 duration: std::time::Duration::from_millis(140),
1443 cursor_color: self.theme.cursor,
1444 bg_color: self.theme.editor_bg,
1445 },
1446 );
1447 self.cursor_jump_animation = Some(id);
1448 }
1449
1450 fn cursor_obscured_by_overlay(&self, x: u16, y: u16) -> bool {
1454 let inside = |rect: ratatui::layout::Rect| -> bool {
1455 x >= rect.x
1456 && x < rect.x.saturating_add(rect.width)
1457 && y >= rect.y
1458 && y < rect.y.saturating_add(rect.height)
1459 };
1460
1461 if self
1462 .cached_layout
1463 .popup_areas
1464 .iter()
1465 .any(|entry| inside(entry.1))
1466 {
1467 return true;
1468 }
1469 if self
1470 .cached_layout
1471 .global_popup_areas
1472 .iter()
1473 .any(|entry| inside(entry.1))
1474 {
1475 return true;
1476 }
1477 if let Some((rect, _, _, _)) = self.cached_layout.suggestions_area {
1478 if inside(rect) {
1479 return true;
1480 }
1481 }
1482 if let Some(ref fb) = self.file_browser_layout {
1483 if inside(fb.popup_area) {
1484 return true;
1485 }
1486 }
1487 false
1488 }
1489
1490 fn render_quick_open_hints(
1492 frame: &mut Frame,
1493 area: ratatui::layout::Rect,
1494 theme: &crate::view::theme::Theme,
1495 ) {
1496 use ratatui::style::{Modifier, Style};
1497 use ratatui::text::{Line, Span};
1498 use ratatui::widgets::Paragraph;
1499 use rust_i18n::t;
1500
1501 let hints_style = Style::default()
1502 .fg(theme.line_number_fg)
1503 .bg(theme.suggestion_selected_bg)
1504 .add_modifier(Modifier::DIM);
1505 let hints_text = t!("quick_open.mode_hints");
1506 let left_margin = 2;
1508 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1509 let mut spans = Vec::new();
1510 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1511 spans.push(Span::styled(hints_text.to_string(), hints_style));
1512 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1513 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1514
1515 let paragraph = Paragraph::new(Line::from(spans));
1516 frame.render_widget(paragraph, area);
1517 }
1518
1519 fn apply_keyboard_capture_dimming(
1522 &self,
1523 frame: &mut Frame,
1524 terminal_area: ratatui::layout::Rect,
1525 ) {
1526 let size = frame.area();
1527 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1528 }
1529
1530 fn render_prompt_popups(
1533 &mut self,
1534 frame: &mut Frame,
1535 prompt_area: ratatui::layout::Rect,
1536 width: u16,
1537 ) {
1538 let Some(prompt) = &self.prompt else { return };
1539
1540 if prompt.overlay {
1543 let frame_area = frame.area();
1544 self.render_overlay_prompt(frame, frame_area);
1545 return;
1546 }
1547
1548 if matches!(
1549 prompt.prompt_type,
1550 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1551 ) {
1552 let Some(file_open_state) = &mut self.file_open_state else {
1553 return;
1554 };
1555 let max_height = prompt_area.y.saturating_sub(1).min(20);
1556 let popup_area = ratatui::layout::Rect {
1557 x: 0,
1558 y: prompt_area.y.saturating_sub(max_height),
1559 width,
1560 height: max_height,
1561 };
1562 let keybindings = self.keybindings.read().unwrap();
1563 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1564 frame,
1565 popup_area,
1566 file_open_state,
1567 &self.theme,
1568 &self.mouse_state.hover_target,
1569 Some(&*keybindings),
1570 );
1571 return;
1572 }
1573
1574 if prompt.suggestions.is_empty() {
1575 return;
1576 }
1577
1578 let suggestion_count = prompt.suggestions.len().min(10);
1579 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1580 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1581 let height = suggestion_count as u16 + 2 + hints_height;
1582
1583 let suggestions_area = ratatui::layout::Rect {
1584 x: 0,
1585 y: prompt_area.y.saturating_sub(height),
1586 width,
1587 height: height - hints_height,
1588 };
1589
1590 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1591
1592 if let Some(prompt) = self.prompt.as_mut() {
1595 prompt.ensure_selected_visible();
1596 }
1597 let Some(prompt) = &self.prompt else { return };
1598
1599 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
1600 frame,
1601 suggestions_area,
1602 prompt,
1603 &self.theme,
1604 self.mouse_state.hover_target.as_ref(),
1605 );
1606 if self.cached_layout.suggestions_area.is_some() {
1607 self.cached_layout.suggestions_outer_area = Some(suggestions_area);
1608 }
1609
1610 if is_quick_open {
1611 let hints_area = ratatui::layout::Rect {
1612 x: 0,
1613 y: prompt_area.y.saturating_sub(hints_height),
1614 width,
1615 height: hints_height,
1616 };
1617 frame.render_widget(ratatui::widgets::Clear, hints_area);
1618 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1619 }
1620 }
1621
1622 fn prepare_overlay_preview(&mut self) {
1628 use crate::input::quick_open::parse_path_line_col;
1629
1630 let (path_str, line, col) = {
1631 let Some(prompt) = self.prompt.as_ref() else {
1632 return;
1633 };
1634 let Some(idx) = prompt.selected_suggestion else {
1635 return;
1636 };
1637 let Some(s) = prompt.suggestions.get(idx) else {
1638 return;
1639 };
1640 let from_text = parse_path_line_col(&s.text);
1645 if !from_text.0.is_empty() && from_text.1.is_some() {
1646 from_text
1647 } else if let Some(v) = s.value.as_deref() {
1648 parse_path_line_col(v)
1649 } else {
1650 from_text
1651 }
1652 };
1653 if path_str.is_empty() {
1654 return;
1655 }
1656 let line = line.unwrap_or(1).saturating_sub(1);
1657 let col = col.unwrap_or(1).saturating_sub(1);
1658
1659 let path_buf = std::path::PathBuf::from(&path_str);
1661 let abs_path = if path_buf.is_absolute() {
1662 path_buf
1663 } else {
1664 self.working_dir.join(&path_buf)
1665 };
1666 let abs_path = self
1668 .authority
1669 .filesystem
1670 .canonicalize(&abs_path)
1671 .unwrap_or(abs_path);
1672
1673 let already_target = self.overlay_preview_state.as_ref().is_some_and(|st| {
1676 self.buffers
1677 .get(&st.buffer_id)
1678 .and_then(|s| s.buffer.file_path())
1679 .is_some_and(|p| p == abs_path.as_path())
1680 });
1681
1682 let buffer_id = if already_target {
1683 self.overlay_preview_state.as_ref().unwrap().buffer_id
1684 } else {
1685 let was_open = self
1689 .buffers
1690 .iter()
1691 .any(|(_, s)| s.buffer.file_path() == Some(abs_path.as_path()));
1692 let source_split = self.split_manager.active_split();
1697 let buffer_id = match self.open_file_for_preview(abs_path.as_path()) {
1702 Ok(id) => id,
1703 Err(_e) => return,
1704 };
1705 if !was_open {
1706 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
1707 meta.hidden_from_tabs = true;
1708 }
1709 let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
1715 for leaf_id in leaf_ids {
1716 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
1717 view_state.remove_buffer(buffer_id);
1718 }
1719 }
1720 if let Some(source_state) = self.split_view_states.get_mut(&source_split) {
1723 if source_state.active_buffer == buffer_id {
1724 let preview_loaded: std::collections::HashSet<BufferId> = self
1725 .overlay_preview_state
1726 .as_ref()
1727 .map(|st| st.loaded_buffers.clone())
1728 .unwrap_or_default();
1729 let fallback = source_state
1730 .open_buffers
1731 .iter()
1732 .find_map(|t| t.as_buffer())
1733 .or_else(|| {
1734 self.buffers
1735 .keys()
1736 .copied()
1737 .find(|b| *b != buffer_id && !preview_loaded.contains(b))
1738 });
1739 if let Some(fb) = fallback {
1740 source_state.switch_buffer(fb);
1741 self.split_manager.set_split_buffer(source_split, fb);
1742 }
1743 }
1744 }
1745 self.split_manager.set_active_split(source_split);
1746 }
1747 buffer_id
1748 };
1749
1750 let need_init = self.overlay_preview_state.is_none();
1754 if need_init {
1755 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1756 self.terminal_width,
1757 self.terminal_height,
1758 buffer_id,
1759 );
1760 view_state.apply_config_defaults(
1761 self.config.editor.line_numbers,
1762 self.config.editor.highlight_current_line,
1763 self.resolve_line_wrap_for_buffer(buffer_id),
1764 self.config.editor.wrap_indent,
1765 self.resolve_wrap_column_for_buffer(buffer_id),
1766 self.config.editor.rulers.clone(),
1767 );
1768 let mut loaded_buffers = std::collections::HashSet::new();
1769 if let Some(meta) = self.buffer_metadata.get(&buffer_id) {
1778 if meta.hidden_from_tabs {
1779 loaded_buffers.insert(buffer_id);
1780 }
1781 }
1782 self.overlay_preview_state = Some(crate::app::types::OverlayPreviewState {
1783 buffer_id,
1784 view_state,
1785 loaded_buffers,
1786 });
1787 } else if let Some(state) = self.overlay_preview_state.as_mut() {
1788 if state.buffer_id != buffer_id {
1789 state.view_state.switch_buffer(buffer_id);
1790 state.buffer_id = buffer_id;
1791 if let Some(meta) = self.buffer_metadata.get(&buffer_id) {
1792 if meta.hidden_from_tabs {
1793 state.loaded_buffers.insert(buffer_id);
1794 }
1795 }
1796 }
1797 }
1798
1799 let byte_offset = self
1801 .buffers
1802 .get(&buffer_id)
1803 .map(|s| {
1804 s.buffer
1805 .position_to_offset(crate::model::piece_tree::Position { line, column: col })
1806 })
1807 .unwrap_or(0);
1808 let line_start = self
1809 .buffers
1810 .get(&buffer_id)
1811 .and_then(|s| s.buffer.line_start_offset(line))
1812 .unwrap_or(byte_offset);
1813 if let Some(state) = self.overlay_preview_state.as_mut() {
1814 state.view_state.cursors.primary_mut().position = byte_offset;
1815 let h = state.view_state.viewport.height.max(1) as usize;
1816 let half = h / 2;
1817 let target_top_line = line.saturating_sub(half);
1818 let top_byte = self
1819 .buffers
1820 .get(&buffer_id)
1821 .and_then(|s| s.buffer.line_start_offset(target_top_line))
1822 .unwrap_or(line_start);
1823 state.view_state.viewport.top_byte = top_byte;
1824 }
1825 }
1826
1827 fn render_overlay_prompt(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
1844 use crate::view::popup::PopupPosition;
1845 use ratatui::layout::Rect;
1846 use ratatui::style::{Modifier, Style};
1847 use ratatui::text::{Line, Span};
1848 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1849
1850 let overlay_pos = PopupPosition::CenteredOverlay {
1853 width_pct: 80,
1854 height_pct: 80,
1855 };
1856 let overlay_rect = match overlay_pos {
1857 PopupPosition::CenteredOverlay {
1858 width_pct,
1859 height_pct,
1860 } => {
1861 let w_pct = width_pct.clamp(1, 100) as u32;
1862 let h_pct = height_pct.clamp(1, 100) as u32;
1863 let w = ((area.width as u32 * w_pct) / 100) as u16;
1864 let h = ((area.height as u32 * h_pct) / 100) as u16;
1865 let w = w.max(20).min(area.width);
1866 let h = h.max(8).min(area.height);
1867 Rect {
1868 x: (area.width.saturating_sub(w)) / 2,
1869 y: (area.height.saturating_sub(h)) / 2,
1870 width: w,
1871 height: h,
1872 }
1873 }
1874 _ => unreachable!(),
1875 };
1876
1877 let theme = self.theme.clone();
1879 let suggestions_visible_rows = (overlay_rect.height as usize).saturating_sub(6);
1892 if let Some(prompt) = self.prompt.as_mut() {
1893 prompt.ensure_selected_visible_within(suggestions_visible_rows);
1894 }
1895 let Some(prompt) = self.prompt.as_ref() else {
1896 return;
1897 };
1898 let prompt = prompt.clone();
1899
1900 frame.render_widget(Clear, overlay_rect);
1906 let default_title_owned: String = {
1907 use crate::input::keybindings::KeyContext;
1908 let keybindings = self.keybindings.read().unwrap();
1909 let mut hints: Vec<String> = Vec::new();
1910 if let Some(k) = keybindings
1911 .find_keybinding_for_action("cycle_live_grep_provider", KeyContext::Prompt)
1912 {
1913 hints.push(format!("{k} cycle"));
1914 }
1915 if let Some(k) = keybindings
1916 .find_keybinding_for_action("live_grep_export_quickfix", KeyContext::Prompt)
1917 {
1918 hints.push(format!("{k} → Quickfix"));
1919 }
1920 if let Some(k) =
1921 keybindings.find_keybinding_for_action("resume_live_grep", KeyContext::Normal)
1922 {
1923 hints.push(format!("{k} resume"));
1924 }
1925 if hints.is_empty() {
1926 " Live Grep ".to_string()
1927 } else {
1928 format!(" Live Grep · {} ", hints.join(" · "))
1929 }
1930 };
1931 let title_owned: String;
1932 let title: &str = match prompt.title.as_deref() {
1933 Some(t) if !t.is_empty() => {
1934 title_owned = format!(" {} ", t.trim());
1937 &title_owned
1938 }
1939 _ => &default_title_owned,
1940 };
1941 let block = Block::default()
1942 .borders(Borders::ALL)
1943 .border_style(Style::default().fg(theme.popup_border_fg))
1944 .style(Style::default().bg(theme.suggestion_bg))
1945 .title(Span::styled(
1946 title,
1947 Style::default()
1948 .fg(theme.prompt_fg)
1949 .add_modifier(Modifier::BOLD),
1950 ));
1951 let inner = block.inner(overlay_rect);
1952 frame.render_widget(block, overlay_rect);
1953
1954 if inner.height == 0 || inner.width == 0 {
1955 return;
1956 }
1957
1958 let preview_min_cols: u16 = 120;
1962 let show_preview = overlay_rect.width >= preview_min_cols;
1963 let (results_area, preview_area) = if show_preview {
1964 let results_w = inner.width / 2;
1965 (
1966 Rect {
1967 x: inner.x,
1968 y: inner.y,
1969 width: results_w,
1970 height: inner.height,
1971 },
1972 Some(Rect {
1973 x: inner.x + results_w,
1974 y: inner.y,
1975 width: inner.width - results_w,
1976 height: inner.height,
1977 }),
1978 )
1979 } else {
1980 (inner, None)
1981 };
1982
1983 let input_row = Rect {
1985 x: results_area.x,
1986 y: results_area.y,
1987 width: results_area.width,
1988 height: 1,
1989 };
1990 let input_style = Style::default().fg(theme.prompt_fg).bg(theme.prompt_bg);
1991 let count_str = if prompt.suggestions.is_empty() {
1992 String::new()
1993 } else {
1994 format!(
1995 " {} / {}",
1996 prompt.selected_suggestion.map(|i| i + 1).unwrap_or(0),
1997 prompt.suggestions.len()
1998 )
1999 };
2000 let visible_input_width =
2001 results_area.width.saturating_sub(count_str.len() as u16) as usize;
2002 let truncated_input: String = prompt
2003 .input
2004 .chars()
2005 .take(visible_input_width.saturating_sub(prompt.message.len()))
2006 .collect();
2007 let line = Line::from(vec![
2008 Span::styled(prompt.message.clone(), input_style),
2009 Span::styled(truncated_input, input_style),
2010 Span::styled(
2011 count_str,
2012 Style::default()
2013 .fg(theme.popup_border_fg)
2014 .bg(theme.suggestion_bg),
2015 ),
2016 ]);
2017 frame.render_widget(Paragraph::new(line).style(input_style), input_row);
2018
2019 use crate::primitives::display_width::str_width;
2021 let cursor_x = (str_width(&prompt.message)
2022 + str_width(&prompt.input[..prompt.cursor_pos.min(prompt.input.len())]))
2023 as u16;
2024 if cursor_x < input_row.width {
2025 frame.set_cursor_position((input_row.x + cursor_x, input_row.y));
2026 }
2027
2028 if results_area.height >= 2 {
2030 let sep = Rect {
2031 x: results_area.x,
2032 y: results_area.y + 1,
2033 width: results_area.width,
2034 height: 1,
2035 };
2036 let sep_style = Style::default()
2037 .fg(theme.popup_border_fg)
2038 .bg(theme.suggestion_bg);
2039 let sep_text = "─".repeat(results_area.width as usize);
2040 frame.render_widget(Paragraph::new(sep_text).style(sep_style), sep);
2041 }
2042
2043 if results_area.height > 2 {
2051 let inner_rows = (results_area.height - 2).saturating_sub(2) as usize; let needs_scrollbar = prompt.suggestions.len() > inner_rows.max(1);
2053 let scrollbar_w: u16 = if needs_scrollbar { 1 } else { 0 };
2054 let list_area = Rect {
2055 x: results_area.x,
2056 y: results_area.y + 2,
2057 width: results_area.width.saturating_sub(scrollbar_w),
2058 height: results_area.height - 2,
2059 };
2060 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
2061 frame,
2062 list_area,
2063 &prompt,
2064 &theme,
2065 self.mouse_state.hover_target.as_ref(),
2066 );
2067 if self.cached_layout.suggestions_area.is_some() {
2068 self.cached_layout.suggestions_outer_area = Some(list_area);
2069 }
2070 if needs_scrollbar {
2075 use crate::view::ui::scrollbar::{
2076 render_scrollbar, ScrollbarColors, ScrollbarState,
2077 };
2078 let scrollbar_rect = Rect {
2083 x: results_area.x + results_area.width - 1,
2084 y: list_area.y + 1,
2085 width: 1,
2086 height: list_area.height.saturating_sub(2),
2087 };
2088 let state = ScrollbarState::new(
2089 prompt.suggestions.len(),
2090 inner_rows.max(1),
2091 prompt.scroll_offset,
2092 );
2093 render_scrollbar(
2094 frame,
2095 scrollbar_rect,
2096 &state,
2097 &ScrollbarColors::from_theme(&theme),
2098 );
2099 self.cached_layout.suggestions_scrollbar_rect = Some(scrollbar_rect);
2102 } else {
2103 self.cached_layout.suggestions_scrollbar_rect = None;
2104 }
2105 } else {
2106 self.cached_layout.suggestions_scrollbar_rect = None;
2107 }
2108
2109 if let Some(preview_rect) = preview_area {
2116 use ratatui::widgets::{Block, Borders, Clear};
2119 frame.render_widget(Clear, preview_rect);
2120 let block = Block::default()
2121 .borders(Borders::LEFT)
2122 .border_style(Style::default().fg(theme.popup_border_fg))
2123 .style(Style::default().bg(theme.suggestion_bg));
2124 let inner = block.inner(preview_rect);
2125 frame.render_widget(block, preview_rect);
2126
2127 if inner.height > 0 && inner.width > 0 {
2128 let bg_fade = self.background_fade;
2135 let estimated_line_length = self.config.editor.estimated_line_length;
2136 let highlight_context_bytes = self.config.editor.highlight_context_bytes;
2137 let relative_line_numbers = self.config.editor.relative_line_numbers;
2138 let use_terminal_bg = self.config.editor.use_terminal_bg;
2139 let session_mode = self.session_mode || !self.software_cursor_only;
2140 let software_cursor_only = self.software_cursor_only;
2141 let diagnostics_inline_text = self.config.editor.diagnostics_inline_text;
2142 let show_tilde = false; let highlight_current_column = self.config.editor.highlight_current_column;
2144 let screen_width = frame.area().width;
2145
2146 let ansi_ref = self.ansi_background.as_ref();
2147 let buffers = &mut self.buffers;
2148 let event_logs = &mut self.event_logs;
2149 let cell_theme_map = &mut self.cached_layout.cell_theme_map;
2150 let Some(preview_state) = self.overlay_preview_state.as_mut() else {
2151 return;
2152 };
2153 preview_state
2154 .view_state
2155 .viewport
2156 .resize(inner.width, inner.height);
2157 let buffer_id = preview_state.buffer_id;
2158
2159 if let Some(state) = buffers.get_mut(&buffer_id) {
2160 let buf_state = preview_state.view_state.active_state_mut();
2165 let cursors = buf_state.cursors.clone();
2166 let view_mode = buf_state.view_mode.clone();
2167 let compose_width = buf_state.compose_width;
2168 let compose_column_guides = buf_state.compose_column_guides.clone();
2169 let view_transform = buf_state.view_transform.clone();
2170 let rulers = buf_state.rulers.clone();
2171 let show_line_numbers = buf_state.show_line_numbers;
2172 let highlight_current_line = buf_state.highlight_current_line;
2173 let viewport_ref = &mut buf_state.viewport;
2174 let folds_ref = &mut buf_state.folds;
2175 let event_log = event_logs.get_mut(&buffer_id);
2176 let _ = crate::view::ui::SplitRenderer::render_phantom_leaf(
2177 frame,
2178 state,
2179 &cursors,
2180 viewport_ref,
2181 folds_ref,
2182 event_log,
2183 inner,
2184 &theme,
2185 ansi_ref,
2186 bg_fade,
2187 view_mode,
2188 compose_width,
2189 compose_column_guides,
2190 view_transform,
2191 estimated_line_length,
2192 highlight_context_bytes,
2193 buffer_id,
2194 relative_line_numbers,
2195 use_terminal_bg,
2196 session_mode,
2197 software_cursor_only,
2198 &rulers,
2199 show_line_numbers,
2200 highlight_current_line,
2201 diagnostics_inline_text,
2202 show_tilde,
2203 highlight_current_column,
2204 cell_theme_map,
2205 screen_width,
2206 );
2207 }
2208 }
2209 }
2210 }
2211
2212 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
2214 use ratatui::style::Style;
2215 use ratatui::text::Span;
2216 use ratatui::widgets::Paragraph;
2217
2218 match &self.mouse_state.hover_target {
2219 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
2220 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
2222 if sid == split_id && dir == direction {
2223 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
2224 match dir {
2225 SplitDirection::Horizontal => {
2226 let line_text = "─".repeat(*length as usize);
2227 let paragraph =
2228 Paragraph::new(Span::styled(line_text, hover_style));
2229 frame.render_widget(
2230 paragraph,
2231 ratatui::layout::Rect::new(*x, *y, *length, 1),
2232 );
2233 }
2234 SplitDirection::Vertical => {
2235 for offset in 0..*length {
2236 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2237 frame.render_widget(
2238 paragraph,
2239 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
2240 );
2241 }
2242 }
2243 }
2244 }
2245 }
2246 }
2247 Some(HoverTarget::ScrollbarThumb(split_id)) => {
2248 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
2250 &self.cached_layout.split_areas
2251 {
2252 if sid == split_id {
2253 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
2254 for row_offset in *thumb_start..*thumb_end {
2255 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
2256 frame.render_widget(
2257 paragraph,
2258 ratatui::layout::Rect::new(
2259 scrollbar_rect.x,
2260 scrollbar_rect.y + row_offset as u16,
2261 1,
2262 1,
2263 ),
2264 );
2265 }
2266 }
2267 }
2268 }
2269 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
2270 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2272 &self.cached_layout.split_areas
2273 {
2274 if sid == split_id {
2275 let track_hover_style =
2276 Style::default().bg(self.theme.scrollbar_track_hover_fg);
2277 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
2278 frame.render_widget(
2279 paragraph,
2280 ratatui::layout::Rect::new(
2281 scrollbar_rect.x,
2282 scrollbar_rect.y + hovered_row,
2283 1,
2284 1,
2285 ),
2286 );
2287 }
2288 }
2289 }
2290 Some(HoverTarget::FileExplorerBorder) => {
2291 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
2293 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
2294 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
2295 for row_offset in 0..explorer_area.height {
2296 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2297 frame.render_widget(
2298 paragraph,
2299 ratatui::layout::Rect::new(
2300 border_x,
2301 explorer_area.y + row_offset,
2302 1,
2303 1,
2304 ),
2305 );
2306 }
2307 }
2308 }
2309 _ => {}
2311 }
2312 }
2313
2314 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
2316 use ratatui::style::Style;
2317 use ratatui::text::{Line, Span};
2318 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2319
2320 let items = super::types::TabContextMenuItem::all();
2321 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
2326 let screen_height = frame.area().height;
2327
2328 let menu_x = if menu.position.0 + menu_width > screen_width {
2329 screen_width.saturating_sub(menu_width)
2330 } else {
2331 menu.position.0
2332 };
2333
2334 let menu_y = if menu.position.1 + menu_height > screen_height {
2335 screen_height.saturating_sub(menu_height)
2336 } else {
2337 menu.position.1
2338 };
2339
2340 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
2341
2342 frame.render_widget(Clear, area);
2344
2345 let mut lines = Vec::new();
2347 for (idx, item) in items.iter().enumerate() {
2348 let is_highlighted = idx == menu.highlighted;
2349
2350 let style = if is_highlighted {
2351 Style::default()
2352 .fg(self.theme.menu_highlight_fg)
2353 .bg(self.theme.menu_highlight_bg)
2354 } else {
2355 Style::default()
2356 .fg(self.theme.menu_dropdown_fg)
2357 .bg(self.theme.menu_dropdown_bg)
2358 };
2359
2360 let label = item.label();
2362 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
2364
2365 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
2366 }
2367
2368 let block = Block::default()
2369 .borders(Borders::ALL)
2370 .border_style(Style::default().fg(self.theme.menu_border_fg))
2371 .style(Style::default().bg(self.theme.menu_dropdown_bg));
2372
2373 let paragraph = Paragraph::new(lines).block(block);
2374 frame.render_widget(paragraph, area);
2375 }
2376
2377 fn render_file_explorer_context_menu(
2379 &self,
2380 frame: &mut Frame,
2381 menu: &super::types::FileExplorerContextMenu,
2382 ) {
2383 use ratatui::style::Style;
2384 use ratatui::text::{Line, Span};
2385 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2386
2387 let items = menu.items();
2388 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
2389 let menu_height = menu.height();
2390 let (menu_x, menu_y) = menu.clamped_position(frame.area().width, frame.area().height);
2391
2392 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
2393
2394 frame.render_widget(Clear, area);
2395
2396 let mut lines = Vec::new();
2397 for (idx, item) in items.iter().enumerate() {
2398 let is_highlighted = idx == menu.highlighted;
2399
2400 let style = if is_highlighted {
2401 Style::default()
2402 .fg(self.theme.menu_highlight_fg)
2403 .bg(self.theme.menu_highlight_bg)
2404 } else {
2405 Style::default()
2406 .fg(self.theme.menu_dropdown_fg)
2407 .bg(self.theme.menu_dropdown_bg)
2408 };
2409
2410 let label = item.label();
2411 let content_width = (menu_width as usize).saturating_sub(2);
2412 let padded_label = format!(" {:<width$}", label, width = content_width - 1);
2413
2414 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
2415 }
2416
2417 let block = Block::default()
2418 .borders(Borders::ALL)
2419 .border_style(Style::default().fg(self.theme.menu_border_fg))
2420 .style(Style::default().bg(self.theme.menu_dropdown_bg));
2421
2422 let paragraph = Paragraph::new(lines).block(block);
2423 frame.render_widget(paragraph, area);
2424 }
2425
2426 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
2428 use ratatui::style::Modifier;
2429
2430 let Some(ref drop_zone) = drag_state.drop_zone else {
2431 return;
2432 };
2433
2434 let split_id = drop_zone.split_id();
2435
2436 let split_area = self
2438 .cached_layout
2439 .split_areas
2440 .iter()
2441 .find(|(sid, _, _, _, _, _)| *sid == split_id)
2442 .map(|(_, _, content_rect, _, _, _)| *content_rect);
2443
2444 let Some(content_rect) = split_area else {
2445 return;
2446 };
2447
2448 use super::types::TabDropZone;
2450
2451 let highlight_area = match drop_zone {
2452 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
2453 content_rect
2456 }
2457 TabDropZone::SplitLeft(_) => {
2458 let width = (content_rect.width / 2).max(3);
2460 ratatui::layout::Rect::new(
2461 content_rect.x,
2462 content_rect.y,
2463 width,
2464 content_rect.height,
2465 )
2466 }
2467 TabDropZone::SplitRight(_) => {
2468 let width = (content_rect.width / 2).max(3);
2470 let x = content_rect.x + content_rect.width - width;
2471 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
2472 }
2473 TabDropZone::SplitTop(_) => {
2474 let height = (content_rect.height / 2).max(2);
2476 ratatui::layout::Rect::new(
2477 content_rect.x,
2478 content_rect.y,
2479 content_rect.width,
2480 height,
2481 )
2482 }
2483 TabDropZone::SplitBottom(_) => {
2484 let height = (content_rect.height / 2).max(2);
2486 let y = content_rect.y + content_rect.height - height;
2487 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
2488 }
2489 };
2490
2491 let buf = frame.buffer_mut();
2494 let drop_zone_bg = self.theme.tab_drop_zone_bg;
2495 let drop_zone_border = self.theme.tab_drop_zone_border;
2496
2497 for y in highlight_area.y..highlight_area.y + highlight_area.height {
2499 for x in highlight_area.x..highlight_area.x + highlight_area.width {
2500 if let Some(cell) = buf.cell_mut((x, y)) {
2501 cell.set_bg(drop_zone_bg);
2504
2505 let is_border = x == highlight_area.x
2507 || x == highlight_area.x + highlight_area.width - 1
2508 || y == highlight_area.y
2509 || y == highlight_area.y + highlight_area.height - 1;
2510
2511 if is_border {
2512 cell.set_fg(drop_zone_border);
2513 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
2514 }
2515 }
2516 }
2517 }
2518
2519 match drop_zone {
2521 TabDropZone::SplitLeft(_) => {
2522 for y in highlight_area.y..highlight_area.y + highlight_area.height {
2524 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
2525 cell.set_symbol("▌");
2526 cell.set_fg(drop_zone_border);
2527 }
2528 }
2529 }
2530 TabDropZone::SplitRight(_) => {
2531 let x = highlight_area.x + highlight_area.width - 1;
2533 for y in highlight_area.y..highlight_area.y + highlight_area.height {
2534 if let Some(cell) = buf.cell_mut((x, y)) {
2535 cell.set_symbol("▐");
2536 cell.set_fg(drop_zone_border);
2537 }
2538 }
2539 }
2540 TabDropZone::SplitTop(_) => {
2541 for x in highlight_area.x..highlight_area.x + highlight_area.width {
2543 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
2544 cell.set_symbol("▀");
2545 cell.set_fg(drop_zone_border);
2546 }
2547 }
2548 }
2549 TabDropZone::SplitBottom(_) => {
2550 let y = highlight_area.y + highlight_area.height - 1;
2552 for x in highlight_area.x..highlight_area.x + highlight_area.width {
2553 if let Some(cell) = buf.cell_mut((x, y)) {
2554 cell.set_symbol("▄");
2555 cell.set_fg(drop_zone_border);
2556 }
2557 }
2558 }
2559 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
2560 }
2562 }
2563 }
2564
2565 pub fn recompute_layout(&mut self, width: u16, height: u16) {
2570 let size = ratatui::layout::Rect::new(0, 0, width, height);
2571
2572 let active_split = self.split_manager.active_split();
2574 self.pre_sync_ensure_visible(active_split);
2575 self.sync_scroll_groups();
2576
2577 let constraints = vec![
2580 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
2581 Constraint::Min(0),
2582 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(if self.prompt_line_visible { 1 } else { 0 }), ];
2586 let main_chunks = Layout::default()
2587 .direction(Direction::Vertical)
2588 .constraints(constraints)
2589 .split(size);
2590 let main_content_area = main_chunks[1];
2591
2592 let file_explorer_should_show = self.file_explorer_visible
2594 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
2595 let editor_content_area = if file_explorer_should_show {
2596 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
2597 let horizontal_chunks = Layout::default()
2598 .direction(Direction::Horizontal)
2599 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
2600 .split(main_content_area);
2601 horizontal_chunks[1]
2602 } else {
2603 main_content_area
2604 };
2605
2606 let view_line_mappings = SplitRenderer::compute_content_layout(
2608 editor_content_area,
2609 &self.split_manager,
2610 &mut self.buffers,
2611 &mut self.split_view_states,
2612 &self.theme,
2613 false, self.config.editor.estimated_line_length,
2615 self.config.editor.highlight_context_bytes,
2616 self.config.editor.relative_line_numbers,
2617 self.config.editor.use_terminal_bg,
2618 self.session_mode || !self.software_cursor_only,
2619 self.software_cursor_only,
2620 self.tab_bar_visible,
2621 self.config.editor.show_vertical_scrollbar,
2622 self.config.editor.show_horizontal_scrollbar,
2623 self.config.editor.diagnostics_inline_text,
2624 self.config.editor.show_tilde,
2625 );
2626
2627 self.cached_layout.view_line_mappings = view_line_mappings;
2628 }
2629
2630 pub fn clear_search_history(&mut self) {
2633 if let Some(history) = self.prompt_histories.get_mut("search") {
2634 history.clear();
2635 }
2636 }
2637
2638 fn update_terminal_title(&mut self, display_name: &str) {
2646 if !self.config.editor.set_window_title {
2647 return;
2648 }
2649 let project_name = self.working_dir.file_name().and_then(|s| s.to_str());
2650 let new_title =
2651 crate::services::terminal_title::build_window_title(display_name, project_name);
2652 if self.last_window_title.as_deref() == Some(new_title.as_str()) {
2653 return;
2654 }
2655 crate::services::terminal_title::write_terminal_title(&new_title);
2656 self.last_window_title = Some(new_title);
2657 }
2658
2659 pub fn save_histories(&self) {
2662 if let Err(e) = self
2664 .authority
2665 .filesystem
2666 .create_dir_all(&self.dir_context.data_dir)
2667 {
2668 tracing::warn!("Failed to create data directory: {}", e);
2669 return;
2670 }
2671
2672 for (key, history) in &self.prompt_histories {
2674 let path = self.dir_context.prompt_history_path(key);
2675 if let Err(e) = history.save_to_file(&path) {
2676 tracing::warn!("Failed to save {} history: {}", key, e);
2677 } else {
2678 tracing::debug!("Saved {} history to {:?}", key, path);
2679 }
2680 }
2681 }
2682}