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 true,
1606 );
1607 if self.cached_layout.suggestions_area.is_some() {
1608 self.cached_layout.suggestions_outer_area = Some(suggestions_area);
1609 }
1610
1611 if is_quick_open {
1612 let hints_area = ratatui::layout::Rect {
1613 x: 0,
1614 y: prompt_area.y.saturating_sub(hints_height),
1615 width,
1616 height: hints_height,
1617 };
1618 frame.render_widget(ratatui::widgets::Clear, hints_area);
1619 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1620 }
1621 }
1622
1623 fn prepare_overlay_preview(&mut self) {
1629 use crate::input::quick_open::parse_path_line_col;
1630
1631 let (path_str, line, col) = {
1632 let Some(prompt) = self.prompt.as_ref() else {
1633 return;
1634 };
1635 let Some(idx) = prompt.selected_suggestion else {
1636 return;
1637 };
1638 let Some(s) = prompt.suggestions.get(idx) else {
1639 return;
1640 };
1641 let from_text = parse_path_line_col(&s.text);
1646 if !from_text.0.is_empty() && from_text.1.is_some() {
1647 from_text
1648 } else if let Some(v) = s.value.as_deref() {
1649 parse_path_line_col(v)
1650 } else {
1651 from_text
1652 }
1653 };
1654 if path_str.is_empty() {
1655 return;
1656 }
1657 let line = line.unwrap_or(1).saturating_sub(1);
1658 let col = col.unwrap_or(1).saturating_sub(1);
1659
1660 let path_buf = std::path::PathBuf::from(&path_str);
1662 let abs_path = if path_buf.is_absolute() {
1663 path_buf
1664 } else {
1665 self.working_dir.join(&path_buf)
1666 };
1667 let abs_path = self
1669 .authority
1670 .filesystem
1671 .canonicalize(&abs_path)
1672 .unwrap_or(abs_path);
1673
1674 let already_target = self.overlay_preview_state.as_ref().is_some_and(|st| {
1677 self.buffers
1678 .get(&st.buffer_id)
1679 .and_then(|s| s.buffer.file_path())
1680 .is_some_and(|p| p == abs_path.as_path())
1681 });
1682
1683 let buffer_id = if already_target {
1684 self.overlay_preview_state.as_ref().unwrap().buffer_id
1685 } else {
1686 let was_open = self
1690 .buffers
1691 .iter()
1692 .any(|(_, s)| s.buffer.file_path() == Some(abs_path.as_path()));
1693 let source_split = self.split_manager.active_split();
1698 let buffer_id = match self.open_file_for_preview(abs_path.as_path()) {
1703 Ok(id) => id,
1704 Err(_e) => return,
1705 };
1706 if !was_open {
1707 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
1708 meta.hidden_from_tabs = true;
1709 }
1710 let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
1716 for leaf_id in leaf_ids {
1717 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
1718 view_state.remove_buffer(buffer_id);
1719 }
1720 }
1721 if let Some(source_state) = self.split_view_states.get_mut(&source_split) {
1724 if source_state.active_buffer == buffer_id {
1725 let preview_loaded: std::collections::HashSet<BufferId> = self
1726 .overlay_preview_state
1727 .as_ref()
1728 .map(|st| st.loaded_buffers.clone())
1729 .unwrap_or_default();
1730 let fallback = source_state
1731 .open_buffers
1732 .iter()
1733 .find_map(|t| t.as_buffer())
1734 .or_else(|| {
1735 self.buffers
1736 .keys()
1737 .copied()
1738 .find(|b| *b != buffer_id && !preview_loaded.contains(b))
1739 });
1740 if let Some(fb) = fallback {
1741 source_state.switch_buffer(fb);
1742 self.split_manager.set_split_buffer(source_split, fb);
1743 }
1744 }
1745 }
1746 self.split_manager.set_active_split(source_split);
1747 }
1748 buffer_id
1749 };
1750
1751 let need_init = self.overlay_preview_state.is_none();
1755 if need_init {
1756 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1757 self.terminal_width,
1758 self.terminal_height,
1759 buffer_id,
1760 );
1761 view_state.apply_config_defaults(
1762 self.config.editor.line_numbers,
1763 self.config.editor.highlight_current_line,
1764 self.resolve_line_wrap_for_buffer(buffer_id),
1765 self.config.editor.wrap_indent,
1766 self.resolve_wrap_column_for_buffer(buffer_id),
1767 self.config.editor.rulers.clone(),
1768 );
1769 let mut loaded_buffers = std::collections::HashSet::new();
1770 if let Some(meta) = self.buffer_metadata.get(&buffer_id) {
1779 if meta.hidden_from_tabs {
1780 loaded_buffers.insert(buffer_id);
1781 }
1782 }
1783 self.overlay_preview_state = Some(crate::app::types::OverlayPreviewState {
1784 buffer_id,
1785 view_state,
1786 loaded_buffers,
1787 });
1788 } else if let Some(state) = self.overlay_preview_state.as_mut() {
1789 if state.buffer_id != buffer_id {
1790 state.view_state.switch_buffer(buffer_id);
1791 state.buffer_id = buffer_id;
1792 if let Some(meta) = self.buffer_metadata.get(&buffer_id) {
1793 if meta.hidden_from_tabs {
1794 state.loaded_buffers.insert(buffer_id);
1795 }
1796 }
1797 }
1798 }
1799
1800 let byte_offset = self
1802 .buffers
1803 .get(&buffer_id)
1804 .map(|s| {
1805 s.buffer
1806 .position_to_offset(crate::model::piece_tree::Position { line, column: col })
1807 })
1808 .unwrap_or(0);
1809 let line_start = self
1810 .buffers
1811 .get(&buffer_id)
1812 .and_then(|s| s.buffer.line_start_offset(line))
1813 .unwrap_or(byte_offset);
1814 if let Some(state) = self.overlay_preview_state.as_mut() {
1815 state.view_state.cursors.primary_mut().position = byte_offset;
1816 let h = state.view_state.viewport.height.max(1) as usize;
1817 let half = h / 2;
1818 let target_top_line = line.saturating_sub(half);
1819 let top_byte = self
1820 .buffers
1821 .get(&buffer_id)
1822 .and_then(|s| s.buffer.line_start_offset(target_top_line))
1823 .unwrap_or(line_start);
1824 state.view_state.viewport.top_byte = top_byte;
1825 }
1826 }
1827
1828 fn render_overlay_prompt(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
1845 use crate::view::popup::PopupPosition;
1846 use ratatui::layout::Rect;
1847 use ratatui::style::{Modifier, Style};
1848 use ratatui::text::{Line, Span};
1849 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1850
1851 let overlay_pos = PopupPosition::CenteredOverlay {
1854 width_pct: 80,
1855 height_pct: 80,
1856 };
1857 let overlay_rect = match overlay_pos {
1858 PopupPosition::CenteredOverlay {
1859 width_pct,
1860 height_pct,
1861 } => {
1862 let w_pct = width_pct.clamp(1, 100) as u32;
1863 let h_pct = height_pct.clamp(1, 100) as u32;
1864 let w = ((area.width as u32 * w_pct) / 100) as u16;
1865 let h = ((area.height as u32 * h_pct) / 100) as u16;
1866 let w = w.max(20).min(area.width);
1867 let h = h.max(8).min(area.height);
1868 Rect {
1869 x: (area.width.saturating_sub(w)) / 2,
1870 y: (area.height.saturating_sub(h)) / 2,
1871 width: w,
1872 height: h,
1873 }
1874 }
1875 _ => unreachable!(),
1876 };
1877
1878 let theme = self.theme.clone();
1880 let toolbar_visible = self
1896 .prompt
1897 .as_ref()
1898 .map(|p| !p.title.is_empty())
1899 .unwrap_or(false);
1900 let chrome_rows: usize = 4 + if toolbar_visible { 1 } else { 0 };
1901 let suggestions_visible_rows = (overlay_rect.height as usize).saturating_sub(chrome_rows);
1902 if let Some(prompt) = self.prompt.as_mut() {
1903 prompt.ensure_selected_visible_within(suggestions_visible_rows);
1904 }
1905 let Some(prompt) = self.prompt.as_ref() else {
1906 return;
1907 };
1908 let prompt = prompt.clone();
1909
1910 crate::view::dimming::apply_dimming_excluding(frame, frame.area(), Some(overlay_rect));
1915
1916 frame.render_widget(Clear, overlay_rect);
1922 let default_title: Vec<fresh_core::api::StyledText> = {
1923 use crate::input::keybindings::KeyContext;
1932 use fresh_core::api::{OverlayColorSpec, OverlayOptions, StyledText};
1933 let keybindings = self.keybindings.read().unwrap();
1934 let mut hints: Vec<(String, &str)> = Vec::new();
1935 if let Some(k) = keybindings
1936 .find_keybinding_for_action("cycle_live_grep_provider", KeyContext::Prompt)
1937 {
1938 hints.push((k, "switch grep provider"));
1939 }
1940 if let Some(k) = keybindings
1941 .find_keybinding_for_action("live_grep_export_quickfix", KeyContext::Prompt)
1942 {
1943 hints.push((k, "save matches"));
1944 }
1945 if hints.is_empty() {
1946 Vec::new()
1947 } else {
1948 let hint_style = Some(OverlayOptions {
1949 fg: Some(OverlayColorSpec::ThemeKey("ui.help_key_fg".into())),
1950 ..OverlayOptions::default()
1951 });
1952 let sep_style = Some(OverlayOptions {
1953 fg: Some(OverlayColorSpec::ThemeKey("ui.popup_border_fg".into())),
1954 ..OverlayOptions::default()
1955 });
1956 let mut segs: Vec<StyledText> = Vec::new();
1957 for (i, (k, verb)) in hints.into_iter().enumerate() {
1958 if i > 0 {
1959 segs.push(StyledText {
1960 text: " · ".into(),
1961 style: sep_style.clone(),
1962 });
1963 }
1964 segs.push(StyledText {
1965 text: k,
1966 style: hint_style.clone(),
1967 });
1968 segs.push(StyledText {
1969 text: format!(" {verb}"),
1970 style: None,
1971 });
1972 }
1973 segs
1974 }
1975 };
1976 let title_segs: &[fresh_core::api::StyledText] = if prompt.title.is_empty() {
1977 &default_title
1978 } else {
1979 &prompt.title
1980 };
1981 let normal_title_style = Style::default()
1982 .fg(theme.prompt_fg)
1983 .add_modifier(Modifier::BOLD);
1984 let title_spans: Vec<Span> = title_segs
1985 .iter()
1986 .map(|seg| {
1987 let style = match &seg.style {
1988 Some(opts) => Self::resolve_overlay_style(opts, &theme),
1989 None => normal_title_style,
1990 };
1991 Span::styled(seg.text.clone(), style)
1992 })
1993 .collect();
1994 let block = Block::default()
1995 .borders(Borders::ALL)
1996 .border_style(Style::default().fg(theme.popup_border_fg))
1997 .style(Style::default().bg(theme.suggestion_bg));
1998 let inner = block.inner(overlay_rect);
1999 frame.render_widget(block, overlay_rect);
2000
2001 if inner.height == 0 || inner.width == 0 {
2002 return;
2003 }
2004
2005 let preview_min_cols: u16 = 120;
2009 let show_preview = overlay_rect.width >= preview_min_cols;
2010 let (results_area, preview_area) = if show_preview {
2011 let results_w = inner.width / 2;
2012 (
2013 Rect {
2014 x: inner.x,
2015 y: inner.y,
2016 width: results_w,
2017 height: inner.height,
2018 },
2019 Some(Rect {
2020 x: inner.x + results_w,
2021 y: inner.y,
2022 width: inner.width - results_w,
2023 height: inner.height,
2024 }),
2025 )
2026 } else {
2027 (inner, None)
2028 };
2029
2030 let input_row = Rect {
2032 x: results_area.x,
2033 y: results_area.y,
2034 width: results_area.width,
2035 height: 1,
2036 };
2037 let input_style = Style::default().fg(theme.prompt_fg).bg(theme.editor_bg);
2043 let count_str = if prompt.suggestions.is_empty() {
2044 String::new()
2045 } else {
2046 format!(
2047 "{} / {}",
2048 prompt.selected_suggestion.map(|i| i + 1).unwrap_or(0),
2049 prompt.suggestions.len()
2050 )
2051 };
2052 use crate::primitives::display_width::str_width;
2053 let count_w = str_width(&count_str);
2054 let right_gap: usize = if count_w > 0 { 1 } else { 0 };
2057 let visible_input_width = (results_area.width as usize).saturating_sub(count_w + right_gap);
2058 let truncated_input: String = prompt
2059 .input
2060 .chars()
2061 .take(visible_input_width.saturating_sub(str_width(&prompt.message)))
2062 .collect();
2063 let used = str_width(&prompt.message) + str_width(&truncated_input) + count_w;
2067 let pad = (results_area.width as usize).saturating_sub(used + right_gap);
2068 let line = Line::from(vec![
2069 Span::styled(prompt.message.clone(), input_style),
2070 Span::styled(truncated_input, input_style),
2071 Span::styled(" ".repeat(pad), input_style),
2072 Span::styled(
2073 count_str,
2074 Style::default()
2075 .fg(theme.popup_border_fg)
2076 .bg(theme.editor_bg),
2077 ),
2078 ]);
2079 frame.render_widget(Paragraph::new(line).style(input_style), input_row);
2080
2081 let cursor_x = (str_width(&prompt.message)
2083 + str_width(&prompt.input[..prompt.cursor_pos.min(prompt.input.len())]))
2084 as u16;
2085 if cursor_x < input_row.width {
2086 frame.set_cursor_position((input_row.x + cursor_x, input_row.y));
2087 }
2088
2089 let toolbar_h: u16 = if toolbar_visible { 1 } else { 0 };
2096 if toolbar_visible && results_area.height >= 2 {
2097 let toolbar = Rect {
2098 x: results_area.x,
2099 y: results_area.y + 1,
2100 width: results_area.width,
2101 height: 1,
2102 };
2103 frame.render_widget(
2104 Paragraph::new(Line::from(title_spans))
2105 .style(Style::default().bg(theme.suggestion_bg)),
2106 toolbar,
2107 );
2108 }
2109
2110 if results_area.height >= 2 + toolbar_h {
2112 let sep = Rect {
2113 x: results_area.x,
2114 y: results_area.y + 1 + toolbar_h,
2115 width: results_area.width,
2116 height: 1,
2117 };
2118 let sep_style = Style::default()
2119 .fg(theme.popup_border_fg)
2120 .bg(theme.suggestion_bg);
2121 let sep_text = "─".repeat(results_area.width as usize);
2122 frame.render_widget(Paragraph::new(sep_text).style(sep_style), sep);
2123 }
2124
2125 let chrome_above_list: u16 = 2 + toolbar_h;
2133 if results_area.height > chrome_above_list {
2134 let inner_rows = (results_area.height - chrome_above_list) as usize;
2138 let needs_scrollbar = prompt.suggestions.len() > inner_rows.max(1);
2139 let scrollbar_w: u16 = if needs_scrollbar { 1 } else { 0 };
2140 let list_area = Rect {
2141 x: results_area.x,
2142 y: results_area.y + chrome_above_list,
2143 width: results_area.width.saturating_sub(scrollbar_w),
2144 height: results_area.height - chrome_above_list,
2145 };
2146 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
2147 frame,
2148 list_area,
2149 &prompt,
2150 &theme,
2151 self.mouse_state.hover_target.as_ref(),
2152 false,
2153 );
2154 if self.cached_layout.suggestions_area.is_some() {
2155 self.cached_layout.suggestions_outer_area = Some(list_area);
2156 }
2157 if needs_scrollbar {
2162 use crate::view::ui::scrollbar::{
2163 render_scrollbar, ScrollbarColors, ScrollbarState,
2164 };
2165 let scrollbar_rect = Rect {
2169 x: results_area.x + results_area.width - 1,
2170 y: list_area.y,
2171 width: 1,
2172 height: list_area.height,
2173 };
2174 let state = ScrollbarState::new(
2175 prompt.suggestions.len(),
2176 inner_rows.max(1),
2177 prompt.scroll_offset,
2178 );
2179 render_scrollbar(
2180 frame,
2181 scrollbar_rect,
2182 &state,
2183 &ScrollbarColors::from_theme(&theme),
2184 );
2185 self.cached_layout.suggestions_scrollbar_rect = Some(scrollbar_rect);
2188 } else {
2189 self.cached_layout.suggestions_scrollbar_rect = None;
2190 }
2191 } else {
2192 self.cached_layout.suggestions_scrollbar_rect = None;
2193 }
2194
2195 if let Some(preview_rect) = preview_area {
2202 use ratatui::widgets::{Block, Borders, Clear};
2205 frame.render_widget(Clear, preview_rect);
2206 let block = Block::default()
2207 .borders(Borders::LEFT)
2208 .border_style(Style::default().fg(theme.popup_border_fg))
2209 .style(Style::default().bg(theme.suggestion_bg));
2210 let inner = block.inner(preview_rect);
2211 frame.render_widget(block, preview_rect);
2212
2213 if inner.height > 0 && inner.width > 0 {
2214 let bg_fade = self.background_fade;
2221 let estimated_line_length = self.config.editor.estimated_line_length;
2222 let highlight_context_bytes = self.config.editor.highlight_context_bytes;
2223 let relative_line_numbers = self.config.editor.relative_line_numbers;
2224 let use_terminal_bg = self.config.editor.use_terminal_bg;
2225 let session_mode = self.session_mode || !self.software_cursor_only;
2226 let software_cursor_only = self.software_cursor_only;
2227 let diagnostics_inline_text = self.config.editor.diagnostics_inline_text;
2228 let show_tilde = false; let highlight_current_column = self.config.editor.highlight_current_column;
2230 let screen_width = frame.area().width;
2231
2232 let ansi_ref = self.ansi_background.as_ref();
2233 let buffers = &mut self.buffers;
2234 let event_logs = &mut self.event_logs;
2235 let cell_theme_map = &mut self.cached_layout.cell_theme_map;
2236 let Some(preview_state) = self.overlay_preview_state.as_mut() else {
2237 return;
2238 };
2239 preview_state
2240 .view_state
2241 .viewport
2242 .resize(inner.width, inner.height);
2243 let buffer_id = preview_state.buffer_id;
2244
2245 if let Some(state) = buffers.get_mut(&buffer_id) {
2246 let buf_state = preview_state.view_state.active_state_mut();
2251 let cursors = buf_state.cursors.clone();
2252 let view_mode = buf_state.view_mode.clone();
2253 let compose_width = buf_state.compose_width;
2254 let compose_column_guides = buf_state.compose_column_guides.clone();
2255 let view_transform = buf_state.view_transform.clone();
2256 let rulers = buf_state.rulers.clone();
2257 let show_line_numbers = buf_state.show_line_numbers;
2258 let highlight_current_line = buf_state.highlight_current_line;
2259 let viewport_ref = &mut buf_state.viewport;
2260 let folds_ref = &mut buf_state.folds;
2261 let event_log = event_logs.get_mut(&buffer_id);
2262 let _ = crate::view::ui::SplitRenderer::render_phantom_leaf(
2263 frame,
2264 state,
2265 &cursors,
2266 viewport_ref,
2267 folds_ref,
2268 event_log,
2269 inner,
2270 &theme,
2271 ansi_ref,
2272 bg_fade,
2273 view_mode,
2274 compose_width,
2275 compose_column_guides,
2276 view_transform,
2277 estimated_line_length,
2278 highlight_context_bytes,
2279 buffer_id,
2280 relative_line_numbers,
2281 use_terminal_bg,
2282 session_mode,
2283 software_cursor_only,
2284 &rulers,
2285 show_line_numbers,
2286 highlight_current_line,
2287 diagnostics_inline_text,
2288 show_tilde,
2289 highlight_current_column,
2290 cell_theme_map,
2291 screen_width,
2292 );
2293 }
2294 }
2295 }
2296 }
2297
2298 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
2300 use ratatui::style::Style;
2301 use ratatui::text::Span;
2302 use ratatui::widgets::Paragraph;
2303
2304 match &self.mouse_state.hover_target {
2305 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
2306 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
2308 if sid == split_id && dir == direction {
2309 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
2310 match dir {
2311 SplitDirection::Horizontal => {
2312 let line_text = "─".repeat(*length as usize);
2313 let paragraph =
2314 Paragraph::new(Span::styled(line_text, hover_style));
2315 frame.render_widget(
2316 paragraph,
2317 ratatui::layout::Rect::new(*x, *y, *length, 1),
2318 );
2319 }
2320 SplitDirection::Vertical => {
2321 for offset in 0..*length {
2322 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2323 frame.render_widget(
2324 paragraph,
2325 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
2326 );
2327 }
2328 }
2329 }
2330 }
2331 }
2332 }
2333 Some(HoverTarget::ScrollbarThumb(split_id)) => {
2334 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
2336 &self.cached_layout.split_areas
2337 {
2338 if sid == split_id {
2339 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
2340 for row_offset in *thumb_start..*thumb_end {
2341 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
2342 frame.render_widget(
2343 paragraph,
2344 ratatui::layout::Rect::new(
2345 scrollbar_rect.x,
2346 scrollbar_rect.y + row_offset as u16,
2347 1,
2348 1,
2349 ),
2350 );
2351 }
2352 }
2353 }
2354 }
2355 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
2356 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2358 &self.cached_layout.split_areas
2359 {
2360 if sid == split_id {
2361 let track_hover_style =
2362 Style::default().bg(self.theme.scrollbar_track_hover_fg);
2363 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
2364 frame.render_widget(
2365 paragraph,
2366 ratatui::layout::Rect::new(
2367 scrollbar_rect.x,
2368 scrollbar_rect.y + hovered_row,
2369 1,
2370 1,
2371 ),
2372 );
2373 }
2374 }
2375 }
2376 Some(HoverTarget::FileExplorerBorder) => {
2377 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
2379 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
2380 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
2381 for row_offset in 0..explorer_area.height {
2382 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2383 frame.render_widget(
2384 paragraph,
2385 ratatui::layout::Rect::new(
2386 border_x,
2387 explorer_area.y + row_offset,
2388 1,
2389 1,
2390 ),
2391 );
2392 }
2393 }
2394 }
2395 _ => {}
2397 }
2398 }
2399
2400 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
2402 use ratatui::style::Style;
2403 use ratatui::text::{Line, Span};
2404 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2405
2406 let items = super::types::TabContextMenuItem::all();
2407 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
2412 let screen_height = frame.area().height;
2413
2414 let menu_x = if menu.position.0 + menu_width > screen_width {
2415 screen_width.saturating_sub(menu_width)
2416 } else {
2417 menu.position.0
2418 };
2419
2420 let menu_y = if menu.position.1 + menu_height > screen_height {
2421 screen_height.saturating_sub(menu_height)
2422 } else {
2423 menu.position.1
2424 };
2425
2426 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
2427
2428 frame.render_widget(Clear, area);
2430
2431 let mut lines = Vec::new();
2433 for (idx, item) in items.iter().enumerate() {
2434 let is_highlighted = idx == menu.highlighted;
2435
2436 let style = if is_highlighted {
2437 Style::default()
2438 .fg(self.theme.menu_highlight_fg)
2439 .bg(self.theme.menu_highlight_bg)
2440 } else {
2441 Style::default()
2442 .fg(self.theme.menu_dropdown_fg)
2443 .bg(self.theme.menu_dropdown_bg)
2444 };
2445
2446 let label = item.label();
2448 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
2450
2451 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
2452 }
2453
2454 let block = Block::default()
2455 .borders(Borders::ALL)
2456 .border_style(Style::default().fg(self.theme.menu_border_fg))
2457 .style(Style::default().bg(self.theme.menu_dropdown_bg));
2458
2459 let paragraph = Paragraph::new(lines).block(block);
2460 frame.render_widget(paragraph, area);
2461 }
2462
2463 fn render_file_explorer_context_menu(
2465 &self,
2466 frame: &mut Frame,
2467 menu: &super::types::FileExplorerContextMenu,
2468 ) {
2469 use ratatui::style::Style;
2470 use ratatui::text::{Line, Span};
2471 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2472
2473 let items = menu.items();
2474 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
2475 let menu_height = menu.height();
2476 let (menu_x, menu_y) = menu.clamped_position(frame.area().width, frame.area().height);
2477
2478 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
2479
2480 frame.render_widget(Clear, area);
2481
2482 let mut lines = Vec::new();
2483 for (idx, item) in items.iter().enumerate() {
2484 let is_highlighted = idx == menu.highlighted;
2485
2486 let style = if is_highlighted {
2487 Style::default()
2488 .fg(self.theme.menu_highlight_fg)
2489 .bg(self.theme.menu_highlight_bg)
2490 } else {
2491 Style::default()
2492 .fg(self.theme.menu_dropdown_fg)
2493 .bg(self.theme.menu_dropdown_bg)
2494 };
2495
2496 let label = item.label();
2497 let content_width = (menu_width as usize).saturating_sub(2);
2498 let padded_label = format!(" {:<width$}", label, width = content_width - 1);
2499
2500 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
2501 }
2502
2503 let block = Block::default()
2504 .borders(Borders::ALL)
2505 .border_style(Style::default().fg(self.theme.menu_border_fg))
2506 .style(Style::default().bg(self.theme.menu_dropdown_bg));
2507
2508 let paragraph = Paragraph::new(lines).block(block);
2509 frame.render_widget(paragraph, area);
2510 }
2511
2512 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
2514 use ratatui::style::Modifier;
2515
2516 let Some(ref drop_zone) = drag_state.drop_zone else {
2517 return;
2518 };
2519
2520 let split_id = drop_zone.split_id();
2521
2522 let split_area = self
2524 .cached_layout
2525 .split_areas
2526 .iter()
2527 .find(|(sid, _, _, _, _, _)| *sid == split_id)
2528 .map(|(_, _, content_rect, _, _, _)| *content_rect);
2529
2530 let Some(content_rect) = split_area else {
2531 return;
2532 };
2533
2534 use super::types::TabDropZone;
2536
2537 let highlight_area = match drop_zone {
2538 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
2539 content_rect
2542 }
2543 TabDropZone::SplitLeft(_) => {
2544 let width = (content_rect.width / 2).max(3);
2546 ratatui::layout::Rect::new(
2547 content_rect.x,
2548 content_rect.y,
2549 width,
2550 content_rect.height,
2551 )
2552 }
2553 TabDropZone::SplitRight(_) => {
2554 let width = (content_rect.width / 2).max(3);
2556 let x = content_rect.x + content_rect.width - width;
2557 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
2558 }
2559 TabDropZone::SplitTop(_) => {
2560 let height = (content_rect.height / 2).max(2);
2562 ratatui::layout::Rect::new(
2563 content_rect.x,
2564 content_rect.y,
2565 content_rect.width,
2566 height,
2567 )
2568 }
2569 TabDropZone::SplitBottom(_) => {
2570 let height = (content_rect.height / 2).max(2);
2572 let y = content_rect.y + content_rect.height - height;
2573 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
2574 }
2575 };
2576
2577 let buf = frame.buffer_mut();
2580 let drop_zone_bg = self.theme.tab_drop_zone_bg;
2581 let drop_zone_border = self.theme.tab_drop_zone_border;
2582
2583 for y in highlight_area.y..highlight_area.y + highlight_area.height {
2585 for x in highlight_area.x..highlight_area.x + highlight_area.width {
2586 if let Some(cell) = buf.cell_mut((x, y)) {
2587 cell.set_bg(drop_zone_bg);
2590
2591 let is_border = x == highlight_area.x
2593 || x == highlight_area.x + highlight_area.width - 1
2594 || y == highlight_area.y
2595 || y == highlight_area.y + highlight_area.height - 1;
2596
2597 if is_border {
2598 cell.set_fg(drop_zone_border);
2599 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
2600 }
2601 }
2602 }
2603 }
2604
2605 match drop_zone {
2607 TabDropZone::SplitLeft(_) => {
2608 for y in highlight_area.y..highlight_area.y + highlight_area.height {
2610 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
2611 cell.set_symbol("▌");
2612 cell.set_fg(drop_zone_border);
2613 }
2614 }
2615 }
2616 TabDropZone::SplitRight(_) => {
2617 let x = highlight_area.x + highlight_area.width - 1;
2619 for y in highlight_area.y..highlight_area.y + highlight_area.height {
2620 if let Some(cell) = buf.cell_mut((x, y)) {
2621 cell.set_symbol("▐");
2622 cell.set_fg(drop_zone_border);
2623 }
2624 }
2625 }
2626 TabDropZone::SplitTop(_) => {
2627 for x in highlight_area.x..highlight_area.x + highlight_area.width {
2629 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
2630 cell.set_symbol("▀");
2631 cell.set_fg(drop_zone_border);
2632 }
2633 }
2634 }
2635 TabDropZone::SplitBottom(_) => {
2636 let y = highlight_area.y + highlight_area.height - 1;
2638 for x in highlight_area.x..highlight_area.x + highlight_area.width {
2639 if let Some(cell) = buf.cell_mut((x, y)) {
2640 cell.set_symbol("▄");
2641 cell.set_fg(drop_zone_border);
2642 }
2643 }
2644 }
2645 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
2646 }
2648 }
2649 }
2650
2651 pub fn recompute_layout(&mut self, width: u16, height: u16) {
2656 let size = ratatui::layout::Rect::new(0, 0, width, height);
2657
2658 let active_split = self.split_manager.active_split();
2660 self.pre_sync_ensure_visible(active_split);
2661 self.sync_scroll_groups();
2662
2663 let constraints = vec![
2666 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
2667 Constraint::Min(0),
2668 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(if self.prompt_line_visible { 1 } else { 0 }), ];
2672 let main_chunks = Layout::default()
2673 .direction(Direction::Vertical)
2674 .constraints(constraints)
2675 .split(size);
2676 let main_content_area = main_chunks[1];
2677
2678 let file_explorer_should_show = self.file_explorer_visible
2680 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
2681 let editor_content_area = if file_explorer_should_show {
2682 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
2683 let horizontal_chunks = Layout::default()
2684 .direction(Direction::Horizontal)
2685 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
2686 .split(main_content_area);
2687 horizontal_chunks[1]
2688 } else {
2689 main_content_area
2690 };
2691
2692 let view_line_mappings = SplitRenderer::compute_content_layout(
2694 editor_content_area,
2695 &self.split_manager,
2696 &mut self.buffers,
2697 &mut self.split_view_states,
2698 &self.theme,
2699 false, self.config.editor.estimated_line_length,
2701 self.config.editor.highlight_context_bytes,
2702 self.config.editor.relative_line_numbers,
2703 self.config.editor.use_terminal_bg,
2704 self.session_mode || !self.software_cursor_only,
2705 self.software_cursor_only,
2706 self.tab_bar_visible,
2707 self.config.editor.show_vertical_scrollbar,
2708 self.config.editor.show_horizontal_scrollbar,
2709 self.config.editor.diagnostics_inline_text,
2710 self.config.editor.show_tilde,
2711 );
2712
2713 self.cached_layout.view_line_mappings = view_line_mappings;
2714 }
2715
2716 pub fn clear_search_history(&mut self) {
2719 if let Some(history) = self.prompt_histories.get_mut("search") {
2720 history.clear();
2721 }
2722 }
2723
2724 fn update_terminal_title(&mut self, display_name: &str) {
2732 if !self.config.editor.set_window_title {
2733 return;
2734 }
2735 let project_name = self.working_dir.file_name().and_then(|s| s.to_str());
2736 let new_title =
2737 crate::services::terminal_title::build_window_title(display_name, project_name);
2738 if self.last_window_title.as_deref() == Some(new_title.as_str()) {
2739 return;
2740 }
2741 crate::services::terminal_title::write_terminal_title(&new_title);
2742 self.last_window_title = Some(new_title);
2743 }
2744
2745 pub fn save_histories(&self) {
2748 if let Err(e) = self
2750 .authority
2751 .filesystem
2752 .create_dir_all(&self.dir_context.data_dir)
2753 {
2754 tracing::warn!("Failed to create data directory: {}", e);
2755 return;
2756 }
2757
2758 for (key, history) in &self.prompt_histories {
2760 let path = self.dir_context.prompt_history_path(key);
2761 if let Err(e) = history.save_to_file(&path) {
2762 tracing::warn!("Failed to save {} history: {}", key, e);
2763 } else {
2764 tracing::debug!("Saved {} history to {:?}", key, path);
2765 }
2766 }
2767 }
2768
2769 fn resolve_overlay_style(
2777 opts: &fresh_core::api::OverlayOptions,
2778 theme: &crate::view::theme::Theme,
2779 ) -> ratatui::style::Style {
2780 use crate::view::theme::named_color_from_str;
2781 use fresh_core::api::OverlayColorSpec;
2782 use ratatui::style::{Color, Modifier, Style};
2783
2784 let resolve = |spec: &OverlayColorSpec| -> Option<Color> {
2785 match spec {
2786 OverlayColorSpec::Rgb(r, g, b) => Some(Color::Rgb(*r, *g, *b)),
2787 OverlayColorSpec::ThemeKey(k) => {
2788 named_color_from_str(k).or_else(|| theme.resolve_theme_key(k))
2789 }
2790 }
2791 };
2792
2793 let mut style = Style::default();
2794 if let Some(ref fg) = opts.fg {
2795 if let Some(c) = resolve(fg) {
2796 style = style.fg(c);
2797 }
2798 }
2799 if let Some(ref bg) = opts.bg {
2800 if let Some(c) = resolve(bg) {
2801 style = style.bg(c);
2802 }
2803 }
2804 let mut m = Modifier::empty();
2805 if opts.bold {
2806 m |= Modifier::BOLD;
2807 }
2808 if opts.italic {
2809 m |= Modifier::ITALIC;
2810 }
2811 if opts.underline {
2812 m |= Modifier::UNDERLINED;
2813 }
2814 if opts.strikethrough {
2815 m |= Modifier::CROSSED_OUT;
2816 }
2817 if !m.is_empty() {
2818 style = style.add_modifier(m);
2819 }
2820 style
2821 }
2822}