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.active_window_mut().animations.capture_before_all();
16
17 self.active_chrome_mut().last_frame_width = size.width;
19 self.active_chrome_mut().last_frame_height = size.height;
20
21 self.active_chrome_mut().reset_cell_theme_map();
23
24 let active_split = self
29 .windows
30 .get(&self.active_window)
31 .and_then(|w| w.buffers.splits())
32 .map(|(mgr, _)| mgr)
33 .expect("active window must have a populated split layout")
34 .active_split();
35 {
36 let _span = tracing::info_span!("pre_sync_ensure_visible").entered();
37 self.active_window_mut()
38 .pre_sync_ensure_visible(active_split);
39 }
40
41 {
44 let _span = tracing::info_span!("sync_scroll_groups").entered();
45 self.active_window_mut().sync_scroll_groups();
46 }
47
48 let mut semantic_ranges: std::collections::HashMap<BufferId, (usize, usize)> =
54 std::collections::HashMap::new();
55 {
56 let _span = tracing::info_span!("compute_semantic_ranges").entered();
57 for (split_id, view_state) in self
58 .windows
59 .get(&self.active_window)
60 .and_then(|w| w.buffers.splits())
61 .map(|(_, vs)| vs)
62 .expect("active window must have a populated split layout")
63 {
64 if let Some(buffer_id) = self
65 .windows
66 .get(&self.active_window)
67 .and_then(|w| w.buffers.splits())
68 .map(|(mgr, _)| mgr)
69 .expect("active window must have a populated split layout")
70 .get_buffer_id((*split_id).into())
71 {
72 if let Some(state) = self
73 .windows
74 .get(&self.active_window)
75 .map(|w| &w.buffers)
76 .expect("active window present")
77 .get(&buffer_id)
78 {
79 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
80 let visible_lines =
81 view_state.viewport.visible_line_count().saturating_sub(1);
82 let end_line = start_line.saturating_add(visible_lines);
83 semantic_ranges
84 .entry(buffer_id)
85 .and_modify(|(min_start, max_end)| {
86 *min_start = (*min_start).min(start_line);
87 *max_end = (*max_end).max(end_line);
88 })
89 .or_insert((start_line, end_line));
90 }
91 }
92 }
93 }
94 for (buffer_id, (start_line, end_line)) in semantic_ranges {
95 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
96 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
97 self.maybe_request_folding_ranges_debounced(buffer_id);
98 }
99
100 {
101 let _span = tracing::info_span!("prepare_for_render").entered();
102 let active_id = self.active_window;
106 let prep_targets: Vec<(BufferId, usize, u16)> = {
107 let win = self
108 .windows
109 .get(&active_id)
110 .expect("active window must exist");
111 let (mgr, vs_map) = win
112 .buffers
113 .splits()
114 .expect("active window must have a populated split layout");
115 vs_map
116 .iter()
117 .filter_map(|(split_id, vs)| {
118 mgr.get_buffer_id((*split_id).into())
119 .map(|bid| (bid, vs.viewport.top_byte, vs.viewport.height))
120 })
121 .collect()
122 };
123 let win_buffers = &mut self
124 .windows
125 .get_mut(&active_id)
126 .expect("active window must exist")
127 .buffers;
128 for (buffer_id, top_byte, height) in prep_targets {
129 if let Some(state) = win_buffers.get_mut(&buffer_id) {
130 if let Err(e) = state.prepare_for_render(top_byte, height) {
131 tracing::error!("Failed to prepare buffer for render: {}", e);
132 }
133 }
134 }
135 }
136
137 let is_search_prompt_active = self.active_window().prompt.as_ref().is_some_and(|p| {
140 matches!(
141 p.prompt_type,
142 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
143 )
144 });
145 if is_search_prompt_active {
146 if let Some(ref search_state) = self.active_window().search_state {
147 let query = search_state.query.clone();
148 self.update_search_highlights(&query);
149 }
150 }
151
152 let mut show_search_options = self.active_window().prompt.as_ref().is_some_and(|p| {
162 matches!(
163 p.prompt_type,
164 PromptType::Search
165 | PromptType::ReplaceSearch
166 | PromptType::Replace { .. }
167 | PromptType::QueryReplaceSearch
168 | PromptType::QueryReplace { .. }
169 )
170 });
171
172 let mut prompt_is_overlay = self
179 .active_window()
180 .prompt
181 .as_ref()
182 .is_some_and(|p| p.overlay);
183 let mut has_suggestions = self
184 .active_window()
185 .prompt
186 .as_ref()
187 .is_some_and(|p| !p.suggestions.is_empty())
188 && !prompt_is_overlay;
189 let mut has_file_browser = self.active_window().prompt.as_ref().is_some_and(|p| {
190 matches!(
191 p.prompt_type,
192 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
193 )
194 }) && self.active_window_mut().file_open_state.is_some();
195
196 let mut main_chunks = Layout::default()
200 .direction(Direction::Vertical)
201 .constraints(vec![
202 Constraint::Length(if self.active_window_mut().menu_bar_visible {
203 1
204 } else {
205 0
206 }), Constraint::Min(0), Constraint::Length(
209 if !self.active_window_mut().status_bar_visible
210 || has_suggestions
211 || has_file_browser
212 {
213 0
214 } else {
215 1
216 },
217 ), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(
220 if (self.active_window_mut().prompt_line_visible
226 || self.active_window().prompt.is_some())
227 && !prompt_is_overlay
228 {
229 1
230 } else {
231 0
232 },
233 ), ])
235 .split(size);
236
237 let menu_bar_area = main_chunks[0];
238 let main_content_area = main_chunks[1];
239 let status_bar_idx = 2;
240 let search_options_idx = 3;
241 let prompt_line_idx = 4;
242
243 let editor_content_area;
246 let file_explorer_should_show = self.file_explorer_visible()
247 && (self.file_explorer().is_some()
248 || self.active_window().file_explorer_sync_in_progress);
249
250 if file_explorer_should_show {
251 tracing::trace!(
253 "render: file explorer layout active (present={}, sync_in_progress={}, side={:?})",
254 self.file_explorer().is_some(),
255 self.active_window().file_explorer_sync_in_progress,
256 self.active_window().file_explorer_side
257 );
258 let explorer_cols = self
259 .active_window()
260 .file_explorer_width
261 .to_cols(main_content_area.width);
262
263 let (explorer_area, editor_area) = match self.active_window().file_explorer_side {
264 FileExplorerSide::Left => {
265 let chunks = Layout::default()
266 .direction(Direction::Horizontal)
267 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
268 .split(main_content_area);
269 (chunks[0], chunks[1])
270 }
271 FileExplorerSide::Right => {
272 let chunks = Layout::default()
273 .direction(Direction::Horizontal)
274 .constraints([Constraint::Min(0), Constraint::Length(explorer_cols)])
275 .split(main_content_area);
276 (chunks[1], chunks[0])
277 }
278 };
279
280 self.active_layout_mut().file_explorer_area = Some(explorer_area);
281 editor_content_area = editor_area;
282
283 let remote_connection = self.connection_display_string();
285
286 let active_id = self.active_window;
291 let is_focused = self.active_window().key_context == KeyContext::FileExplorer;
294 let key_context_clone = self.active_window().key_context.clone();
295 let close_button_hovered = matches!(
296 &self.active_window().mouse_state.hover_target,
297 Some(HoverTarget::FileExplorerCloseButton)
298 );
299 let __win = self
302 .windows
303 .get_mut(&active_id)
304 .expect("active window must exist");
305 let __buffers_ref: &crate::app::window::WindowBuffers = &__win.buffers;
306 if let Some(explorer) = __win.file_explorer.as_mut() {
307 let mut files_with_unsaved_changes = std::collections::HashSet::new();
309 for (buffer_id, state) in __buffers_ref {
310 if state.buffer.is_modified() {
311 if let Some(metadata) = __win.buffer_metadata.get(buffer_id) {
312 if let Some(file_path) = metadata.file_path() {
313 files_with_unsaved_changes.insert(file_path.clone());
314 }
315 }
316 }
317 }
318
319 let keybindings = self.keybindings.read().unwrap();
320 let empty: Vec<std::path::PathBuf> = Vec::new();
321 let cut_paths = __win
322 .file_explorer_clipboard
323 .as_ref()
324 .filter(|cb| cb.is_cut)
325 .map(|cb| cb.paths.as_slice())
326 .unwrap_or(empty.as_slice());
327 FileExplorerRenderer::render(
328 explorer,
329 frame,
330 explorer_area,
331 is_focused,
332 &files_with_unsaved_changes,
333 &__win.file_explorer_decoration_cache,
334 &keybindings,
335 key_context_clone,
336 &*self.theme.read().unwrap(),
337 close_button_hovered,
338 remote_connection.as_deref(),
339 cut_paths,
340 &self.config.file_explorer.tree_indicator_collapsed,
341 &self.config.file_explorer.tree_indicator_expanded,
342 );
343 }
344 } else {
347 self.active_layout_mut().file_explorer_area = None;
349 editor_content_area = main_content_area;
350 }
351
352 if self.plugin_manager.read().unwrap().is_active() {
359 let hooks_start = std::time::Instant::now();
360 let visible_buffers = self
362 .windows
363 .get(&self.active_window)
364 .and_then(|w| w.buffers.splits())
365 .map(|(mgr, _)| mgr)
366 .expect("active window must have a populated split layout")
367 .get_visible_buffers(editor_content_area);
368
369 let mut total_new_lines = 0usize;
370 for (split_id, buffer_id, split_area) in visible_buffers {
371 let viewport_top_byte = self
373 .windows
374 .get(&self.active_window)
375 .and_then(|w| w.buffers.splits())
376 .map(|(_, vs)| vs)
377 .expect("active window must have a populated split layout")
378 .get(&split_id)
379 .map(|vs| vs.viewport.top_byte)
380 .unwrap_or(0);
381
382 let __active_id = self.active_window;
383 let __win = self
384 .windows
385 .get_mut(&__active_id)
386 .expect("active window must exist");
387 let seen_ranges_for_win = &mut __win.seen_byte_ranges;
392 let plugin_manager = &self.plugin_manager;
393 let estimated_line_length = self.config.editor.estimated_line_length;
394 let added = __win
395 .buffers
396 .with_buffer_and_view_states(buffer_id, |state, vs_map| {
397 let pm_guard = plugin_manager.read().unwrap();
401 pm_guard.run_hook(
402 "render_start",
403 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
404 );
405
406 let visible_count = split_area.height as usize;
407
408 if pm_guard.has_subscribers("view_transform_request") {
414 let is_binary = state.buffer.is_binary();
415 let line_ending = state.buffer.line_ending();
416 let base_tokens =
417 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
418 &mut state.buffer,
419 viewport_top_byte,
420 estimated_line_length,
421 visible_count,
422 is_binary,
423 line_ending,
424 );
425 let viewport_start = viewport_top_byte;
426 let viewport_end = base_tokens
427 .last()
428 .and_then(|t| t.source_offset)
429 .unwrap_or(viewport_start);
430 let cursor_positions: Vec<usize> = vs_map
431 .get(&split_id)
432 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
433 .unwrap_or_default();
434 pm_guard.run_hook(
435 "view_transform_request",
436 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
437 buffer_id,
438 split_id: split_id.into(),
439 viewport_start,
440 viewport_end,
441 tokens: base_tokens,
442 cursor_positions,
443 },
444 );
445
446 if let Some(vs) = vs_map.get_mut(&split_id) {
449 vs.view_transform_stale = false;
450 }
451 }
452 drop(pm_guard);
453
454 let top_byte = viewport_top_byte;
455 let seen_byte_ranges =
456 seen_ranges_for_win.entry(buffer_id).or_default();
457
458 let mut new_lines: Vec<
459 crate::services::plugins::hooks::LineInfo,
460 > = Vec::new();
461 let mut line_number = state.buffer.get_line_number(top_byte);
462 let mut iter = state
463 .buffer
464 .line_iterator(top_byte, estimated_line_length);
465
466 for _ in 0..visible_count {
467 if let Some((line_start, line_content)) = iter.next_line() {
468 let byte_end = line_start + line_content.len();
469 let byte_range = (line_start, byte_end);
470
471 if !seen_byte_ranges.contains(&byte_range) {
472 new_lines.push(
473 crate::services::plugins::hooks::LineInfo {
474 line_number,
475 byte_start: line_start,
476 byte_end,
477 content: line_content,
478 },
479 );
480 seen_byte_ranges.insert(byte_range);
481 }
482 line_number += 1;
483 } else {
484 break;
485 }
486 }
487
488 let count = new_lines.len();
489 if !new_lines.is_empty() {
490 plugin_manager.read().unwrap().run_hook(
491 "lines_changed",
492 crate::services::plugins::hooks::HookArgs::LinesChanged {
493 buffer_id,
494 lines: new_lines,
495 },
496 );
497 }
498 count
499 })
500 .unwrap_or(0);
501 total_new_lines += added;
502 }
503 let hooks_elapsed = hooks_start.elapsed();
504 tracing::trace!(
505 new_lines = total_new_lines,
506 elapsed_ms = hooks_elapsed.as_millis(),
507 elapsed_us = hooks_elapsed.as_micros(),
508 "lines_changed hooks total"
509 );
510
511 let commands = self.plugin_manager.write().unwrap().process_commands();
523 let dispatched_any = !commands.is_empty();
524 if dispatched_any {
525 let cmd_names: Vec<String> =
526 commands.iter().map(|c| c.debug_variant_name()).collect();
527 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
528 }
529 for command in commands {
530 if let Err(e) = self.handle_plugin_command(command) {
531 tracing::error!("Error handling plugin command: {}", e);
532 }
533 }
534
535 self.flush_pending_grammars();
537
538 if dispatched_any {
566 show_search_options = self.active_window().prompt.as_ref().is_some_and(|p| {
567 matches!(
568 p.prompt_type,
569 PromptType::Search
570 | PromptType::ReplaceSearch
571 | PromptType::Replace { .. }
572 | PromptType::QueryReplaceSearch
573 | PromptType::QueryReplace { .. }
574 )
575 });
576 prompt_is_overlay = self
577 .active_window()
578 .prompt
579 .as_ref()
580 .is_some_and(|p| p.overlay);
581 has_suggestions = self
582 .active_window()
583 .prompt
584 .as_ref()
585 .is_some_and(|p| !p.suggestions.is_empty())
586 && !prompt_is_overlay;
587 has_file_browser = self.active_window().prompt.as_ref().is_some_and(|p| {
588 matches!(
589 p.prompt_type,
590 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
591 )
592 }) && self.active_window_mut().file_open_state.is_some();
593 main_chunks = Layout::default()
594 .direction(Direction::Vertical)
595 .constraints(vec![
596 Constraint::Length(if self.active_window_mut().menu_bar_visible {
597 1
598 } else {
599 0
600 }),
601 Constraint::Min(0),
602 Constraint::Length(
603 if !self.active_window_mut().status_bar_visible
604 || has_suggestions
605 || has_file_browser
606 {
607 0
608 } else {
609 1
610 },
611 ),
612 Constraint::Length(if show_search_options { 1 } else { 0 }),
613 Constraint::Length(
614 if (self.active_window_mut().prompt_line_visible
615 || self.active_window().prompt.is_some())
616 && !prompt_is_overlay
617 {
618 1
619 } else {
620 0
621 },
622 ),
623 ])
624 .split(size);
625 }
626 }
627
628 let lsp_waiting = !self.active_window().pending_completion_requests.is_empty()
630 || self
631 .active_window()
632 .pending_goto_definition_request
633 .is_some();
634
635 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
642 let hide_cursor = self.menu_state.active_menu.is_some()
643 || self.active_window_mut().key_context == KeyContext::FileExplorer
644 || self.active_window().terminal_mode
645 || settings_visible
646 || self.keybinding_editor.is_some();
647
648 let hovered_tab = match &self.active_window_mut().mouse_state.hover_target {
650 Some(HoverTarget::TabName(target, split_id)) => Some((*target, *split_id, false)),
651 Some(HoverTarget::TabCloseButton(target, split_id)) => Some((*target, *split_id, true)),
652 _ => None,
653 };
654
655 let hovered_close_split = match &self.active_window_mut().mouse_state.hover_target {
657 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
658 _ => None,
659 };
660
661 let hovered_maximize_split = match &self.active_window_mut().mouse_state.hover_target {
663 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
664 _ => None,
665 };
666
667 let is_maximized = self
668 .windows
669 .get(&self.active_window)
670 .and_then(|w| w.buffers.splits())
671 .map(|(mgr, _)| mgr)
672 .expect("active window must have a populated split layout")
673 .is_maximized();
674
675 let mut pending_hardware_cursor: Option<(u16, u16)> = None;
682
683 let _content_span = tracing::info_span!("render_content").entered();
684 let active_window_id = self.active_window;
689 let __win = self
693 .windows
694 .get_mut(&active_window_id)
695 .expect("active window must exist");
696 let __metadata_ref = &__win.buffer_metadata;
697 let __event_logs_mut = &mut __win.event_logs;
698 let __grouped_ref = &__win.grouped_subtrees;
699 let __composite_buffers_mut = &mut __win.composite_buffers;
700 let __composite_view_states_mut = &mut __win.composite_view_states;
701 let __cell_theme_map_mut = &mut __win.chrome_layout.cell_theme_map;
702 let __tab_bar_visible = __win.tab_bar_visible;
703 let (
704 split_areas,
705 tab_layouts,
706 close_split_areas,
707 maximize_split_areas,
708 view_line_mappings,
709 horizontal_scrollbar_areas,
710 grouped_separator_areas,
711 ) = __win
712 .buffers
713 .with_all_mut(|__buffers_mut, __mgr, __vs_map| {
714 SplitRenderer::render_content(
715 frame,
716 editor_content_area,
717 &*__mgr,
718 __buffers_mut,
719 __metadata_ref,
720 __event_logs_mut,
721 __composite_buffers_mut,
722 __composite_view_states_mut,
723 &*self.theme.read().unwrap(),
724 self.ansi_background.as_ref(),
725 self.background_fade,
726 lsp_waiting,
727 self.config.editor.large_file_threshold_bytes,
728 self.config.editor.line_wrap,
729 self.config.editor.estimated_line_length,
730 self.config.editor.highlight_context_bytes,
731 Some(__vs_map),
732 __grouped_ref,
733 hide_cursor,
734 hovered_tab,
735 hovered_close_split,
736 hovered_maximize_split,
737 is_maximized,
738 self.config.editor.relative_line_numbers,
739 __tab_bar_visible,
740 self.config.editor.use_terminal_bg,
741 self.session_mode || !self.software_cursor_only,
742 self.software_cursor_only,
743 self.config.editor.show_vertical_scrollbar,
744 self.config.editor.show_horizontal_scrollbar,
745 self.config.editor.diagnostics_inline_text,
746 self.config.editor.show_tilde,
747 self.config.editor.highlight_current_column,
748 __cell_theme_map_mut,
749 size.width,
750 &mut pending_hardware_cursor,
751 )
752 })
753 .expect("active window must have a populated split layout");
754
755 drop(_content_span);
756
757 self.maybe_start_cursor_jump_animation(pending_hardware_cursor, active_split);
763
764 if self.plugin_manager.read().unwrap().is_active() {
768 for (split_id, view_state) in self
769 .windows
770 .get(&self.active_window)
771 .and_then(|w| w.buffers.splits())
772 .map(|(_, vs)| vs)
773 .expect("active window must have a populated split layout")
774 {
775 let current = (
776 view_state.viewport.top_byte,
777 view_state.viewport.width,
778 view_state.viewport.height,
779 );
780 let (changed, previous) =
785 match self.active_window().previous_viewports.get(split_id) {
786 Some(previous) => (*previous != current, Some(*previous)),
787 None => (false, None), };
789 tracing::trace!(
790 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
791 split_id,
792 current,
793 previous,
794 changed
795 );
796 if changed {
797 if let Some(buffer_id) = self
798 .windows
799 .get(&self.active_window)
800 .and_then(|w| w.buffers.splits())
801 .map(|(mgr, _)| mgr)
802 .expect("active window must have a populated split layout")
803 .get_buffer_id((*split_id).into())
804 {
805 let top_line = self
807 .windows
808 .get(&self.active_window)
809 .map(|w| &w.buffers)
810 .expect("active window present")
811 .get(&buffer_id)
812 .and_then(|state| {
813 if state.buffer.line_count().is_some() {
814 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
815 } else {
816 None
817 }
818 });
819 tracing::debug!(
820 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
821 split_id,
822 buffer_id,
823 view_state.viewport.top_byte,
824 top_line
825 );
826 self.plugin_manager.read().unwrap().run_hook(
827 "viewport_changed",
828 crate::services::plugins::hooks::HookArgs::ViewportChanged {
829 split_id: (*split_id).into(),
830 buffer_id,
831 top_byte: view_state.viewport.top_byte,
832 top_line,
833 width: view_state.viewport.width,
834 height: view_state.viewport.height,
835 },
836 );
837 }
838 }
839 }
840 }
841
842 let __vp_win = self
847 .windows
848 .get_mut(&self.active_window)
849 .expect("active window present");
850 __vp_win.previous_viewports.clear();
851 let (_, __vp_vs_map) = __vp_win
852 .buffers
853 .splits()
854 .expect("active window must have a populated split layout");
855 let snapshot: Vec<(LeafId, (usize, u16, u16))> = __vp_vs_map
856 .iter()
857 .map(|(split_id, view_state)| {
858 (
859 *split_id,
860 (
861 view_state.viewport.top_byte,
862 view_state.viewport.width,
863 view_state.viewport.height,
864 ),
865 )
866 })
867 .collect();
868 for (split_id, vp) in snapshot {
869 __vp_win.previous_viewports.insert(split_id, vp);
870 }
871
872 self.active_window()
875 .render_terminal_splits(frame, &split_areas, true);
876
877 self.active_layout_mut().split_areas = split_areas;
878 self.active_layout_mut().horizontal_scrollbar_areas = horizontal_scrollbar_areas;
879 self.active_layout_mut().tab_layouts = tab_layouts;
880 self.active_layout_mut().close_split_areas = close_split_areas;
881 self.active_layout_mut().maximize_split_areas = maximize_split_areas;
882 self.active_layout_mut().view_line_mappings = view_line_mappings;
883
884 self.drain_pending_vb_animations();
889 let mut separator_areas = self
890 .split_manager_mut()
891 .get_separators_with_ids(editor_content_area);
892 separator_areas.extend(grouped_separator_areas);
897 self.active_layout_mut().separator_areas = separator_areas;
898 self.active_layout_mut().editor_content_area = Some(editor_content_area);
899
900 self.render_hover_highlights(frame);
902
903 self.active_chrome_mut().suggestions_area = None;
905 self.active_chrome_mut().suggestions_outer_area = None;
906 self.active_window_mut().file_browser_layout = None;
907
908 let display_name = self
910 .active_window()
911 .buffer_metadata
912 .get(&self.active_buffer())
913 .map(|m| m.display_name.clone())
914 .unwrap_or_else(|| "[No Name]".to_string());
915
916 self.update_terminal_title(&display_name);
920
921 let status_message = self.active_window().status_message.clone();
922 let plugin_status_message = self.active_window().plugin_status_message.clone();
923 let prompt = self.active_window().prompt.clone();
924 let current_language = self
947 .buffers()
948 .get(&self.active_buffer())
949 .map(|s| s.language.clone())
950 .unwrap_or_default();
951 let buffer_lsp_disabled_reason = self
952 .active_window()
953 .buffer_metadata
954 .get(&self.active_buffer())
955 .filter(|m| !m.lsp_enabled)
956 .and_then(|m| m.lsp_disabled_reason.as_deref());
957 let (lsp_status, lsp_indicator_state) = compose_lsp_status(
958 ¤t_language,
959 buffer_lsp_disabled_reason,
960 &self.active_window().lsp_progress,
961 &self.active_window().lsp_server_statuses,
962 &self.config.lsp,
963 &self.active_window().user_dismissed_lsp_languages,
964 );
965 let theme = self.theme.read().unwrap().clone();
966 let keybindings_cloned = self.keybindings.read().unwrap().clone(); let chord_state_cloned = self.active_window_mut().chord_state.clone(); let update_available = self.latest_version().map(|v| v.to_string());
971
972 if self.active_window_mut().status_bar_visible && !has_suggestions && !has_file_browser {
974 let (warning_level, general_warning_count) =
977 if self.config.warnings.show_status_indicator {
978 let lsp_level = {
979 use crate::services::async_bridge::LspServerStatus;
980 let mut level = WarningLevel::None;
981 for ((lang, _), status) in &self.active_window().lsp_server_statuses {
982 if lang == ¤t_language {
983 match status {
984 LspServerStatus::Error => {
985 level = WarningLevel::Error;
986 break;
987 }
988 LspServerStatus::Starting | LspServerStatus::Initializing => {
989 if level != WarningLevel::Error {
990 level = WarningLevel::Warning;
991 }
992 }
993 _ => {}
994 }
995 }
996 }
997 level
998 };
999 (
1000 lsp_level,
1001 self.active_window().warning_domains.general.count,
1002 )
1003 } else {
1004 (WarningLevel::None, 0)
1005 };
1006
1007 use crate::view::ui::status_bar::StatusBarHover;
1009 let status_bar_hover = match &self.active_window_mut().mouse_state.hover_target {
1010 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
1011 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
1012 Some(HoverTarget::StatusBarLineEndingIndicator) => {
1013 StatusBarHover::LineEndingIndicator
1014 }
1015 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
1016 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
1017 Some(HoverTarget::StatusBarRemoteIndicator) => StatusBarHover::RemoteIndicator,
1018 _ => StatusBarHover::None,
1019 };
1020
1021 let remote_connection = self.connection_display_string();
1022
1023 let session_name = self.session_name().map(|s| s.to_string());
1025
1026 let active_split = self.effective_active_split();
1027 let active_buf = self.active_buffer();
1028 let default_cursors = crate::model::cursor::Cursors::new();
1029 let is_read_only = self
1030 .active_window()
1031 .buffer_metadata
1032 .get(&active_buf)
1033 .map(|m| m.read_only)
1034 .unwrap_or(false);
1035 let is_synthetic_placeholder = self
1036 .active_window()
1037 .buffer_metadata
1038 .get(&active_buf)
1039 .map(|m| m.synthetic_placeholder)
1040 .unwrap_or(false);
1041 let dynamic_status_bar_elements = self.get_status_bar_element_values(active_buf);
1044 let __active_id = self.active_window;
1047 let __win = self
1048 .windows
1049 .get_mut(&__active_id)
1050 .expect("active window must exist");
1051 let status_bar_layout = __win
1052 .buffers
1053 .with_buffer_and_view_states(active_buf, |state, vs_map| {
1054 let cursors = vs_map
1055 .get(&active_split)
1056 .map(|v| &v.cursors)
1057 .unwrap_or(&default_cursors);
1058 let mut status_ctx = crate::view::ui::status_bar::StatusBarContext {
1059 state,
1060 cursors,
1061 status_message: &status_message,
1062 plugin_status_message: &plugin_status_message,
1063 lsp_status: &lsp_status,
1064 lsp_indicator_state,
1065 theme: &theme,
1066 display_name: &display_name,
1067 keybindings: &keybindings_cloned,
1068 chord_state: &chord_state_cloned,
1069 update_available: update_available.as_deref(),
1070 warning_level,
1071 general_warning_count,
1072 hover: status_bar_hover,
1073 remote_connection: remote_connection.as_deref(),
1074 session_name: session_name.as_deref(),
1075 read_only: is_read_only,
1076 remote_state_override: self.remote_indicator_override.as_ref(),
1077 is_synthetic_placeholder,
1078 remote_indicator_on_bar: false,
1083 dynamic_status_bar_elements: dynamic_status_bar_elements.clone(),
1084 };
1085 StatusBarRenderer::render_status_bar(
1086 frame,
1087 main_chunks[status_bar_idx],
1088 &mut status_ctx,
1089 &self.config.editor.status_bar,
1090 )
1091 })
1092 .expect("active buffer must be present");
1093
1094 let status_bar_area = main_chunks[status_bar_idx];
1096 self.active_chrome_mut().status_bar_area =
1097 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
1098 self.active_chrome_mut().status_bar_lsp_area = status_bar_layout.lsp_indicator;
1099 self.active_chrome_mut().status_bar_warning_area = status_bar_layout.warning_badge;
1100 self.active_chrome_mut().status_bar_line_ending_area =
1101 status_bar_layout.line_ending_indicator;
1102 self.active_chrome_mut().status_bar_encoding_area =
1103 status_bar_layout.encoding_indicator;
1104 self.active_chrome_mut().status_bar_language_area =
1105 status_bar_layout.language_indicator;
1106 self.active_chrome_mut().status_bar_message_area = status_bar_layout.message_area;
1107 self.active_chrome_mut().status_bar_remote_area = status_bar_layout.remote_indicator;
1108 }
1109
1110 if show_search_options {
1112 let confirm_each = self.active_window().prompt.as_ref().and_then(|p| {
1114 if matches!(
1115 p.prompt_type,
1116 PromptType::ReplaceSearch
1117 | PromptType::Replace { .. }
1118 | PromptType::QueryReplaceSearch
1119 | PromptType::QueryReplace { .. }
1120 ) {
1121 Some(self.active_window().search_confirm_each)
1122 } else {
1123 None
1124 }
1125 });
1126
1127 use crate::view::ui::status_bar::SearchOptionsHover;
1129 let search_options_hover = match &self.active_window_mut().mouse_state.hover_target {
1130 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
1131 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
1132 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
1133 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
1134 _ => SearchOptionsHover::None,
1135 };
1136
1137 let search_options_layout = StatusBarRenderer::render_search_options(
1138 frame,
1139 main_chunks[search_options_idx],
1140 self.active_window().search_case_sensitive,
1141 self.active_window().search_whole_word,
1142 self.active_window().search_use_regex,
1143 confirm_each,
1144 &theme,
1145 &keybindings_cloned,
1146 search_options_hover,
1147 );
1148 self.active_chrome_mut().search_options_layout = Some(search_options_layout);
1149 } else {
1150 self.active_chrome_mut().search_options_layout = None;
1151 }
1152
1153 if let Some(prompt) = &prompt {
1158 if !prompt.overlay {
1159 if matches!(
1161 prompt.prompt_type,
1162 crate::view::prompt::PromptType::OpenFile
1163 | crate::view::prompt::PromptType::SwitchProject
1164 ) {
1165 if let Some(file_open_state) = &self.active_window_mut().file_open_state {
1166 StatusBarRenderer::render_file_open_prompt(
1167 frame,
1168 main_chunks[prompt_line_idx],
1169 prompt,
1170 file_open_state,
1171 &theme,
1172 );
1173 } else {
1174 StatusBarRenderer::render_prompt(
1175 frame,
1176 main_chunks[prompt_line_idx],
1177 prompt,
1178 &theme,
1179 );
1180 }
1181 } else {
1182 StatusBarRenderer::render_prompt(
1183 frame,
1184 main_chunks[prompt_line_idx],
1185 prompt,
1186 &theme,
1187 );
1188 }
1189 }
1190 }
1191
1192 if self
1197 .active_window()
1198 .prompt
1199 .as_ref()
1200 .is_some_and(|p| p.overlay)
1201 {
1202 self.prepare_overlay_preview();
1203 }
1204
1205 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
1208
1209 let theme_clone = self.theme.read().unwrap().clone();
1212 let hover_target = self.active_window_mut().mouse_state.hover_target.clone();
1213
1214 self.active_chrome_mut().popup_areas.clear();
1216
1217 let popup_info: Vec<_> = {
1219 let active_split = self
1221 .windows
1222 .get(&self.active_window)
1223 .and_then(|w| w.buffers.splits())
1224 .map(|(mgr, _)| mgr)
1225 .expect("active window must have a populated split layout")
1226 .active_split();
1227 let viewport = self
1228 .windows
1229 .get(&self.active_window)
1230 .and_then(|w| w.buffers.splits())
1231 .map(|(_, vs)| vs)
1232 .expect("active window must have a populated split layout")
1233 .get(&active_split)
1234 .map(|vs| vs.viewport.clone());
1235
1236 let content_rect = self
1241 .active_layout()
1242 .split_areas
1243 .iter()
1244 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1245 .map(|(_, _, rect, _, _, _)| *rect);
1246
1247 let primary_cursor = self
1248 .windows
1249 .get(&self.active_window)
1250 .and_then(|w| w.buffers.splits())
1251 .map(|(_, vs)| vs)
1252 .expect("active window must have a populated split layout")
1253 .get(&active_split)
1254 .map(|vs| *vs.cursors.primary());
1255 let state = self.active_state_mut();
1256 if state.popups.is_visible() {
1257 let primary_cursor =
1259 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
1260
1261 let gutter_width = viewport
1263 .as_ref()
1264 .map(|vp| vp.gutter_width(&state.buffer) as u16)
1265 .unwrap_or(0);
1266
1267 let cursor_screen_pos = viewport
1268 .as_ref()
1269 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
1270 .unwrap_or((0, 0));
1271
1272 let word_start_screen_pos = {
1276 use crate::primitives::word_navigation::find_completion_word_start;
1277 let word_start =
1278 find_completion_word_start(&state.buffer, primary_cursor.position);
1279 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
1280 viewport
1281 .as_ref()
1282 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
1283 .unwrap_or((0, 0))
1284 };
1285
1286 let (base_x, base_y) = content_rect
1291 .map(|r| (r.x + gutter_width, r.y))
1292 .unwrap_or((gutter_width, 1));
1293
1294 let cursor_screen_pos =
1295 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
1296 let word_start_screen_pos = (
1297 word_start_screen_pos.0 + base_x,
1298 word_start_screen_pos.1 + base_y,
1299 );
1300
1301 state
1303 .popups
1304 .all()
1305 .iter()
1306 .enumerate()
1307 .map(|(popup_idx, popup)| {
1308 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
1310 (word_start_screen_pos.0, cursor_screen_pos.1)
1311 } else {
1312 cursor_screen_pos
1313 };
1314 let popup_area = popup.calculate_area(size, Some(popup_pos));
1315
1316 let desc_height = popup.description_height();
1319 let inner_area = if popup.bordered {
1320 ratatui::layout::Rect {
1321 x: popup_area.x + 1,
1322 y: popup_area.y + 1 + desc_height,
1323 width: popup_area.width.saturating_sub(2),
1324 height: popup_area.height.saturating_sub(2 + desc_height),
1325 }
1326 } else {
1327 ratatui::layout::Rect {
1328 x: popup_area.x,
1329 y: popup_area.y + desc_height,
1330 width: popup_area.width,
1331 height: popup_area.height.saturating_sub(desc_height),
1332 }
1333 };
1334
1335 let num_items = match &popup.content {
1336 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1337 _ => 0,
1338 };
1339
1340 let total_lines = popup.item_count();
1342 let visible_lines = inner_area.height as usize;
1343 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
1344 {
1345 Some(ratatui::layout::Rect {
1346 x: inner_area.x + inner_area.width - 1,
1347 y: inner_area.y,
1348 width: 1,
1349 height: inner_area.height,
1350 })
1351 } else {
1352 None
1353 };
1354
1355 (
1356 popup_idx,
1357 popup_area,
1358 inner_area,
1359 popup.scroll_offset,
1360 num_items,
1361 scrollbar_rect,
1362 total_lines,
1363 )
1364 })
1365 .collect()
1366 } else {
1367 Vec::new()
1368 }
1369 };
1370
1371 self.active_chrome_mut().popup_areas = popup_info.clone();
1373
1374 let state = self.active_state_mut();
1376 if state.popups.is_visible() {
1377 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
1378 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
1379 popup.render_with_hover(
1380 frame,
1381 *popup_area,
1382 &theme_clone,
1383 hover_target.as_ref(),
1384 );
1385 }
1386 }
1387 }
1388
1389 self.active_chrome_mut().global_popup_areas.clear();
1400 if let Some(popup) = self.global_popups.top() {
1401 let top_idx = self.global_popups.all().len() - 1;
1402 let popup_area = popup.calculate_area(size, None);
1403 let desc_height = popup.description_height();
1404 let inner_area = if popup.bordered {
1405 ratatui::layout::Rect {
1406 x: popup_area.x + 1,
1407 y: popup_area.y + 1 + desc_height,
1408 width: popup_area.width.saturating_sub(2),
1409 height: popup_area.height.saturating_sub(2 + desc_height),
1410 }
1411 } else {
1412 ratatui::layout::Rect {
1413 x: popup_area.x,
1414 y: popup_area.y + desc_height,
1415 width: popup_area.width,
1416 height: popup_area.height.saturating_sub(desc_height),
1417 }
1418 };
1419 let num_items = match &popup.content {
1420 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1421 _ => 0,
1422 };
1423 let scroll_offset = popup.scroll_offset;
1424 popup.render_with_hover(frame, popup_area, &theme_clone, hover_target.as_ref());
1425 self.active_chrome_mut().global_popup_areas.push((
1426 top_idx,
1427 popup_area,
1428 inner_area,
1429 scroll_offset,
1430 num_items,
1431 ));
1432 }
1433
1434 self.update_menu_context();
1437
1438 let settings_visible = self
1441 .settings_state
1442 .as_ref()
1443 .map(|s| s.visible)
1444 .unwrap_or(false);
1445 if settings_visible {
1446 crate::view::dimming::apply_dimming(frame, size);
1448 }
1449 if let Some(ref mut settings_state) = self.settings_state {
1450 if settings_state.visible {
1451 settings_state.update_focus_states();
1452 let settings_layout = crate::view::settings::render_settings(
1453 frame,
1454 size,
1455 settings_state,
1456 &*self.theme.read().unwrap(),
1457 );
1458 self.active_chrome_mut().settings_layout = Some(settings_layout);
1459 }
1460 }
1461
1462 if let Some(ref wizard) = self.calibration_wizard {
1464 crate::view::dimming::apply_dimming(frame, size);
1466 crate::view::calibration_wizard::render_calibration_wizard(
1467 frame,
1468 size,
1469 wizard,
1470 &*self.theme.read().unwrap(),
1471 );
1472 }
1473
1474 if let Some(ref mut kb_editor) = self.keybinding_editor {
1476 crate::view::dimming::apply_dimming(frame, size);
1477 crate::view::keybinding_editor::render_keybinding_editor(
1478 frame,
1479 size,
1480 kb_editor,
1481 &*self.theme.read().unwrap(),
1482 );
1483 }
1484
1485 if let Some(ref debug) = self.active_window().event_debug {
1487 crate::view::dimming::apply_dimming(frame, size);
1489 crate::view::event_debug::render_event_debug(
1490 frame,
1491 size,
1492 debug,
1493 &*self.theme.read().unwrap(),
1494 );
1495 }
1496
1497 if self.active_window_mut().menu_bar_visible {
1498 self.expanded_menus_cache.update(
1502 &self.theme_registry,
1503 &self.menus,
1504 &self.menu_state.themes_dir,
1505 );
1506 let hover_target = self.active_window().mouse_state.hover_target.clone();
1507 let menu_bar_mnemonics = self.config.editor.menu_bar_mnemonics;
1508 let expanded = self.expanded_menus_cache.get().expect("just updated");
1509 let keybindings = self.keybindings.read().unwrap();
1510 let new_menu_layout = crate::view::ui::MenuRenderer::render(
1511 frame,
1512 menu_bar_area,
1513 expanded,
1514 &self.menu_state,
1515 &keybindings,
1516 &*self.theme.read().unwrap(),
1517 hover_target.as_ref(),
1518 menu_bar_mnemonics,
1519 );
1520 drop(keybindings);
1521 self.active_chrome_mut().menu_layout = Some(new_menu_layout);
1522 } else {
1523 self.active_chrome_mut().menu_layout = None;
1524 }
1525
1526 let tab_ctx_menu = self.active_window().tab_context_menu.clone();
1528 if let Some(menu) = tab_ctx_menu {
1529 self.render_tab_context_menu(frame, &menu);
1530 }
1531
1532 let fe_ctx_menu = self.active_window().file_explorer_context_menu.clone();
1533 if let Some(menu) = fe_ctx_menu {
1534 self.render_file_explorer_context_menu(frame, &menu);
1535 }
1536
1537 self.record_non_editor_theme_regions();
1539
1540 self.render_theme_info_popup(frame);
1542
1543 let drag_state_clone = self.active_window().mouse_state.dragging_tab.clone();
1545 if let Some(ref drag_state) = drag_state_clone {
1546 if drag_state.is_dragging() {
1547 self.render_tab_drop_zone(frame, drag_state);
1548 }
1549 }
1550
1551 if self.active_window_mut().gpm_active {
1557 if let Some((col, row)) = self.active_window_mut().mouse_cursor_position {
1558 use ratatui::style::Modifier;
1559
1560 if col < size.width && row < size.height {
1562 let buf = frame.buffer_mut();
1564 if let Some(cell) = buf.cell_mut((col, row)) {
1565 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1566 }
1567 }
1568 }
1569 }
1570
1571 if self.active_window_mut().keyboard_capture && self.active_window().terminal_mode {
1574 let active_split = self
1576 .windows
1577 .get(&self.active_window)
1578 .and_then(|w| w.buffers.splits())
1579 .map(|(mgr, _)| mgr)
1580 .expect("active window must have a populated split layout")
1581 .active_split();
1582 let active_split_area = self
1583 .active_layout()
1584 .split_areas
1585 .iter()
1586 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1587 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1588
1589 if let Some(terminal_area) = active_split_area {
1590 self.apply_keyboard_capture_dimming(frame, terminal_area);
1591 }
1592 }
1593
1594 if let Some((cx, cy)) = pending_hardware_cursor {
1605 if self.active_window().prompt.is_none() && !self.cursor_obscured_by_overlay(cx, cy) {
1606 frame.set_cursor_position((cx, cy));
1607 }
1608 }
1609
1610 crate::view::color_support::convert_buffer_colors(
1612 frame.buffer_mut(),
1613 self.color_capability,
1614 );
1615
1616 self.active_window_mut()
1618 .animations
1619 .apply_all(frame.buffer_mut());
1620
1621 if self.floating_widget_panel.is_some() {
1624 let frame_area = frame.area();
1625 self.render_floating_widget_panel(frame, frame_area);
1626 }
1627 }
1628
1629 fn maybe_start_cursor_jump_animation(
1644 &mut self,
1645 current_pos: Option<(u16, u16)>,
1646 active_split: crate::model::event::LeafId,
1647 ) {
1648 if !self.config.editor.animations || !self.config.editor.cursor_jump_animation {
1656 self.previous_cursor_screen_pos = current_pos.map(|p| (p, active_split));
1657 return;
1658 }
1659
1660 let Some(current) = current_pos else {
1661 self.previous_cursor_screen_pos = None;
1665 return;
1666 };
1667
1668 let prev_entry = self.previous_cursor_screen_pos;
1669 self.previous_cursor_screen_pos = Some((current, active_split));
1671
1672 let Some((prev, prev_split)) = prev_entry else {
1673 return;
1674 };
1675 if prev == current && prev_split == active_split {
1676 return;
1677 }
1678
1679 let dx = (current.0 as i32 - prev.0 as i32).abs();
1680 let dy = (current.1 as i32 - prev.1 as i32).abs();
1681 let crossed_panes = prev_split != active_split;
1689 let row_jump = dy > 2;
1690 let col_jump = dx >= 80;
1691 if !crossed_panes && !row_jump && !col_jump {
1692 return;
1693 }
1694
1695 if let Some(prev_anim) = self.cursor_jump_animation.take() {
1697 self.active_window_mut().animations.cancel(prev_anim);
1698 }
1699
1700 let cursor_color = self.theme.read().unwrap().cursor;
1701 let bg_color = self.theme.read().unwrap().editor_bg;
1702 let id = self.active_window_mut().animations.start(
1703 ratatui::layout::Rect {
1706 x: prev.0.min(current.0),
1707 y: prev.1.min(current.1),
1708 width: dx as u16 + 1,
1709 height: dy as u16 + 1,
1710 },
1711 crate::view::animation::AnimationKind::CursorJump {
1712 from: prev,
1713 to: current,
1714 duration: std::time::Duration::from_millis(140),
1715 cursor_color,
1716 bg_color,
1717 },
1718 );
1719 self.cursor_jump_animation = Some(id);
1720 }
1721
1722 fn cursor_obscured_by_overlay(&self, x: u16, y: u16) -> bool {
1726 let inside = |rect: ratatui::layout::Rect| -> bool {
1727 x >= rect.x
1728 && x < rect.x.saturating_add(rect.width)
1729 && y >= rect.y
1730 && y < rect.y.saturating_add(rect.height)
1731 };
1732
1733 if self
1734 .active_chrome()
1735 .popup_areas
1736 .iter()
1737 .any(|entry| inside(entry.1))
1738 {
1739 return true;
1740 }
1741 if self
1742 .active_chrome()
1743 .global_popup_areas
1744 .iter()
1745 .any(|entry| inside(entry.1))
1746 {
1747 return true;
1748 }
1749 if let Some((rect, _, _, _)) = self.active_chrome().suggestions_area {
1750 if inside(rect) {
1751 return true;
1752 }
1753 }
1754 if let Some(ref fb) = self.active_window().file_browser_layout {
1755 if inside(fb.popup_area) {
1756 return true;
1757 }
1758 }
1759 false
1760 }
1761
1762 fn render_quick_open_hints(
1764 frame: &mut Frame,
1765 area: ratatui::layout::Rect,
1766 theme: &crate::view::theme::Theme,
1767 ) {
1768 use ratatui::style::{Modifier, Style};
1769 use ratatui::text::{Line, Span};
1770 use ratatui::widgets::Paragraph;
1771 use rust_i18n::t;
1772
1773 let hints_style = Style::default()
1774 .fg(theme.line_number_fg)
1775 .bg(theme.suggestion_selected_bg)
1776 .add_modifier(Modifier::DIM);
1777 let hints_text = t!("quick_open.mode_hints");
1778 let left_margin = 2;
1780 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1781 let mut spans = Vec::new();
1782 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1783 spans.push(Span::styled(hints_text.to_string(), hints_style));
1784 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1785 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1786
1787 let paragraph = Paragraph::new(Line::from(spans));
1788 frame.render_widget(paragraph, area);
1789 }
1790
1791 fn apply_keyboard_capture_dimming(
1794 &self,
1795 frame: &mut Frame,
1796 terminal_area: ratatui::layout::Rect,
1797 ) {
1798 let size = frame.area();
1799 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1800 }
1801
1802 fn render_prompt_popups(
1805 &mut self,
1806 frame: &mut Frame,
1807 prompt_area: ratatui::layout::Rect,
1808 width: u16,
1809 ) {
1810 let Some(prompt) = &self.active_window_mut().prompt else {
1811 return;
1812 };
1813
1814 if prompt.overlay {
1817 let frame_area = frame.area();
1818 self.render_overlay_prompt(frame, frame_area);
1819 return;
1820 }
1821
1822 if matches!(
1823 prompt.prompt_type,
1824 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1825 ) {
1826 let hover_target = self.active_window().mouse_state.hover_target.clone();
1827 let theme = self.theme.read().unwrap().clone();
1828 let keybindings = self.keybindings.read().unwrap();
1829 let kb_clone = keybindings.clone();
1830 drop(keybindings);
1831 let max_height = prompt_area.y.saturating_sub(1).min(20);
1832 let popup_area = ratatui::layout::Rect {
1833 x: 0,
1834 y: prompt_area.y.saturating_sub(max_height),
1835 width,
1836 height: max_height,
1837 };
1838 let __win = self.active_window_mut();
1839 let Some(file_open_state) = &mut __win.file_open_state else {
1840 return;
1841 };
1842 __win.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1843 frame,
1844 popup_area,
1845 file_open_state,
1846 &theme,
1847 &hover_target,
1848 Some(&kb_clone),
1849 );
1850 return;
1851 }
1852
1853 if prompt.suggestions.is_empty() {
1854 return;
1855 }
1856
1857 let suggestion_count = prompt.suggestions.len().min(10);
1858 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1859 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1860 let height = suggestion_count as u16 + 2 + hints_height;
1861
1862 let suggestions_area = ratatui::layout::Rect {
1863 x: 0,
1864 y: prompt_area.y.saturating_sub(height),
1865 width,
1866 height: height - hints_height,
1867 };
1868
1869 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1870
1871 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
1874 prompt.ensure_selected_visible();
1875 }
1876 let Some(prompt) = &self.active_window().prompt else {
1877 return;
1878 };
1879
1880 let new_suggestions_area = SuggestionsRenderer::render_with_hover(
1881 frame,
1882 suggestions_area,
1883 prompt,
1884 &*self.theme.read().unwrap(),
1885 self.active_window().mouse_state.hover_target.as_ref(),
1886 true,
1887 );
1888 let chrome = self.active_chrome_mut();
1889 chrome.suggestions_area = new_suggestions_area;
1890 if chrome.suggestions_area.is_some() {
1891 chrome.suggestions_outer_area = Some(suggestions_area);
1892 }
1893
1894 if is_quick_open {
1895 let hints_area = ratatui::layout::Rect {
1896 x: 0,
1897 y: prompt_area.y.saturating_sub(hints_height),
1898 width,
1899 height: hints_height,
1900 };
1901 frame.render_widget(ratatui::widgets::Clear, hints_area);
1902 Self::render_quick_open_hints(frame, hints_area, &*self.theme.read().unwrap());
1903 }
1904 }
1905
1906 fn render_session_preview_into_rect(
1927 &mut self,
1928 frame: &mut ratatui::Frame,
1929 inner: ratatui::layout::Rect,
1930 theme: &crate::view::theme::Theme,
1931 ) {
1932 let Some(sid) = self.preview_window_id else {
1933 return;
1934 };
1935
1936 let Some(__win_for_preview) = self.windows.get_mut(&sid) else {
1976 return;
1977 };
1978 let __preview_metadata = &__win_for_preview.buffer_metadata;
1979 let __preview_event_logs = &mut __win_for_preview.event_logs;
1980 let __preview_composite_buffers = &mut __win_for_preview.composite_buffers;
1981 let __preview_composite_view_states = &mut __win_for_preview.composite_view_states;
1982 let preview_tab_bar_visible = __win_for_preview.tab_bar_visible;
1983
1984 let mut scratch_cell_theme_map: Vec<crate::app::types::CellThemeInfo> = Vec::new();
1988 let mut scratch_pending_cursor: Option<(u16, u16)> = None;
1989 let lsp_waiting = false; let no_grouped_subtrees: std::collections::HashMap<
1991 crate::model::event::LeafId,
1992 crate::view::split::SplitNode,
1993 > = std::collections::HashMap::new();
1994
1995 let mut preview_split_areas: Vec<(
1996 crate::model::event::LeafId,
1997 fresh_core::BufferId,
1998 ratatui::layout::Rect,
1999 ratatui::layout::Rect,
2000 usize,
2001 usize,
2002 )> = Vec::new();
2003 __win_for_preview
2004 .buffers
2005 .with_all_mut(|preview_buffers, mgr, view_states| {
2006 let result = crate::view::ui::SplitRenderer::render_content(
2007 frame,
2008 inner,
2009 &*mgr,
2010 preview_buffers,
2011 __preview_metadata,
2012 __preview_event_logs,
2013 __preview_composite_buffers,
2014 __preview_composite_view_states,
2015 theme,
2016 self.ansi_background.as_ref(),
2017 self.background_fade,
2018 lsp_waiting,
2019 self.config.editor.large_file_threshold_bytes,
2020 self.config.editor.line_wrap,
2021 self.config.editor.estimated_line_length,
2022 self.config.editor.highlight_context_bytes,
2023 Some(view_states),
2024 &no_grouped_subtrees,
2025 true, None, None,
2028 None,
2029 false, self.config.editor.relative_line_numbers,
2031 preview_tab_bar_visible,
2032 self.config.editor.use_terminal_bg,
2033 self.session_mode || !self.software_cursor_only,
2034 self.software_cursor_only,
2035 false,
2038 false,
2039 self.config.editor.diagnostics_inline_text,
2040 false, self.config.editor.highlight_current_column,
2042 &mut scratch_cell_theme_map,
2043 inner.width,
2044 &mut scratch_pending_cursor,
2045 );
2046 preview_split_areas = result.0;
2047 });
2048
2049 if let Some(win) = self.windows.get_mut(&sid) {
2061 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _, _) in &preview_split_areas
2062 {
2063 if win.terminal_buffers.contains_key(buffer_id)
2064 && content_rect.width > 0
2065 && content_rect.height > 0
2066 {
2067 win.resize_terminal(*buffer_id, content_rect.width, content_rect.height);
2068 }
2069 }
2070 }
2071
2072 if let Some(win) = self.windows.get(&sid) {
2079 win.render_terminal_splits(frame, &preview_split_areas, false);
2080 }
2081 }
2082
2083 fn prepare_overlay_preview(&mut self) {
2084 use crate::input::quick_open::parse_path_line_col;
2085
2086 let (path_str, line, col) = {
2087 let Some(prompt) = self.active_window().prompt.as_ref() else {
2088 return;
2089 };
2090 let Some(idx) = prompt.selected_suggestion else {
2091 return;
2092 };
2093 let Some(s) = prompt.suggestions.get(idx) else {
2094 return;
2095 };
2096 let from_text = parse_path_line_col(&s.text);
2101 if !from_text.0.is_empty() && from_text.1.is_some() {
2102 from_text
2103 } else if let Some(v) = s.value.as_deref() {
2104 parse_path_line_col(v)
2105 } else {
2106 from_text
2107 }
2108 };
2109 if path_str.is_empty() {
2110 return;
2111 }
2112 let line = line.unwrap_or(1).saturating_sub(1);
2113 let col = col.unwrap_or(1).saturating_sub(1);
2114
2115 let path_buf = std::path::PathBuf::from(&path_str);
2117 let abs_path = if path_buf.is_absolute() {
2118 path_buf
2119 } else {
2120 self.working_dir.join(&path_buf)
2121 };
2122 let abs_path = self
2124 .authority
2125 .filesystem
2126 .canonicalize(&abs_path)
2127 .unwrap_or(abs_path);
2128
2129 let already_target = self
2132 .active_window()
2133 .overlay_preview_state
2134 .as_ref()
2135 .is_some_and(|st| {
2136 self.windows
2137 .get(&self.active_window)
2138 .map(|w| &w.buffers)
2139 .expect("active window present")
2140 .get(&st.buffer_id)
2141 .and_then(|s| s.buffer.file_path())
2142 .is_some_and(|p| p == abs_path.as_path())
2143 });
2144
2145 let buffer_id = if already_target {
2146 self.active_window_mut()
2147 .overlay_preview_state
2148 .as_ref()
2149 .unwrap()
2150 .buffer_id
2151 } else {
2152 let was_open = self
2156 .buffers()
2157 .iter()
2158 .any(|(_, s)| s.buffer.file_path() == Some(abs_path.as_path()));
2159 let source_split = self
2164 .windows
2165 .get(&self.active_window)
2166 .and_then(|w| w.buffers.splits())
2167 .map(|(mgr, _)| mgr)
2168 .expect("active window must have a populated split layout")
2169 .active_split();
2170 let buffer_id = match self.open_file_for_preview(abs_path.as_path()) {
2175 Ok(id) => id,
2176 Err(_e) => return,
2177 };
2178 if !was_open {
2179 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2180 meta.hidden_from_tabs = true;
2181 }
2182 let leaf_ids: Vec<_> = self
2188 .windows
2189 .get(&self.active_window)
2190 .and_then(|w| w.buffers.splits())
2191 .map(|(_, vs)| vs)
2192 .expect("active window must have a populated split layout")
2193 .keys()
2194 .copied()
2195 .collect();
2196 for leaf_id in leaf_ids {
2197 if let Some(view_state) = self
2198 .windows
2199 .get_mut(&self.active_window)
2200 .and_then(|w| w.split_view_states_mut())
2201 .expect("active window must have a populated split layout")
2202 .get_mut(&leaf_id)
2203 {
2204 view_state.remove_buffer(buffer_id);
2205 }
2206 }
2207 let preview_loaded: std::collections::HashSet<BufferId> = self
2210 .active_window_mut()
2211 .overlay_preview_state
2212 .as_ref()
2213 .map(|st| st.loaded_buffers.clone())
2214 .unwrap_or_default();
2215 let __active_id = self.active_window;
2216 let __win = self
2217 .windows
2218 .get_mut(&__active_id)
2219 .expect("active window must exist");
2220 let __buffer_keys: Vec<BufferId> = __win.buffers.ids();
2221 let (__mgr, __vs_map) = __win
2222 .buffers
2223 .splits_mut()
2224 .expect("active window must have a populated split layout");
2225 if let Some(source_state) = __vs_map.get_mut(&source_split) {
2226 if source_state.active_buffer == buffer_id {
2227 let fallback = source_state
2228 .open_buffers
2229 .iter()
2230 .find_map(|t| t.as_buffer())
2231 .or_else(|| {
2232 __buffer_keys
2233 .iter()
2234 .copied()
2235 .find(|b| *b != buffer_id && !preview_loaded.contains(b))
2236 });
2237 if let Some(fb) = fallback {
2238 source_state.switch_buffer(fb);
2239 __mgr.set_split_buffer(source_split, fb);
2240 }
2241 }
2242 }
2243 self.windows
2244 .get_mut(&self.active_window)
2245 .and_then(|w| w.split_manager_mut())
2246 .expect("active window must have a populated split layout")
2247 .set_active_split(source_split);
2248 }
2249 buffer_id
2250 };
2251
2252 let need_init = self.active_window_mut().overlay_preview_state.is_none();
2256 if need_init {
2257 let mut view_state = crate::view::split::SplitViewState::with_buffer(
2258 self.terminal_width,
2259 self.terminal_height,
2260 buffer_id,
2261 );
2262 view_state.apply_config_defaults(
2263 self.config.editor.line_numbers,
2264 self.config.editor.highlight_current_line,
2265 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
2266 self.config.editor.wrap_indent,
2267 self.active_window()
2268 .resolve_wrap_column_for_buffer(buffer_id),
2269 self.config.editor.rulers.clone(),
2270 );
2271 let mut loaded_buffers = std::collections::HashSet::new();
2272 if let Some(meta) = self.active_window().buffer_metadata.get(&buffer_id) {
2281 if meta.hidden_from_tabs {
2282 loaded_buffers.insert(buffer_id);
2283 }
2284 }
2285 self.active_window_mut().overlay_preview_state =
2286 Some(crate::app::types::OverlayPreviewState {
2287 buffer_id,
2288 view_state,
2289 loaded_buffers,
2290 });
2291 } else {
2292 let hidden_from_tabs = self
2295 .windows
2296 .get(&self.active_window)
2297 .and_then(|w| w.buffer_metadata.get(&buffer_id))
2298 .is_some_and(|meta| meta.hidden_from_tabs);
2299 if let Some(state) = self.active_window_mut().overlay_preview_state.as_mut() {
2300 if state.buffer_id != buffer_id {
2301 state.view_state.switch_buffer(buffer_id);
2302 if hidden_from_tabs {
2303 state.loaded_buffers.insert(buffer_id);
2304 }
2305 }
2306 }
2307 }
2308
2309 let byte_offset = self
2311 .buffers()
2312 .get(&buffer_id)
2313 .map(|s| {
2314 s.buffer
2315 .position_to_offset(crate::model::piece_tree::Position { line, column: col })
2316 })
2317 .unwrap_or(0);
2318 let line_start = self
2319 .buffers()
2320 .get(&buffer_id)
2321 .and_then(|s| s.buffer.line_start_offset(line))
2322 .unwrap_or(byte_offset);
2323 let h_for_preview = self
2326 .active_window_mut()
2327 .overlay_preview_state
2328 .as_ref()
2329 .map(|s| s.view_state.viewport.height.max(1) as usize)
2330 .unwrap_or(1);
2331 let half = h_for_preview / 2;
2332 let target_top_line = line.saturating_sub(half);
2333 let top_byte = self
2334 .windows
2335 .get(&self.active_window)
2336 .map(|w| &w.buffers)
2337 .expect("active window present")
2338 .get(&buffer_id)
2339 .and_then(|s| s.buffer.line_start_offset(target_top_line))
2340 .unwrap_or(line_start);
2341 if let Some(state) = self.active_window_mut().overlay_preview_state.as_mut() {
2342 state.view_state.cursors.primary_mut().position = byte_offset;
2343 state.view_state.viewport.top_byte = top_byte;
2344 }
2345 }
2346
2347 fn render_overlay_prompt(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
2364 use ratatui::layout::Rect;
2365 use ratatui::style::{Modifier, Style};
2366 use ratatui::text::{Line, Span};
2367 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2368
2369 let overlay_rect = Self::centered_overlay_rect(area, 80, 80);
2372
2373 let theme = self.theme.read().unwrap().clone();
2375 let toolbar_visible = self
2391 .active_window()
2392 .prompt
2393 .as_ref()
2394 .map(|p| !p.title.is_empty())
2395 .unwrap_or(false);
2396 let chrome_rows: usize = 4 + if toolbar_visible { 1 } else { 0 };
2397 let suggestions_visible_rows = (overlay_rect.height as usize).saturating_sub(chrome_rows);
2398 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2399 prompt.ensure_selected_visible_within(suggestions_visible_rows);
2400 }
2401 let Some(prompt) = self.active_window().prompt.as_ref() else {
2402 return;
2403 };
2404 let prompt = prompt.clone();
2405
2406 crate::view::dimming::apply_dimming_excluding(frame, frame.area(), Some(overlay_rect));
2411
2412 frame.render_widget(Clear, overlay_rect);
2418 let default_title: Vec<fresh_core::api::StyledText> = {
2419 use crate::input::keybindings::KeyContext;
2428 use fresh_core::api::{OverlayColorSpec, OverlayOptions, StyledText};
2429 let keybindings = self.keybindings.read().unwrap();
2430 let mut hints: Vec<(String, &str)> = Vec::new();
2431 if let Some(k) = keybindings
2432 .find_keybinding_for_action("cycle_live_grep_provider", KeyContext::Prompt)
2433 {
2434 hints.push((k, "switch grep provider"));
2435 }
2436 if let Some(k) = keybindings
2437 .find_keybinding_for_action("live_grep_export_quickfix", KeyContext::Prompt)
2438 {
2439 hints.push((k, "save matches"));
2440 }
2441 if hints.is_empty() {
2442 Vec::new()
2443 } else {
2444 let hint_style = Some(OverlayOptions {
2445 fg: Some(OverlayColorSpec::ThemeKey("ui.help_key_fg".into())),
2446 ..OverlayOptions::default()
2447 });
2448 let sep_style = Some(OverlayOptions {
2449 fg: Some(OverlayColorSpec::ThemeKey("ui.popup_border_fg".into())),
2450 ..OverlayOptions::default()
2451 });
2452 let mut segs: Vec<StyledText> = Vec::new();
2453 for (i, (k, verb)) in hints.into_iter().enumerate() {
2454 if i > 0 {
2455 segs.push(StyledText {
2456 text: " · ".into(),
2457 style: sep_style.clone(),
2458 });
2459 }
2460 segs.push(StyledText {
2461 text: k,
2462 style: hint_style.clone(),
2463 });
2464 segs.push(StyledText {
2465 text: format!(" {verb}"),
2466 style: None,
2467 });
2468 }
2469 segs
2470 }
2471 };
2472 let title_segs: &[fresh_core::api::StyledText] = if prompt.title.is_empty() {
2473 &default_title
2474 } else {
2475 &prompt.title
2476 };
2477 let normal_title_style = Style::default()
2478 .fg(theme.prompt_fg)
2479 .add_modifier(Modifier::BOLD);
2480 let title_spans: Vec<Span> = title_segs
2481 .iter()
2482 .map(|seg| {
2483 let style = match &seg.style {
2484 Some(opts) => Self::resolve_overlay_style(opts, &theme),
2485 None => normal_title_style,
2486 };
2487 Span::styled(seg.text.clone(), style)
2488 })
2489 .collect();
2490 let block = Block::default()
2491 .borders(Borders::ALL)
2492 .border_style(Style::default().fg(theme.popup_border_fg))
2493 .style(Style::default().bg(theme.suggestion_bg));
2494 let inner = block.inner(overlay_rect);
2495 frame.render_widget(block, overlay_rect);
2496
2497 if inner.height == 0 || inner.width == 0 {
2498 return;
2499 }
2500
2501 let preview_min_cols: u16 = 120;
2505 let show_preview = overlay_rect.width >= preview_min_cols;
2506 let (results_area, preview_area) = if show_preview {
2507 let results_w = inner.width / 2;
2508 (
2509 Rect {
2510 x: inner.x,
2511 y: inner.y,
2512 width: results_w,
2513 height: inner.height,
2514 },
2515 Some(Rect {
2516 x: inner.x + results_w,
2517 y: inner.y,
2518 width: inner.width - results_w,
2519 height: inner.height,
2520 }),
2521 )
2522 } else {
2523 (inner, None)
2524 };
2525
2526 let input_row = Rect {
2528 x: results_area.x,
2529 y: results_area.y,
2530 width: results_area.width,
2531 height: 1,
2532 };
2533 let title_style = Style::default().fg(theme.prompt_fg).bg(theme.suggestion_bg);
2540 let input_style = Style::default().fg(theme.prompt_fg).bg(theme.editor_bg);
2541 let count_str = if prompt.suggestions.is_empty() {
2542 String::new()
2543 } else {
2544 format!(
2545 "{} / {}",
2546 prompt.selected_suggestion.map(|i| i + 1).unwrap_or(0),
2547 prompt.suggestions.len()
2548 )
2549 };
2550 use crate::primitives::display_width::str_width;
2551 let count_w = str_width(&count_str);
2552 let right_gap: usize = if count_w > 0 { 1 } else { 0 };
2555 let visible_input_width = (results_area.width as usize).saturating_sub(count_w + right_gap);
2556 let truncated_input: String = prompt
2557 .input
2558 .chars()
2559 .take(visible_input_width.saturating_sub(str_width(&prompt.message)))
2560 .collect();
2561 let used = str_width(&prompt.message) + str_width(&truncated_input) + count_w;
2565 let pad = (results_area.width as usize).saturating_sub(used + right_gap);
2566 let line = Line::from(vec![
2567 Span::styled(prompt.message.clone(), title_style),
2568 Span::styled(truncated_input, input_style),
2569 Span::styled(" ".repeat(pad), input_style),
2570 Span::styled(
2571 count_str,
2572 Style::default()
2573 .fg(theme.popup_border_fg)
2574 .bg(theme.editor_bg),
2575 ),
2576 ]);
2577 frame.render_widget(Paragraph::new(line).style(input_style), input_row);
2578
2579 let cursor_x = (str_width(&prompt.message)
2581 + str_width(&prompt.input[..prompt.cursor_pos.min(prompt.input.len())]))
2582 as u16;
2583 if cursor_x < input_row.width {
2584 frame.set_cursor_position((input_row.x + cursor_x, input_row.y));
2585 }
2586
2587 let toolbar_h: u16 = if toolbar_visible { 1 } else { 0 };
2594 if toolbar_visible && results_area.height >= 2 {
2595 let toolbar = Rect {
2596 x: results_area.x,
2597 y: results_area.y + 1,
2598 width: results_area.width,
2599 height: 1,
2600 };
2601 frame.render_widget(
2602 Paragraph::new(Line::from(title_spans))
2603 .style(Style::default().bg(theme.suggestion_bg)),
2604 toolbar,
2605 );
2606 }
2607
2608 if results_area.height >= 2 + toolbar_h {
2610 let sep = Rect {
2611 x: results_area.x,
2612 y: results_area.y + 1 + toolbar_h,
2613 width: results_area.width,
2614 height: 1,
2615 };
2616 let sep_style = Style::default()
2617 .fg(theme.popup_border_fg)
2618 .bg(theme.suggestion_bg);
2619 let sep_text = "─".repeat(results_area.width as usize);
2620 frame.render_widget(Paragraph::new(sep_text).style(sep_style), sep);
2621 }
2622
2623 let chrome_above_list: u16 = 2 + toolbar_h;
2631 let footer_h: u16 = if prompt.footer.is_empty() { 0 } else { 1 };
2637 if results_area.height > chrome_above_list + footer_h {
2638 let inner_rows = (results_area.height - chrome_above_list - footer_h) as usize;
2642 let needs_scrollbar = prompt.suggestions.len() > inner_rows.max(1);
2643 let scrollbar_w: u16 = if needs_scrollbar { 1 } else { 0 };
2644 let list_area = Rect {
2645 x: results_area.x,
2646 y: results_area.y + chrome_above_list,
2647 width: results_area.width.saturating_sub(scrollbar_w),
2648 height: results_area.height - chrome_above_list - footer_h,
2649 };
2650 self.active_chrome_mut().suggestions_area = SuggestionsRenderer::render_with_hover(
2651 frame,
2652 list_area,
2653 &prompt,
2654 &theme,
2655 self.active_window_mut().mouse_state.hover_target.as_ref(),
2656 false,
2657 );
2658 if self.active_chrome_mut().suggestions_area.is_some() {
2659 self.active_chrome_mut().suggestions_outer_area = Some(list_area);
2660 }
2661 if needs_scrollbar {
2666 use crate::view::ui::scrollbar::{
2667 render_scrollbar, ScrollbarColors, ScrollbarState,
2668 };
2669 let scrollbar_rect = Rect {
2673 x: results_area.x + results_area.width - 1,
2674 y: list_area.y,
2675 width: 1,
2676 height: list_area.height,
2677 };
2678 let state = ScrollbarState::new(
2679 prompt.suggestions.len(),
2680 inner_rows.max(1),
2681 prompt.scroll_offset,
2682 );
2683 render_scrollbar(
2684 frame,
2685 scrollbar_rect,
2686 &state,
2687 &ScrollbarColors::from_theme(&theme),
2688 );
2689 self.active_chrome_mut().suggestions_scrollbar_rect = Some(scrollbar_rect);
2692 } else {
2693 self.active_chrome_mut().suggestions_scrollbar_rect = None;
2694 }
2695 } else {
2696 self.active_chrome_mut().suggestions_scrollbar_rect = None;
2697 }
2698
2699 if footer_h == 1 && results_area.height >= 1 {
2705 let footer_row = Rect {
2706 x: results_area.x,
2707 y: results_area.y + results_area.height - 1,
2708 width: results_area.width,
2709 height: 1,
2710 };
2711 let footer_default_style = Style::default().fg(theme.prompt_fg).bg(theme.suggestion_bg);
2712 let footer_spans: Vec<Span> = prompt
2713 .footer
2714 .iter()
2715 .map(|seg| {
2716 let style = match &seg.style {
2717 Some(opts) => Self::resolve_overlay_style(opts, &theme),
2718 None => footer_default_style,
2719 };
2720 Span::styled(seg.text.clone(), style)
2721 })
2722 .collect();
2723 frame.render_widget(
2724 Paragraph::new(Line::from(footer_spans))
2725 .style(Style::default().bg(theme.suggestion_bg)),
2726 footer_row,
2727 );
2728 }
2729
2730 if let Some(preview_rect) = preview_area {
2737 use ratatui::widgets::{Block, Borders, Clear};
2740 frame.render_widget(Clear, preview_rect);
2741 let block = Block::default()
2742 .borders(Borders::LEFT)
2743 .border_style(Style::default().fg(theme.popup_border_fg))
2744 .style(Style::default().bg(theme.suggestion_bg));
2745 let inner = block.inner(preview_rect);
2746 frame.render_widget(block, preview_rect);
2747
2748 if inner.height > 0
2755 && inner.width > 0
2756 && self
2757 .preview_window_id
2758 .is_some_and(|sid| sid != self.active_window && self.windows.contains_key(&sid))
2759 {
2760 self.render_session_preview_into_rect(frame, inner, &theme);
2761 } else if inner.height > 0 && inner.width > 0 {
2762 let bg_fade = self.background_fade;
2769 let estimated_line_length = self.config.editor.estimated_line_length;
2770 let highlight_context_bytes = self.config.editor.highlight_context_bytes;
2771 let relative_line_numbers = self.config.editor.relative_line_numbers;
2772 let use_terminal_bg = self.config.editor.use_terminal_bg;
2773 let session_mode = self.session_mode || !self.software_cursor_only;
2774 let software_cursor_only = self.software_cursor_only;
2775 let diagnostics_inline_text = self.config.editor.diagnostics_inline_text;
2776 let show_tilde = false; let highlight_current_column = self.config.editor.highlight_current_column;
2778 let screen_width = frame.area().width;
2779
2780 let ansi_ref = self.ansi_background.as_ref();
2781 let __win = self
2782 .windows
2783 .get_mut(&self.active_window)
2784 .expect("active window present");
2785 let buffers = &mut __win.buffers;
2786 let event_logs = &mut __win.event_logs;
2787 let cell_theme_map = &mut __win.chrome_layout.cell_theme_map;
2788 let Some(preview_state) = __win.overlay_preview_state.as_mut() else {
2789 return;
2790 };
2791 preview_state
2792 .view_state
2793 .viewport
2794 .resize(inner.width, inner.height);
2795 let buffer_id = preview_state.buffer_id;
2796
2797 if let Some(state) = buffers.get_mut(&buffer_id) {
2798 let buf_state = preview_state.view_state.active_state_mut();
2803 let cursors = buf_state.cursors.clone();
2804 let view_mode = buf_state.view_mode.clone();
2805 let compose_width = buf_state.compose_width;
2806 let compose_column_guides = buf_state.compose_column_guides.clone();
2807 let view_transform = buf_state.view_transform.clone();
2808 let rulers = buf_state.rulers.clone();
2809 let show_line_numbers = buf_state.show_line_numbers;
2810 let highlight_current_line = buf_state.highlight_current_line;
2811 let viewport_ref = &mut buf_state.viewport;
2812 let folds_ref = &mut buf_state.folds;
2813 let event_log = event_logs.get_mut(&buffer_id);
2814 let _ = crate::view::ui::SplitRenderer::render_phantom_leaf(
2815 frame,
2816 state,
2817 &cursors,
2818 viewport_ref,
2819 folds_ref,
2820 event_log,
2821 inner,
2822 &theme,
2823 ansi_ref,
2824 bg_fade,
2825 view_mode,
2826 compose_width,
2827 compose_column_guides,
2828 view_transform,
2829 estimated_line_length,
2830 highlight_context_bytes,
2831 buffer_id,
2832 relative_line_numbers,
2833 use_terminal_bg,
2834 session_mode,
2835 software_cursor_only,
2836 &rulers,
2837 show_line_numbers,
2838 highlight_current_line,
2839 diagnostics_inline_text,
2840 show_tilde,
2841 highlight_current_column,
2842 cell_theme_map,
2843 screen_width,
2844 );
2845 }
2846 }
2847 }
2848 }
2849
2850 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
2852 use ratatui::style::Style;
2853 use ratatui::text::Span;
2854 use ratatui::widgets::Paragraph;
2855
2856 match &self.active_window().mouse_state.hover_target {
2857 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
2858 for (sid, dir, x, y, length) in &self.active_layout().separator_areas {
2860 if sid == split_id && dir == direction {
2861 let (hover_fg, editor_bg) = {
2862 let theme = self.theme.read().unwrap();
2863 (theme.split_separator_hover_fg, theme.editor_bg)
2864 };
2865 let hover_style = Style::default().fg(hover_fg).bg(editor_bg);
2866 match dir {
2867 SplitDirection::Horizontal => {
2868 let line_text = "─".repeat(*length as usize);
2869 let paragraph =
2870 Paragraph::new(Span::styled(line_text, hover_style));
2871 frame.render_widget(
2872 paragraph,
2873 ratatui::layout::Rect::new(*x, *y, *length, 1),
2874 );
2875 }
2876 SplitDirection::Vertical => {
2877 for offset in 0..*length {
2878 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2879 frame.render_widget(
2880 paragraph,
2881 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
2882 );
2883 }
2884 }
2885 }
2886 }
2887 }
2888 }
2889 Some(HoverTarget::ScrollbarThumb(split_id)) => {
2890 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
2892 &self.active_layout().split_areas
2893 {
2894 if sid == split_id {
2895 let hover_style = Style::default().bg(self
2896 .theme
2897 .read()
2898 .unwrap()
2899 .scrollbar_thumb_hover_fg);
2900 for row_offset in *thumb_start..*thumb_end {
2901 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
2902 frame.render_widget(
2903 paragraph,
2904 ratatui::layout::Rect::new(
2905 scrollbar_rect.x,
2906 scrollbar_rect.y + row_offset as u16,
2907 1,
2908 1,
2909 ),
2910 );
2911 }
2912 }
2913 }
2914 }
2915 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
2916 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2918 &self.active_layout().split_areas
2919 {
2920 if sid == split_id {
2921 let track_hover_style = Style::default().bg(self
2922 .theme
2923 .read()
2924 .unwrap()
2925 .scrollbar_track_hover_fg);
2926 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
2927 frame.render_widget(
2928 paragraph,
2929 ratatui::layout::Rect::new(
2930 scrollbar_rect.x,
2931 scrollbar_rect.y + hovered_row,
2932 1,
2933 1,
2934 ),
2935 );
2936 }
2937 }
2938 }
2939 Some(HoverTarget::FileExplorerBorder) => {
2940 if let Some(explorer_area) = self.active_layout().file_explorer_area {
2942 let (hover_fg, editor_bg) = {
2943 let theme = self.theme.read().unwrap();
2944 (theme.split_separator_hover_fg, theme.editor_bg)
2945 };
2946 let hover_style = Style::default().fg(hover_fg).bg(editor_bg);
2947 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
2948 for row_offset in 0..explorer_area.height {
2949 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2950 frame.render_widget(
2951 paragraph,
2952 ratatui::layout::Rect::new(
2953 border_x,
2954 explorer_area.y + row_offset,
2955 1,
2956 1,
2957 ),
2958 );
2959 }
2960 }
2961 }
2962 _ => {}
2964 }
2965 }
2966
2967 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
2969 use ratatui::style::Style;
2970 use ratatui::text::{Line, Span};
2971 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2972
2973 let items = super::types::TabContextMenuItem::all();
2974 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
2979 let screen_height = frame.area().height;
2980
2981 let menu_x = if menu.position.0 + menu_width > screen_width {
2982 screen_width.saturating_sub(menu_width)
2983 } else {
2984 menu.position.0
2985 };
2986
2987 let menu_y = if menu.position.1 + menu_height > screen_height {
2988 screen_height.saturating_sub(menu_height)
2989 } else {
2990 menu.position.1
2991 };
2992
2993 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
2994
2995 frame.render_widget(Clear, area);
2997
2998 let mut lines = Vec::new();
3000 for (idx, item) in items.iter().enumerate() {
3001 let is_highlighted = idx == menu.highlighted;
3002
3003 let style = if is_highlighted {
3004 Style::default()
3005 .fg(self.theme.read().unwrap().menu_highlight_fg)
3006 .bg(self.theme.read().unwrap().menu_highlight_bg)
3007 } else {
3008 Style::default()
3009 .fg(self.theme.read().unwrap().menu_dropdown_fg)
3010 .bg(self.theme.read().unwrap().menu_dropdown_bg)
3011 };
3012
3013 let label = item.label();
3015 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
3017
3018 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
3019 }
3020
3021 let block = Block::default()
3022 .borders(Borders::ALL)
3023 .border_style(Style::default().fg(self.theme.read().unwrap().menu_border_fg))
3024 .style(Style::default().bg(self.theme.read().unwrap().menu_dropdown_bg));
3025
3026 let paragraph = Paragraph::new(lines).block(block);
3027 frame.render_widget(paragraph, area);
3028 }
3029
3030 fn render_file_explorer_context_menu(
3032 &self,
3033 frame: &mut Frame,
3034 menu: &super::types::FileExplorerContextMenu,
3035 ) {
3036 use ratatui::style::Style;
3037 use ratatui::text::{Line, Span};
3038 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
3039
3040 let items = menu.items();
3041 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3042 let menu_height = menu.height();
3043 let (menu_x, menu_y) = menu.clamped_position(frame.area().width, frame.area().height);
3044
3045 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
3046
3047 frame.render_widget(Clear, area);
3048
3049 let mut lines = Vec::new();
3050 for (idx, item) in items.iter().enumerate() {
3051 let is_highlighted = idx == menu.highlighted;
3052
3053 let style = if is_highlighted {
3054 Style::default()
3055 .fg(self.theme.read().unwrap().menu_highlight_fg)
3056 .bg(self.theme.read().unwrap().menu_highlight_bg)
3057 } else {
3058 Style::default()
3059 .fg(self.theme.read().unwrap().menu_dropdown_fg)
3060 .bg(self.theme.read().unwrap().menu_dropdown_bg)
3061 };
3062
3063 let label = item.label();
3064 let content_width = (menu_width as usize).saturating_sub(2);
3065 let padded_label = format!(" {:<width$}", label, width = content_width - 1);
3066
3067 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
3068 }
3069
3070 let block = Block::default()
3071 .borders(Borders::ALL)
3072 .border_style(Style::default().fg(self.theme.read().unwrap().menu_border_fg))
3073 .style(Style::default().bg(self.theme.read().unwrap().menu_dropdown_bg));
3074
3075 let paragraph = Paragraph::new(lines).block(block);
3076 frame.render_widget(paragraph, area);
3077 }
3078
3079 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
3081 use ratatui::style::Modifier;
3082
3083 let Some(ref drop_zone) = drag_state.drop_zone else {
3084 return;
3085 };
3086
3087 let split_id = drop_zone.split_id();
3088
3089 let split_area = self
3091 .active_layout()
3092 .split_areas
3093 .iter()
3094 .find(|(sid, _, _, _, _, _)| *sid == split_id)
3095 .map(|(_, _, content_rect, _, _, _)| *content_rect);
3096
3097 let Some(content_rect) = split_area else {
3098 return;
3099 };
3100
3101 use super::types::TabDropZone;
3103
3104 let highlight_area = match drop_zone {
3105 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
3106 content_rect
3109 }
3110 TabDropZone::SplitLeft(_) => {
3111 let width = (content_rect.width / 2).max(3);
3113 ratatui::layout::Rect::new(
3114 content_rect.x,
3115 content_rect.y,
3116 width,
3117 content_rect.height,
3118 )
3119 }
3120 TabDropZone::SplitRight(_) => {
3121 let width = (content_rect.width / 2).max(3);
3123 let x = content_rect.x + content_rect.width - width;
3124 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
3125 }
3126 TabDropZone::SplitTop(_) => {
3127 let height = (content_rect.height / 2).max(2);
3129 ratatui::layout::Rect::new(
3130 content_rect.x,
3131 content_rect.y,
3132 content_rect.width,
3133 height,
3134 )
3135 }
3136 TabDropZone::SplitBottom(_) => {
3137 let height = (content_rect.height / 2).max(2);
3139 let y = content_rect.y + content_rect.height - height;
3140 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
3141 }
3142 };
3143
3144 let buf = frame.buffer_mut();
3147 let drop_zone_bg = self.theme.read().unwrap().tab_drop_zone_bg;
3148 let drop_zone_border = self.theme.read().unwrap().tab_drop_zone_border;
3149
3150 for y in highlight_area.y..highlight_area.y + highlight_area.height {
3152 for x in highlight_area.x..highlight_area.x + highlight_area.width {
3153 if let Some(cell) = buf.cell_mut((x, y)) {
3154 cell.set_bg(drop_zone_bg);
3157
3158 let is_border = x == highlight_area.x
3160 || x == highlight_area.x + highlight_area.width - 1
3161 || y == highlight_area.y
3162 || y == highlight_area.y + highlight_area.height - 1;
3163
3164 if is_border {
3165 cell.set_fg(drop_zone_border);
3166 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
3167 }
3168 }
3169 }
3170 }
3171
3172 match drop_zone {
3174 TabDropZone::SplitLeft(_) => {
3175 for y in highlight_area.y..highlight_area.y + highlight_area.height {
3177 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
3178 cell.set_symbol("▌");
3179 cell.set_fg(drop_zone_border);
3180 }
3181 }
3182 }
3183 TabDropZone::SplitRight(_) => {
3184 let x = highlight_area.x + highlight_area.width - 1;
3186 for y in highlight_area.y..highlight_area.y + highlight_area.height {
3187 if let Some(cell) = buf.cell_mut((x, y)) {
3188 cell.set_symbol("▐");
3189 cell.set_fg(drop_zone_border);
3190 }
3191 }
3192 }
3193 TabDropZone::SplitTop(_) => {
3194 for x in highlight_area.x..highlight_area.x + highlight_area.width {
3196 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
3197 cell.set_symbol("▀");
3198 cell.set_fg(drop_zone_border);
3199 }
3200 }
3201 }
3202 TabDropZone::SplitBottom(_) => {
3203 let y = highlight_area.y + highlight_area.height - 1;
3205 for x in highlight_area.x..highlight_area.x + highlight_area.width {
3206 if let Some(cell) = buf.cell_mut((x, y)) {
3207 cell.set_symbol("▄");
3208 cell.set_fg(drop_zone_border);
3209 }
3210 }
3211 }
3212 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
3213 }
3215 }
3216 }
3217
3218 pub fn recompute_layout(&mut self, width: u16, height: u16) {
3223 let size = ratatui::layout::Rect::new(0, 0, width, height);
3224
3225 let active_split = self
3227 .windows
3228 .get(&self.active_window)
3229 .and_then(|w| w.buffers.splits())
3230 .map(|(mgr, _)| mgr)
3231 .expect("active window must have a populated split layout")
3232 .active_split();
3233 self.active_window_mut()
3234 .pre_sync_ensure_visible(active_split);
3235 self.active_window_mut().sync_scroll_groups();
3236
3237 let constraints = vec![
3240 Constraint::Length(if self.active_window_mut().menu_bar_visible {
3241 1
3242 } else {
3243 0
3244 }),
3245 Constraint::Min(0),
3246 Constraint::Length(if self.active_window_mut().status_bar_visible {
3247 1
3248 } else {
3249 0
3250 }), Constraint::Length(0), Constraint::Length(if self.active_window_mut().prompt_line_visible {
3253 1
3254 } else {
3255 0
3256 }), ];
3258 let main_chunks = Layout::default()
3259 .direction(Direction::Vertical)
3260 .constraints(constraints)
3261 .split(size);
3262 let main_content_area = main_chunks[1];
3263
3264 let file_explorer_should_show = self.file_explorer_visible()
3266 && (self.file_explorer().is_some()
3267 || self.active_window().file_explorer_sync_in_progress);
3268 let editor_content_area = if file_explorer_should_show {
3269 let explorer_cols = self
3270 .active_window()
3271 .file_explorer_width
3272 .to_cols(main_content_area.width);
3273 let horizontal_chunks = Layout::default()
3274 .direction(Direction::Horizontal)
3275 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
3276 .split(main_content_area);
3277 horizontal_chunks[1]
3278 } else {
3279 main_content_area
3280 };
3281
3282 let active_window_id = self.active_window;
3287 let __win_l = self
3288 .windows
3289 .get_mut(&active_window_id)
3290 .expect("active window must exist");
3291 let tab_bar_visible = __win_l.tab_bar_visible;
3292 let theme = self.theme.read().unwrap().clone();
3293 let view_line_mappings = __win_l
3294 .buffers
3295 .with_all_mut(|buffers, mgr, vs_map| {
3296 SplitRenderer::compute_content_layout(
3297 editor_content_area,
3298 &*mgr,
3299 buffers,
3300 vs_map,
3301 &theme,
3302 false, self.config.editor.estimated_line_length,
3304 self.config.editor.highlight_context_bytes,
3305 self.config.editor.relative_line_numbers,
3306 self.config.editor.use_terminal_bg,
3307 self.session_mode || !self.software_cursor_only,
3308 self.software_cursor_only,
3309 tab_bar_visible,
3310 self.config.editor.show_vertical_scrollbar,
3311 self.config.editor.show_horizontal_scrollbar,
3312 self.config.editor.diagnostics_inline_text,
3313 self.config.editor.show_tilde,
3314 )
3315 })
3316 .expect("active window must have a populated split layout");
3317
3318 self.active_layout_mut().view_line_mappings = view_line_mappings;
3319 }
3320
3321 pub fn clear_search_history(&mut self) {
3324 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("search") {
3325 history.clear();
3326 }
3327 }
3328
3329 fn update_terminal_title(&mut self, display_name: &str) {
3337 if !self.config.editor.set_window_title {
3338 return;
3339 }
3340 let project_name = self.working_dir.file_name().and_then(|s| s.to_str());
3341 let new_title =
3342 crate::services::terminal_title::build_window_title(display_name, project_name);
3343 if self.last_window_title.as_deref() == Some(new_title.as_str()) {
3344 return;
3345 }
3346 crate::services::terminal_title::write_terminal_title(&new_title);
3347 self.last_window_title = Some(new_title);
3348 }
3349
3350 pub fn save_histories(&self) {
3353 if let Err(e) = self
3355 .authority
3356 .filesystem
3357 .create_dir_all(&self.dir_context.data_dir)
3358 {
3359 tracing::warn!("Failed to create data directory: {}", e);
3360 return;
3361 }
3362
3363 for (key, history) in &self.active_window().prompt_histories {
3365 let path = self.dir_context.prompt_history_path(key);
3366 if let Err(e) = history.save_to_file(&path) {
3367 tracing::warn!("Failed to save {} history: {}", key, e);
3368 } else {
3369 tracing::debug!("Saved {} history to {:?}", key, path);
3370 }
3371 }
3372 }
3373
3374 pub(super) fn centered_overlay_rect(
3386 area: ratatui::layout::Rect,
3387 width_pct: u8,
3388 height_pct: u8,
3389 ) -> ratatui::layout::Rect {
3390 let w_pct = width_pct.clamp(1, 100) as u32;
3391 let h_pct = height_pct.clamp(1, 100) as u32;
3392 let w = ((area.width as u32 * w_pct) / 100) as u16;
3393 let h = ((area.height as u32 * h_pct) / 100) as u16;
3394 let w = w.max(20).min(area.width);
3395 let h = h.max(8).min(area.height);
3396 ratatui::layout::Rect {
3397 x: area.x + (area.width.saturating_sub(w)) / 2,
3398 y: area.y + (area.height.saturating_sub(h)) / 2,
3399 width: w,
3400 height: h,
3401 }
3402 }
3403
3404 pub(super) fn render_floating_widget_panel(
3411 &mut self,
3412 frame: &mut Frame,
3413 area: ratatui::layout::Rect,
3414 ) {
3415 use ratatui::widgets::{Block, Borders, Clear};
3416
3417 let (width_pct, height_pct, entries, focus_cursor, embeds, overlays) =
3418 match self.floating_widget_panel.as_ref() {
3419 Some(fwp) => (
3420 fwp.width_pct,
3421 fwp.height_pct,
3422 fwp.entries.clone(),
3423 fwp.focus_cursor,
3424 fwp.embeds.clone(),
3425 fwp.overlays.clone(),
3426 ),
3427 None => return,
3428 };
3429 let theme = self.theme.read().unwrap().clone();
3430 let overlay_rect = {
3446 let requested = Self::centered_overlay_rect(area, width_pct, height_pct);
3447 let needed_h = (entries.len() as u16).saturating_add(2);
3448 let effective_h = needed_h.min(requested.height).max(3);
3449 ratatui::layout::Rect {
3450 x: requested.x,
3451 y: area.y + (area.height.saturating_sub(effective_h)) / 2,
3452 width: requested.width,
3453 height: effective_h,
3454 }
3455 };
3456
3457 crate::view::dimming::apply_dimming_excluding(frame, area, Some(overlay_rect));
3458 frame.render_widget(Clear, overlay_rect);
3459 let block = Block::default()
3460 .borders(Borders::ALL)
3461 .border_style(ratatui::style::Style::default().fg(theme.popup_border_fg))
3462 .style(ratatui::style::Style::default().bg(theme.suggestion_bg));
3463 let inner = block.inner(overlay_rect);
3464 frame.render_widget(block, overlay_rect);
3465
3466 if inner.width == 0 || inner.height == 0 {
3467 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3468 fwp.last_inner_rect = Some(inner);
3469 }
3470 return;
3471 }
3472
3473 let max_rows = inner.height as usize;
3474 for (i, entry) in entries.iter().take(max_rows).enumerate() {
3475 paint_text_property_entry(
3476 frame,
3477 entry,
3478 inner.x,
3479 inner.y + i as u16,
3480 inner.width,
3481 &theme,
3482 );
3483 }
3484
3485 let saved_preview = self.preview_window_id;
3492 for emb in &embeds {
3493 if emb.window_id == 0 {
3494 continue;
3495 }
3496 let ex = inner.x.saturating_add(emb.col_in_row as u16);
3497 let ey = inner.y.saturating_add(emb.buffer_row as u16);
3498 let max_w = inner.x.saturating_add(inner.width).saturating_sub(ex);
3502 let max_h = inner.y.saturating_add(inner.height).saturating_sub(ey);
3503 let w = (emb.width_cols as u16).min(max_w);
3504 let h = (emb.height_rows as u16).min(max_h);
3505 if w == 0 || h == 0 {
3506 continue;
3507 }
3508 let rect = ratatui::layout::Rect {
3509 x: ex,
3510 y: ey,
3511 width: w,
3512 height: h,
3513 };
3514 self.preview_window_id = Some(fresh_core::WindowId(emb.window_id as u64));
3515 self.render_session_preview_into_rect(frame, rect, &theme);
3516 }
3517 self.preview_window_id = saved_preview;
3518
3519 let panel_bg = theme.popup_bg;
3534 let panel_bg_style = ratatui::style::Style::default().bg(panel_bg);
3535 for o in &overlays {
3536 let row_y = inner.y.saturating_add(o.buffer_row as u16);
3537 if row_y >= inner.y.saturating_add(inner.height) {
3538 continue;
3539 }
3540 let row_rect = ratatui::layout::Rect {
3541 x: inner.x,
3542 y: row_y,
3543 width: inner.width,
3544 height: 1,
3545 };
3546 frame.render_widget(Clear, row_rect);
3547 frame.render_widget(Block::default().style(panel_bg_style), row_rect);
3548 paint_text_property_entry(frame, &o.entry, inner.x, row_y, inner.width, &theme);
3549 }
3550
3551 if let Some(fc) = focus_cursor {
3552 let cx = inner.x.saturating_add(byte_to_screen_col(
3553 entries
3554 .get(fc.buffer_row as usize)
3555 .map(|e| e.text.as_str())
3556 .unwrap_or(""),
3557 fc.byte_in_row as usize,
3558 ) as u16);
3559 let cy = inner.y.saturating_add(fc.buffer_row as u16);
3560 if cx < inner.x + inner.width && cy < inner.y + inner.height {
3561 frame.set_cursor_position((cx, cy));
3562 }
3563 } else {
3564 let cx = inner.x + inner.width.saturating_sub(1);
3572 let cy = inner.y + inner.height.saturating_sub(1);
3573 frame.set_cursor_position((cx, cy));
3574 }
3575
3576 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3577 fwp.last_inner_rect = Some(inner);
3578 }
3579 }
3580
3581 fn resolve_overlay_style(
3582 opts: &fresh_core::api::OverlayOptions,
3583 theme: &crate::view::theme::Theme,
3584 ) -> ratatui::style::Style {
3585 use crate::view::theme::named_color_from_str;
3586 use fresh_core::api::OverlayColorSpec;
3587 use ratatui::style::{Color, Modifier, Style};
3588
3589 let resolve = |spec: &OverlayColorSpec| -> Option<Color> {
3590 match spec {
3591 OverlayColorSpec::Rgb(r, g, b) => Some(Color::Rgb(*r, *g, *b)),
3592 OverlayColorSpec::ThemeKey(k) => {
3593 named_color_from_str(k).or_else(|| theme.resolve_theme_key(k))
3594 }
3595 }
3596 };
3597
3598 let mut style = Style::default();
3599 if let Some(ref fg) = opts.fg {
3600 if let Some(c) = resolve(fg) {
3601 style = style.fg(c);
3602 }
3603 }
3604 if let Some(ref bg) = opts.bg {
3605 if let Some(c) = resolve(bg) {
3606 style = style.bg(c);
3607 }
3608 }
3609 let mut m = Modifier::empty();
3610 if opts.bold {
3611 m |= Modifier::BOLD;
3612 }
3613 if opts.italic {
3614 m |= Modifier::ITALIC;
3615 }
3616 if opts.underline {
3617 m |= Modifier::UNDERLINED;
3618 }
3619 if opts.strikethrough {
3620 m |= Modifier::CROSSED_OUT;
3621 }
3622 if !m.is_empty() {
3623 style = style.add_modifier(m);
3624 }
3625 style
3626 }
3627}
3628
3629fn paint_text_property_entry(
3635 frame: &mut ratatui::Frame,
3636 entry: &fresh_core::text_property::TextPropertyEntry,
3637 x: u16,
3638 y: u16,
3639 width: u16,
3640 theme: &crate::view::theme::Theme,
3641) {
3642 use ratatui::style::Style;
3643 use ratatui::text::{Line, Span};
3644 use ratatui::widgets::Paragraph;
3645
3646 let mut normalized = entry.clone();
3647 normalized.normalize_widths();
3648 let mut text = normalized.text.clone();
3649 while text.ends_with('\n') {
3650 text.pop();
3651 }
3652
3653 let base_bg = theme.suggestion_bg;
3654 let base_style = if let Some(opts) = normalized.style.as_ref() {
3655 let resolved = Editor::resolve_overlay_style(opts, theme);
3663 if resolved.bg.is_none() {
3664 resolved.bg(base_bg)
3665 } else {
3666 resolved
3667 }
3668 } else {
3669 Style::default().bg(base_bg)
3670 };
3671
3672 let boundaries: std::collections::BTreeSet<usize> = std::iter::once(0)
3677 .chain(std::iter::once(text.len()))
3678 .chain(
3679 normalized
3680 .inline_overlays
3681 .iter()
3682 .flat_map(|o| [o.start.min(text.len()), o.end.min(text.len())]),
3683 )
3684 .collect();
3685 let bounds: Vec<usize> = boundaries.into_iter().collect();
3686
3687 let mut spans: Vec<Span<'_>> = Vec::new();
3688 for win in bounds.windows(2) {
3689 let (a, b) = (win[0], win[1]);
3690 if a >= b {
3691 continue;
3692 }
3693 let slice = text[a..b].to_string();
3694 let mut style = base_style;
3704 for o in &normalized.inline_overlays {
3705 let os = o.start.min(text.len());
3706 let oe = o.end.min(text.len());
3707 if a >= os && b <= oe && oe > os {
3708 let resolved = Editor::resolve_overlay_style(&o.style, theme);
3709 if let Some(fg) = resolved.fg {
3710 style = style.fg(fg);
3711 }
3712 if let Some(bg) = resolved.bg {
3713 style = style.bg(bg);
3714 }
3715 style = style.add_modifier(resolved.add_modifier);
3720 style = style.remove_modifier(resolved.sub_modifier);
3721 }
3722 }
3723 if style.bg.is_none() {
3727 style = style.bg(base_bg);
3728 }
3729 spans.push(Span::styled(slice, style));
3730 }
3731
3732 let line = Line::from(spans);
3733 let rect = ratatui::layout::Rect {
3734 x,
3735 y,
3736 width,
3737 height: 1,
3738 };
3739 frame.render_widget(Paragraph::new(line).style(base_style), rect);
3740}
3741
3742fn byte_to_screen_col(text: &str, target_byte: usize) -> usize {
3747 use unicode_width::UnicodeWidthChar;
3748 let mut byte = 0;
3749 let mut col = 0usize;
3750 for ch in text.chars() {
3751 if byte >= target_byte {
3752 break;
3753 }
3754 col += UnicodeWidthChar::width(ch).unwrap_or(0);
3755 byte += ch.len_utf8();
3756 }
3757 col
3758}