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 );
341 }
342 } else {
345 self.active_layout_mut().file_explorer_area = None;
347 editor_content_area = main_content_area;
348 }
349
350 if self.plugin_manager.read().unwrap().is_active() {
357 let hooks_start = std::time::Instant::now();
358 let visible_buffers = self
360 .windows
361 .get(&self.active_window)
362 .and_then(|w| w.buffers.splits())
363 .map(|(mgr, _)| mgr)
364 .expect("active window must have a populated split layout")
365 .get_visible_buffers(editor_content_area);
366
367 let mut total_new_lines = 0usize;
368 for (split_id, buffer_id, split_area) in visible_buffers {
369 let viewport_top_byte = self
371 .windows
372 .get(&self.active_window)
373 .and_then(|w| w.buffers.splits())
374 .map(|(_, vs)| vs)
375 .expect("active window must have a populated split layout")
376 .get(&split_id)
377 .map(|vs| vs.viewport.top_byte)
378 .unwrap_or(0);
379
380 let __active_id = self.active_window;
381 let __win = self
382 .windows
383 .get_mut(&__active_id)
384 .expect("active window must exist");
385 let seen_ranges_for_win = &mut __win.seen_byte_ranges;
390 let plugin_manager = &self.plugin_manager;
391 let estimated_line_length = self.config.editor.estimated_line_length;
392 let added = __win
393 .buffers
394 .with_buffer_and_view_states(buffer_id, |state, vs_map| {
395 plugin_manager.read().unwrap().run_hook(
396 "render_start",
397 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
398 );
399
400 let visible_count = split_area.height as usize;
401 let is_binary = state.buffer.is_binary();
402 let line_ending = state.buffer.line_ending();
403 let base_tokens =
404 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
405 &mut state.buffer,
406 viewport_top_byte,
407 estimated_line_length,
408 visible_count,
409 is_binary,
410 line_ending,
411 );
412 let viewport_start = viewport_top_byte;
413 let viewport_end = base_tokens
414 .last()
415 .and_then(|t| t.source_offset)
416 .unwrap_or(viewport_start);
417 let cursor_positions: Vec<usize> = vs_map
418 .get(&split_id)
419 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
420 .unwrap_or_default();
421 plugin_manager.read().unwrap().run_hook(
422 "view_transform_request",
423 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
424 buffer_id,
425 split_id: split_id.into(),
426 viewport_start,
427 viewport_end,
428 tokens: base_tokens,
429 cursor_positions,
430 },
431 );
432
433 if let Some(vs) = vs_map.get_mut(&split_id) {
436 vs.view_transform_stale = false;
437 }
438
439 let top_byte = viewport_top_byte;
440 let seen_byte_ranges =
441 seen_ranges_for_win.entry(buffer_id).or_default();
442
443 let mut new_lines: Vec<
444 crate::services::plugins::hooks::LineInfo,
445 > = Vec::new();
446 let mut line_number = state.buffer.get_line_number(top_byte);
447 let mut iter = state
448 .buffer
449 .line_iterator(top_byte, estimated_line_length);
450
451 for _ in 0..visible_count {
452 if let Some((line_start, line_content)) = iter.next_line() {
453 let byte_end = line_start + line_content.len();
454 let byte_range = (line_start, byte_end);
455
456 if !seen_byte_ranges.contains(&byte_range) {
457 new_lines.push(
458 crate::services::plugins::hooks::LineInfo {
459 line_number,
460 byte_start: line_start,
461 byte_end,
462 content: line_content,
463 },
464 );
465 seen_byte_ranges.insert(byte_range);
466 }
467 line_number += 1;
468 } else {
469 break;
470 }
471 }
472
473 let count = new_lines.len();
474 if !new_lines.is_empty() {
475 plugin_manager.read().unwrap().run_hook(
476 "lines_changed",
477 crate::services::plugins::hooks::HookArgs::LinesChanged {
478 buffer_id,
479 lines: new_lines,
480 },
481 );
482 }
483 count
484 })
485 .unwrap_or(0);
486 total_new_lines += added;
487 }
488 let hooks_elapsed = hooks_start.elapsed();
489 tracing::trace!(
490 new_lines = total_new_lines,
491 elapsed_ms = hooks_elapsed.as_millis(),
492 elapsed_us = hooks_elapsed.as_micros(),
493 "lines_changed hooks total"
494 );
495
496 let commands = self.plugin_manager.write().unwrap().process_commands();
508 let dispatched_any = !commands.is_empty();
509 if dispatched_any {
510 let cmd_names: Vec<String> =
511 commands.iter().map(|c| c.debug_variant_name()).collect();
512 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
513 }
514 for command in commands {
515 if let Err(e) = self.handle_plugin_command(command) {
516 tracing::error!("Error handling plugin command: {}", e);
517 }
518 }
519
520 self.flush_pending_grammars();
522
523 if dispatched_any {
551 show_search_options = self.active_window().prompt.as_ref().is_some_and(|p| {
552 matches!(
553 p.prompt_type,
554 PromptType::Search
555 | PromptType::ReplaceSearch
556 | PromptType::Replace { .. }
557 | PromptType::QueryReplaceSearch
558 | PromptType::QueryReplace { .. }
559 )
560 });
561 prompt_is_overlay = self
562 .active_window()
563 .prompt
564 .as_ref()
565 .is_some_and(|p| p.overlay);
566 has_suggestions = self
567 .active_window()
568 .prompt
569 .as_ref()
570 .is_some_and(|p| !p.suggestions.is_empty())
571 && !prompt_is_overlay;
572 has_file_browser = self.active_window().prompt.as_ref().is_some_and(|p| {
573 matches!(
574 p.prompt_type,
575 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
576 )
577 }) && self.active_window_mut().file_open_state.is_some();
578 main_chunks = Layout::default()
579 .direction(Direction::Vertical)
580 .constraints(vec![
581 Constraint::Length(if self.active_window_mut().menu_bar_visible {
582 1
583 } else {
584 0
585 }),
586 Constraint::Min(0),
587 Constraint::Length(
588 if !self.active_window_mut().status_bar_visible
589 || has_suggestions
590 || has_file_browser
591 {
592 0
593 } else {
594 1
595 },
596 ),
597 Constraint::Length(if show_search_options { 1 } else { 0 }),
598 Constraint::Length(
599 if (self.active_window_mut().prompt_line_visible
600 || self.active_window().prompt.is_some())
601 && !prompt_is_overlay
602 {
603 1
604 } else {
605 0
606 },
607 ),
608 ])
609 .split(size);
610 }
611 }
612
613 let lsp_waiting = !self.active_window().pending_completion_requests.is_empty()
615 || self
616 .active_window()
617 .pending_goto_definition_request
618 .is_some();
619
620 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
627 let hide_cursor = self.menu_state.active_menu.is_some()
628 || self.active_window_mut().key_context == KeyContext::FileExplorer
629 || self.active_window().terminal_mode
630 || settings_visible
631 || self.keybinding_editor.is_some();
632
633 let hovered_tab = match &self.active_window_mut().mouse_state.hover_target {
635 Some(HoverTarget::TabName(target, split_id)) => Some((*target, *split_id, false)),
636 Some(HoverTarget::TabCloseButton(target, split_id)) => Some((*target, *split_id, true)),
637 _ => None,
638 };
639
640 let hovered_close_split = match &self.active_window_mut().mouse_state.hover_target {
642 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
643 _ => None,
644 };
645
646 let hovered_maximize_split = match &self.active_window_mut().mouse_state.hover_target {
648 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
649 _ => None,
650 };
651
652 let is_maximized = self
653 .windows
654 .get(&self.active_window)
655 .and_then(|w| w.buffers.splits())
656 .map(|(mgr, _)| mgr)
657 .expect("active window must have a populated split layout")
658 .is_maximized();
659
660 let mut pending_hardware_cursor: Option<(u16, u16)> = None;
667
668 let _content_span = tracing::info_span!("render_content").entered();
669 let active_window_id = self.active_window;
674 let __win = self
678 .windows
679 .get_mut(&active_window_id)
680 .expect("active window must exist");
681 let __metadata_ref = &__win.buffer_metadata;
682 let __event_logs_mut = &mut __win.event_logs;
683 let __grouped_ref = &__win.grouped_subtrees;
684 let __composite_buffers_mut = &mut __win.composite_buffers;
685 let __composite_view_states_mut = &mut __win.composite_view_states;
686 let __cell_theme_map_mut = &mut __win.chrome_layout.cell_theme_map;
687 let __tab_bar_visible = __win.tab_bar_visible;
688 let (
689 split_areas,
690 tab_layouts,
691 close_split_areas,
692 maximize_split_areas,
693 view_line_mappings,
694 horizontal_scrollbar_areas,
695 grouped_separator_areas,
696 ) = __win
697 .buffers
698 .with_all_mut(|__buffers_mut, __mgr, __vs_map| {
699 SplitRenderer::render_content(
700 frame,
701 editor_content_area,
702 &*__mgr,
703 __buffers_mut,
704 __metadata_ref,
705 __event_logs_mut,
706 __composite_buffers_mut,
707 __composite_view_states_mut,
708 &*self.theme.read().unwrap(),
709 self.ansi_background.as_ref(),
710 self.background_fade,
711 lsp_waiting,
712 self.config.editor.large_file_threshold_bytes,
713 self.config.editor.line_wrap,
714 self.config.editor.estimated_line_length,
715 self.config.editor.highlight_context_bytes,
716 Some(__vs_map),
717 __grouped_ref,
718 hide_cursor,
719 hovered_tab,
720 hovered_close_split,
721 hovered_maximize_split,
722 is_maximized,
723 self.config.editor.relative_line_numbers,
724 __tab_bar_visible,
725 self.config.editor.use_terminal_bg,
726 self.session_mode || !self.software_cursor_only,
727 self.software_cursor_only,
728 self.config.editor.show_vertical_scrollbar,
729 self.config.editor.show_horizontal_scrollbar,
730 self.config.editor.diagnostics_inline_text,
731 self.config.editor.show_tilde,
732 self.config.editor.highlight_current_column,
733 __cell_theme_map_mut,
734 size.width,
735 &mut pending_hardware_cursor,
736 )
737 })
738 .expect("active window must have a populated split layout");
739
740 drop(_content_span);
741
742 self.maybe_start_cursor_jump_animation(pending_hardware_cursor, active_split);
748
749 if self.plugin_manager.read().unwrap().is_active() {
753 for (split_id, view_state) in self
754 .windows
755 .get(&self.active_window)
756 .and_then(|w| w.buffers.splits())
757 .map(|(_, vs)| vs)
758 .expect("active window must have a populated split layout")
759 {
760 let current = (
761 view_state.viewport.top_byte,
762 view_state.viewport.width,
763 view_state.viewport.height,
764 );
765 let (changed, previous) =
770 match self.active_window().previous_viewports.get(split_id) {
771 Some(previous) => (*previous != current, Some(*previous)),
772 None => (false, None), };
774 tracing::trace!(
775 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
776 split_id,
777 current,
778 previous,
779 changed
780 );
781 if changed {
782 if let Some(buffer_id) = self
783 .windows
784 .get(&self.active_window)
785 .and_then(|w| w.buffers.splits())
786 .map(|(mgr, _)| mgr)
787 .expect("active window must have a populated split layout")
788 .get_buffer_id((*split_id).into())
789 {
790 let top_line = self
792 .windows
793 .get(&self.active_window)
794 .map(|w| &w.buffers)
795 .expect("active window present")
796 .get(&buffer_id)
797 .and_then(|state| {
798 if state.buffer.line_count().is_some() {
799 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
800 } else {
801 None
802 }
803 });
804 tracing::debug!(
805 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
806 split_id,
807 buffer_id,
808 view_state.viewport.top_byte,
809 top_line
810 );
811 self.plugin_manager.read().unwrap().run_hook(
812 "viewport_changed",
813 crate::services::plugins::hooks::HookArgs::ViewportChanged {
814 split_id: (*split_id).into(),
815 buffer_id,
816 top_byte: view_state.viewport.top_byte,
817 top_line,
818 width: view_state.viewport.width,
819 height: view_state.viewport.height,
820 },
821 );
822 }
823 }
824 }
825 }
826
827 let __vp_win = self
832 .windows
833 .get_mut(&self.active_window)
834 .expect("active window present");
835 __vp_win.previous_viewports.clear();
836 let (_, __vp_vs_map) = __vp_win
837 .buffers
838 .splits()
839 .expect("active window must have a populated split layout");
840 let snapshot: Vec<(LeafId, (usize, u16, u16))> = __vp_vs_map
841 .iter()
842 .map(|(split_id, view_state)| {
843 (
844 *split_id,
845 (
846 view_state.viewport.top_byte,
847 view_state.viewport.width,
848 view_state.viewport.height,
849 ),
850 )
851 })
852 .collect();
853 for (split_id, vp) in snapshot {
854 __vp_win.previous_viewports.insert(split_id, vp);
855 }
856
857 self.render_terminal_splits(frame, &split_areas);
859
860 self.active_layout_mut().split_areas = split_areas;
861 self.active_layout_mut().horizontal_scrollbar_areas = horizontal_scrollbar_areas;
862 self.active_layout_mut().tab_layouts = tab_layouts;
863 self.active_layout_mut().close_split_areas = close_split_areas;
864 self.active_layout_mut().maximize_split_areas = maximize_split_areas;
865 self.active_layout_mut().view_line_mappings = view_line_mappings;
866
867 self.drain_pending_vb_animations();
872 let mut separator_areas = self
873 .split_manager_mut()
874 .get_separators_with_ids(editor_content_area);
875 separator_areas.extend(grouped_separator_areas);
880 self.active_layout_mut().separator_areas = separator_areas;
881 self.active_layout_mut().editor_content_area = Some(editor_content_area);
882
883 self.render_hover_highlights(frame);
885
886 self.active_chrome_mut().suggestions_area = None;
888 self.active_chrome_mut().suggestions_outer_area = None;
889 self.active_window_mut().file_browser_layout = None;
890
891 let display_name = self
893 .active_window()
894 .buffer_metadata
895 .get(&self.active_buffer())
896 .map(|m| m.display_name.clone())
897 .unwrap_or_else(|| "[No Name]".to_string());
898
899 self.update_terminal_title(&display_name);
903
904 let status_message = self.active_window().status_message.clone();
905 let plugin_status_message = self.active_window().plugin_status_message.clone();
906 let prompt = self.active_window().prompt.clone();
907 let current_language = self
930 .buffers()
931 .get(&self.active_buffer())
932 .map(|s| s.language.clone())
933 .unwrap_or_default();
934 let buffer_lsp_disabled_reason = self
935 .active_window()
936 .buffer_metadata
937 .get(&self.active_buffer())
938 .filter(|m| !m.lsp_enabled)
939 .and_then(|m| m.lsp_disabled_reason.as_deref());
940 let (lsp_status, lsp_indicator_state) = compose_lsp_status(
941 ¤t_language,
942 buffer_lsp_disabled_reason,
943 &self.active_window().lsp_progress,
944 &self.active_window().lsp_server_statuses,
945 &self.config.lsp,
946 &self.active_window().user_dismissed_lsp_languages,
947 );
948 let theme = self.theme.read().unwrap().clone();
949 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());
954
955 if self.active_window_mut().status_bar_visible && !has_suggestions && !has_file_browser {
957 let (warning_level, general_warning_count) =
960 if self.config.warnings.show_status_indicator {
961 let lsp_level = {
962 use crate::services::async_bridge::LspServerStatus;
963 let mut level = WarningLevel::None;
964 for ((lang, _), status) in &self.active_window().lsp_server_statuses {
965 if lang == ¤t_language {
966 match status {
967 LspServerStatus::Error => {
968 level = WarningLevel::Error;
969 break;
970 }
971 LspServerStatus::Starting | LspServerStatus::Initializing => {
972 if level != WarningLevel::Error {
973 level = WarningLevel::Warning;
974 }
975 }
976 _ => {}
977 }
978 }
979 }
980 level
981 };
982 (
983 lsp_level,
984 self.active_window().warning_domains.general.count,
985 )
986 } else {
987 (WarningLevel::None, 0)
988 };
989
990 use crate::view::ui::status_bar::StatusBarHover;
992 let status_bar_hover = match &self.active_window_mut().mouse_state.hover_target {
993 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
994 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
995 Some(HoverTarget::StatusBarLineEndingIndicator) => {
996 StatusBarHover::LineEndingIndicator
997 }
998 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
999 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
1000 Some(HoverTarget::StatusBarRemoteIndicator) => StatusBarHover::RemoteIndicator,
1001 _ => StatusBarHover::None,
1002 };
1003
1004 let remote_connection = self.connection_display_string();
1005
1006 let session_name = self.session_name().map(|s| s.to_string());
1008
1009 let active_split = self.effective_active_split();
1010 let active_buf = self.active_buffer();
1011 let default_cursors = crate::model::cursor::Cursors::new();
1012 let is_read_only = self
1013 .active_window()
1014 .buffer_metadata
1015 .get(&active_buf)
1016 .map(|m| m.read_only)
1017 .unwrap_or(false);
1018 let is_synthetic_placeholder = self
1019 .active_window()
1020 .buffer_metadata
1021 .get(&active_buf)
1022 .map(|m| m.synthetic_placeholder)
1023 .unwrap_or(false);
1024 let __active_id = self.active_window;
1027 let __win = self
1028 .windows
1029 .get_mut(&__active_id)
1030 .expect("active window must exist");
1031 let status_bar_layout = __win
1032 .buffers
1033 .with_buffer_and_view_states(active_buf, |state, vs_map| {
1034 let cursors = vs_map
1035 .get(&active_split)
1036 .map(|v| &v.cursors)
1037 .unwrap_or(&default_cursors);
1038 let mut status_ctx = crate::view::ui::status_bar::StatusBarContext {
1039 state,
1040 cursors,
1041 status_message: &status_message,
1042 plugin_status_message: &plugin_status_message,
1043 lsp_status: &lsp_status,
1044 lsp_indicator_state,
1045 theme: &theme,
1046 display_name: &display_name,
1047 keybindings: &keybindings_cloned,
1048 chord_state: &chord_state_cloned,
1049 update_available: update_available.as_deref(),
1050 warning_level,
1051 general_warning_count,
1052 hover: status_bar_hover,
1053 remote_connection: remote_connection.as_deref(),
1054 session_name: session_name.as_deref(),
1055 read_only: is_read_only,
1056 remote_state_override: self.remote_indicator_override.as_ref(),
1057 is_synthetic_placeholder,
1058 remote_indicator_on_bar: false,
1063 };
1064 StatusBarRenderer::render_status_bar(
1065 frame,
1066 main_chunks[status_bar_idx],
1067 &mut status_ctx,
1068 &self.config.editor.status_bar,
1069 )
1070 })
1071 .expect("active buffer must be present");
1072
1073 let status_bar_area = main_chunks[status_bar_idx];
1075 self.active_chrome_mut().status_bar_area =
1076 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
1077 self.active_chrome_mut().status_bar_lsp_area = status_bar_layout.lsp_indicator;
1078 self.active_chrome_mut().status_bar_warning_area = status_bar_layout.warning_badge;
1079 self.active_chrome_mut().status_bar_line_ending_area =
1080 status_bar_layout.line_ending_indicator;
1081 self.active_chrome_mut().status_bar_encoding_area =
1082 status_bar_layout.encoding_indicator;
1083 self.active_chrome_mut().status_bar_language_area =
1084 status_bar_layout.language_indicator;
1085 self.active_chrome_mut().status_bar_message_area = status_bar_layout.message_area;
1086 self.active_chrome_mut().status_bar_remote_area = status_bar_layout.remote_indicator;
1087 }
1088
1089 if show_search_options {
1091 let confirm_each = self.active_window().prompt.as_ref().and_then(|p| {
1093 if matches!(
1094 p.prompt_type,
1095 PromptType::ReplaceSearch
1096 | PromptType::Replace { .. }
1097 | PromptType::QueryReplaceSearch
1098 | PromptType::QueryReplace { .. }
1099 ) {
1100 Some(self.active_window().search_confirm_each)
1101 } else {
1102 None
1103 }
1104 });
1105
1106 use crate::view::ui::status_bar::SearchOptionsHover;
1108 let search_options_hover = match &self.active_window_mut().mouse_state.hover_target {
1109 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
1110 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
1111 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
1112 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
1113 _ => SearchOptionsHover::None,
1114 };
1115
1116 let search_options_layout = StatusBarRenderer::render_search_options(
1117 frame,
1118 main_chunks[search_options_idx],
1119 self.active_window().search_case_sensitive,
1120 self.active_window().search_whole_word,
1121 self.active_window().search_use_regex,
1122 confirm_each,
1123 &theme,
1124 &keybindings_cloned,
1125 search_options_hover,
1126 );
1127 self.active_chrome_mut().search_options_layout = Some(search_options_layout);
1128 } else {
1129 self.active_chrome_mut().search_options_layout = None;
1130 }
1131
1132 if let Some(prompt) = &prompt {
1137 if !prompt.overlay {
1138 if matches!(
1140 prompt.prompt_type,
1141 crate::view::prompt::PromptType::OpenFile
1142 | crate::view::prompt::PromptType::SwitchProject
1143 ) {
1144 if let Some(file_open_state) = &self.active_window_mut().file_open_state {
1145 StatusBarRenderer::render_file_open_prompt(
1146 frame,
1147 main_chunks[prompt_line_idx],
1148 prompt,
1149 file_open_state,
1150 &theme,
1151 );
1152 } else {
1153 StatusBarRenderer::render_prompt(
1154 frame,
1155 main_chunks[prompt_line_idx],
1156 prompt,
1157 &theme,
1158 );
1159 }
1160 } else {
1161 StatusBarRenderer::render_prompt(
1162 frame,
1163 main_chunks[prompt_line_idx],
1164 prompt,
1165 &theme,
1166 );
1167 }
1168 }
1169 }
1170
1171 if self
1176 .active_window()
1177 .prompt
1178 .as_ref()
1179 .is_some_and(|p| p.overlay)
1180 {
1181 self.prepare_overlay_preview();
1182 }
1183
1184 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
1187
1188 let theme_clone = self.theme.read().unwrap().clone();
1191 let hover_target = self.active_window_mut().mouse_state.hover_target.clone();
1192
1193 self.active_chrome_mut().popup_areas.clear();
1195
1196 let popup_info: Vec<_> = {
1198 let active_split = self
1200 .windows
1201 .get(&self.active_window)
1202 .and_then(|w| w.buffers.splits())
1203 .map(|(mgr, _)| mgr)
1204 .expect("active window must have a populated split layout")
1205 .active_split();
1206 let viewport = self
1207 .windows
1208 .get(&self.active_window)
1209 .and_then(|w| w.buffers.splits())
1210 .map(|(_, vs)| vs)
1211 .expect("active window must have a populated split layout")
1212 .get(&active_split)
1213 .map(|vs| vs.viewport.clone());
1214
1215 let content_rect = self
1220 .active_layout()
1221 .split_areas
1222 .iter()
1223 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1224 .map(|(_, _, rect, _, _, _)| *rect);
1225
1226 let primary_cursor = self
1227 .windows
1228 .get(&self.active_window)
1229 .and_then(|w| w.buffers.splits())
1230 .map(|(_, vs)| vs)
1231 .expect("active window must have a populated split layout")
1232 .get(&active_split)
1233 .map(|vs| *vs.cursors.primary());
1234 let state = self.active_state_mut();
1235 if state.popups.is_visible() {
1236 let primary_cursor =
1238 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
1239
1240 let gutter_width = viewport
1242 .as_ref()
1243 .map(|vp| vp.gutter_width(&state.buffer) as u16)
1244 .unwrap_or(0);
1245
1246 let cursor_screen_pos = viewport
1247 .as_ref()
1248 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
1249 .unwrap_or((0, 0));
1250
1251 let word_start_screen_pos = {
1255 use crate::primitives::word_navigation::find_completion_word_start;
1256 let word_start =
1257 find_completion_word_start(&state.buffer, primary_cursor.position);
1258 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
1259 viewport
1260 .as_ref()
1261 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
1262 .unwrap_or((0, 0))
1263 };
1264
1265 let (base_x, base_y) = content_rect
1270 .map(|r| (r.x + gutter_width, r.y))
1271 .unwrap_or((gutter_width, 1));
1272
1273 let cursor_screen_pos =
1274 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
1275 let word_start_screen_pos = (
1276 word_start_screen_pos.0 + base_x,
1277 word_start_screen_pos.1 + base_y,
1278 );
1279
1280 state
1282 .popups
1283 .all()
1284 .iter()
1285 .enumerate()
1286 .map(|(popup_idx, popup)| {
1287 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
1289 (word_start_screen_pos.0, cursor_screen_pos.1)
1290 } else {
1291 cursor_screen_pos
1292 };
1293 let popup_area = popup.calculate_area(size, Some(popup_pos));
1294
1295 let desc_height = popup.description_height();
1298 let inner_area = if popup.bordered {
1299 ratatui::layout::Rect {
1300 x: popup_area.x + 1,
1301 y: popup_area.y + 1 + desc_height,
1302 width: popup_area.width.saturating_sub(2),
1303 height: popup_area.height.saturating_sub(2 + desc_height),
1304 }
1305 } else {
1306 ratatui::layout::Rect {
1307 x: popup_area.x,
1308 y: popup_area.y + desc_height,
1309 width: popup_area.width,
1310 height: popup_area.height.saturating_sub(desc_height),
1311 }
1312 };
1313
1314 let num_items = match &popup.content {
1315 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1316 _ => 0,
1317 };
1318
1319 let total_lines = popup.item_count();
1321 let visible_lines = inner_area.height as usize;
1322 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
1323 {
1324 Some(ratatui::layout::Rect {
1325 x: inner_area.x + inner_area.width - 1,
1326 y: inner_area.y,
1327 width: 1,
1328 height: inner_area.height,
1329 })
1330 } else {
1331 None
1332 };
1333
1334 (
1335 popup_idx,
1336 popup_area,
1337 inner_area,
1338 popup.scroll_offset,
1339 num_items,
1340 scrollbar_rect,
1341 total_lines,
1342 )
1343 })
1344 .collect()
1345 } else {
1346 Vec::new()
1347 }
1348 };
1349
1350 self.active_chrome_mut().popup_areas = popup_info.clone();
1352
1353 let state = self.active_state_mut();
1355 if state.popups.is_visible() {
1356 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
1357 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
1358 popup.render_with_hover(
1359 frame,
1360 *popup_area,
1361 &theme_clone,
1362 hover_target.as_ref(),
1363 );
1364 }
1365 }
1366 }
1367
1368 self.active_chrome_mut().global_popup_areas.clear();
1379 if let Some(popup) = self.global_popups.top() {
1380 let top_idx = self.global_popups.all().len() - 1;
1381 let popup_area = popup.calculate_area(size, None);
1382 let desc_height = popup.description_height();
1383 let inner_area = if popup.bordered {
1384 ratatui::layout::Rect {
1385 x: popup_area.x + 1,
1386 y: popup_area.y + 1 + desc_height,
1387 width: popup_area.width.saturating_sub(2),
1388 height: popup_area.height.saturating_sub(2 + desc_height),
1389 }
1390 } else {
1391 ratatui::layout::Rect {
1392 x: popup_area.x,
1393 y: popup_area.y + desc_height,
1394 width: popup_area.width,
1395 height: popup_area.height.saturating_sub(desc_height),
1396 }
1397 };
1398 let num_items = match &popup.content {
1399 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1400 _ => 0,
1401 };
1402 let scroll_offset = popup.scroll_offset;
1403 popup.render_with_hover(frame, popup_area, &theme_clone, hover_target.as_ref());
1404 self.active_chrome_mut().global_popup_areas.push((
1405 top_idx,
1406 popup_area,
1407 inner_area,
1408 scroll_offset,
1409 num_items,
1410 ));
1411 }
1412
1413 self.update_menu_context();
1416
1417 let settings_visible = self
1420 .settings_state
1421 .as_ref()
1422 .map(|s| s.visible)
1423 .unwrap_or(false);
1424 if settings_visible {
1425 crate::view::dimming::apply_dimming(frame, size);
1427 }
1428 if let Some(ref mut settings_state) = self.settings_state {
1429 if settings_state.visible {
1430 settings_state.update_focus_states();
1431 let settings_layout = crate::view::settings::render_settings(
1432 frame,
1433 size,
1434 settings_state,
1435 &*self.theme.read().unwrap(),
1436 );
1437 self.active_chrome_mut().settings_layout = Some(settings_layout);
1438 }
1439 }
1440
1441 if let Some(ref wizard) = self.calibration_wizard {
1443 crate::view::dimming::apply_dimming(frame, size);
1445 crate::view::calibration_wizard::render_calibration_wizard(
1446 frame,
1447 size,
1448 wizard,
1449 &*self.theme.read().unwrap(),
1450 );
1451 }
1452
1453 if let Some(ref mut kb_editor) = self.keybinding_editor {
1455 crate::view::dimming::apply_dimming(frame, size);
1456 crate::view::keybinding_editor::render_keybinding_editor(
1457 frame,
1458 size,
1459 kb_editor,
1460 &*self.theme.read().unwrap(),
1461 );
1462 }
1463
1464 if let Some(ref debug) = self.active_window().event_debug {
1466 crate::view::dimming::apply_dimming(frame, size);
1468 crate::view::event_debug::render_event_debug(
1469 frame,
1470 size,
1471 debug,
1472 &*self.theme.read().unwrap(),
1473 );
1474 }
1475
1476 if self.active_window_mut().menu_bar_visible {
1477 self.expanded_menus_cache.update(
1481 &self.theme_registry,
1482 &self.menus,
1483 &self.menu_state.themes_dir,
1484 );
1485 let hover_target = self.active_window().mouse_state.hover_target.clone();
1486 let menu_bar_mnemonics = self.config.editor.menu_bar_mnemonics;
1487 let expanded = self.expanded_menus_cache.get().expect("just updated");
1488 let keybindings = self.keybindings.read().unwrap();
1489 let new_menu_layout = crate::view::ui::MenuRenderer::render(
1490 frame,
1491 menu_bar_area,
1492 expanded,
1493 &self.menu_state,
1494 &keybindings,
1495 &*self.theme.read().unwrap(),
1496 hover_target.as_ref(),
1497 menu_bar_mnemonics,
1498 );
1499 drop(keybindings);
1500 self.active_chrome_mut().menu_layout = Some(new_menu_layout);
1501 } else {
1502 self.active_chrome_mut().menu_layout = None;
1503 }
1504
1505 let tab_ctx_menu = self.active_window().tab_context_menu.clone();
1507 if let Some(menu) = tab_ctx_menu {
1508 self.render_tab_context_menu(frame, &menu);
1509 }
1510
1511 let fe_ctx_menu = self.active_window().file_explorer_context_menu.clone();
1512 if let Some(menu) = fe_ctx_menu {
1513 self.render_file_explorer_context_menu(frame, &menu);
1514 }
1515
1516 self.record_non_editor_theme_regions();
1518
1519 self.render_theme_info_popup(frame);
1521
1522 let drag_state_clone = self.active_window().mouse_state.dragging_tab.clone();
1524 if let Some(ref drag_state) = drag_state_clone {
1525 if drag_state.is_dragging() {
1526 self.render_tab_drop_zone(frame, drag_state);
1527 }
1528 }
1529
1530 if self.active_window_mut().gpm_active {
1536 if let Some((col, row)) = self.active_window_mut().mouse_cursor_position {
1537 use ratatui::style::Modifier;
1538
1539 if col < size.width && row < size.height {
1541 let buf = frame.buffer_mut();
1543 if let Some(cell) = buf.cell_mut((col, row)) {
1544 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1545 }
1546 }
1547 }
1548 }
1549
1550 if self.active_window_mut().keyboard_capture && self.active_window().terminal_mode {
1553 let active_split = self
1555 .windows
1556 .get(&self.active_window)
1557 .and_then(|w| w.buffers.splits())
1558 .map(|(mgr, _)| mgr)
1559 .expect("active window must have a populated split layout")
1560 .active_split();
1561 let active_split_area = self
1562 .active_layout()
1563 .split_areas
1564 .iter()
1565 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1566 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1567
1568 if let Some(terminal_area) = active_split_area {
1569 self.apply_keyboard_capture_dimming(frame, terminal_area);
1570 }
1571 }
1572
1573 if let Some((cx, cy)) = pending_hardware_cursor {
1584 if self.active_window().prompt.is_none() && !self.cursor_obscured_by_overlay(cx, cy) {
1585 frame.set_cursor_position((cx, cy));
1586 }
1587 }
1588
1589 crate::view::color_support::convert_buffer_colors(
1591 frame.buffer_mut(),
1592 self.color_capability,
1593 );
1594
1595 self.active_window_mut()
1597 .animations
1598 .apply_all(frame.buffer_mut());
1599
1600 if self.floating_widget_panel.is_some() {
1603 let frame_area = frame.area();
1604 self.render_floating_widget_panel(frame, frame_area);
1605 }
1606 }
1607
1608 fn maybe_start_cursor_jump_animation(
1623 &mut self,
1624 current_pos: Option<(u16, u16)>,
1625 active_split: crate::model::event::LeafId,
1626 ) {
1627 if !self.config.editor.animations || !self.config.editor.cursor_jump_animation {
1635 self.previous_cursor_screen_pos = current_pos.map(|p| (p, active_split));
1636 return;
1637 }
1638
1639 let Some(current) = current_pos else {
1640 self.previous_cursor_screen_pos = None;
1644 return;
1645 };
1646
1647 let prev_entry = self.previous_cursor_screen_pos;
1648 self.previous_cursor_screen_pos = Some((current, active_split));
1650
1651 let Some((prev, prev_split)) = prev_entry else {
1652 return;
1653 };
1654 if prev == current && prev_split == active_split {
1655 return;
1656 }
1657
1658 let dx = (current.0 as i32 - prev.0 as i32).abs();
1659 let dy = (current.1 as i32 - prev.1 as i32).abs();
1660 let crossed_panes = prev_split != active_split;
1668 let row_jump = dy > 2;
1669 let col_jump = dx >= 80;
1670 if !crossed_panes && !row_jump && !col_jump {
1671 return;
1672 }
1673
1674 if let Some(prev_anim) = self.cursor_jump_animation.take() {
1676 self.active_window_mut().animations.cancel(prev_anim);
1677 }
1678
1679 let cursor_color = self.theme.read().unwrap().cursor;
1680 let bg_color = self.theme.read().unwrap().editor_bg;
1681 let id = self.active_window_mut().animations.start(
1682 ratatui::layout::Rect {
1685 x: prev.0.min(current.0),
1686 y: prev.1.min(current.1),
1687 width: dx as u16 + 1,
1688 height: dy as u16 + 1,
1689 },
1690 crate::view::animation::AnimationKind::CursorJump {
1691 from: prev,
1692 to: current,
1693 duration: std::time::Duration::from_millis(140),
1694 cursor_color,
1695 bg_color,
1696 },
1697 );
1698 self.cursor_jump_animation = Some(id);
1699 }
1700
1701 fn cursor_obscured_by_overlay(&self, x: u16, y: u16) -> bool {
1705 let inside = |rect: ratatui::layout::Rect| -> bool {
1706 x >= rect.x
1707 && x < rect.x.saturating_add(rect.width)
1708 && y >= rect.y
1709 && y < rect.y.saturating_add(rect.height)
1710 };
1711
1712 if self
1713 .active_chrome()
1714 .popup_areas
1715 .iter()
1716 .any(|entry| inside(entry.1))
1717 {
1718 return true;
1719 }
1720 if self
1721 .active_chrome()
1722 .global_popup_areas
1723 .iter()
1724 .any(|entry| inside(entry.1))
1725 {
1726 return true;
1727 }
1728 if let Some((rect, _, _, _)) = self.active_chrome().suggestions_area {
1729 if inside(rect) {
1730 return true;
1731 }
1732 }
1733 if let Some(ref fb) = self.active_window().file_browser_layout {
1734 if inside(fb.popup_area) {
1735 return true;
1736 }
1737 }
1738 false
1739 }
1740
1741 fn render_quick_open_hints(
1743 frame: &mut Frame,
1744 area: ratatui::layout::Rect,
1745 theme: &crate::view::theme::Theme,
1746 ) {
1747 use ratatui::style::{Modifier, Style};
1748 use ratatui::text::{Line, Span};
1749 use ratatui::widgets::Paragraph;
1750 use rust_i18n::t;
1751
1752 let hints_style = Style::default()
1753 .fg(theme.line_number_fg)
1754 .bg(theme.suggestion_selected_bg)
1755 .add_modifier(Modifier::DIM);
1756 let hints_text = t!("quick_open.mode_hints");
1757 let left_margin = 2;
1759 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1760 let mut spans = Vec::new();
1761 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1762 spans.push(Span::styled(hints_text.to_string(), hints_style));
1763 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1764 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1765
1766 let paragraph = Paragraph::new(Line::from(spans));
1767 frame.render_widget(paragraph, area);
1768 }
1769
1770 fn apply_keyboard_capture_dimming(
1773 &self,
1774 frame: &mut Frame,
1775 terminal_area: ratatui::layout::Rect,
1776 ) {
1777 let size = frame.area();
1778 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1779 }
1780
1781 fn render_prompt_popups(
1784 &mut self,
1785 frame: &mut Frame,
1786 prompt_area: ratatui::layout::Rect,
1787 width: u16,
1788 ) {
1789 let Some(prompt) = &self.active_window_mut().prompt else {
1790 return;
1791 };
1792
1793 if prompt.overlay {
1796 let frame_area = frame.area();
1797 self.render_overlay_prompt(frame, frame_area);
1798 return;
1799 }
1800
1801 if matches!(
1802 prompt.prompt_type,
1803 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1804 ) {
1805 let hover_target = self.active_window().mouse_state.hover_target.clone();
1806 let theme = self.theme.read().unwrap().clone();
1807 let keybindings = self.keybindings.read().unwrap();
1808 let kb_clone = keybindings.clone();
1809 drop(keybindings);
1810 let max_height = prompt_area.y.saturating_sub(1).min(20);
1811 let popup_area = ratatui::layout::Rect {
1812 x: 0,
1813 y: prompt_area.y.saturating_sub(max_height),
1814 width,
1815 height: max_height,
1816 };
1817 let __win = self.active_window_mut();
1818 let Some(file_open_state) = &mut __win.file_open_state else {
1819 return;
1820 };
1821 __win.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1822 frame,
1823 popup_area,
1824 file_open_state,
1825 &theme,
1826 &hover_target,
1827 Some(&kb_clone),
1828 );
1829 return;
1830 }
1831
1832 if prompt.suggestions.is_empty() {
1833 return;
1834 }
1835
1836 let suggestion_count = prompt.suggestions.len().min(10);
1837 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1838 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1839 let height = suggestion_count as u16 + 2 + hints_height;
1840
1841 let suggestions_area = ratatui::layout::Rect {
1842 x: 0,
1843 y: prompt_area.y.saturating_sub(height),
1844 width,
1845 height: height - hints_height,
1846 };
1847
1848 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1849
1850 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
1853 prompt.ensure_selected_visible();
1854 }
1855 let Some(prompt) = &self.active_window().prompt else {
1856 return;
1857 };
1858
1859 let new_suggestions_area = SuggestionsRenderer::render_with_hover(
1860 frame,
1861 suggestions_area,
1862 prompt,
1863 &*self.theme.read().unwrap(),
1864 self.active_window().mouse_state.hover_target.as_ref(),
1865 true,
1866 );
1867 let chrome = self.active_chrome_mut();
1868 chrome.suggestions_area = new_suggestions_area;
1869 if chrome.suggestions_area.is_some() {
1870 chrome.suggestions_outer_area = Some(suggestions_area);
1871 }
1872
1873 if is_quick_open {
1874 let hints_area = ratatui::layout::Rect {
1875 x: 0,
1876 y: prompt_area.y.saturating_sub(hints_height),
1877 width,
1878 height: hints_height,
1879 };
1880 frame.render_widget(ratatui::widgets::Clear, hints_area);
1881 Self::render_quick_open_hints(frame, hints_area, &*self.theme.read().unwrap());
1882 }
1883 }
1884
1885 fn render_session_preview_into_rect(
1906 &mut self,
1907 frame: &mut ratatui::Frame,
1908 inner: ratatui::layout::Rect,
1909 theme: &crate::view::theme::Theme,
1910 ) {
1911 let Some(sid) = self.preview_window_id else {
1912 return;
1913 };
1914
1915 let preview_buffers: Vec<fresh_core::BufferId> = self
1925 .windows
1926 .get(&sid)
1927 .map(|s| s.buffers.ids())
1928 .unwrap_or_default();
1929 for bid in preview_buffers {
1930 let Some(&terminal_id) = self.active_window().terminal_buffers.get(&bid) else {
1931 continue;
1932 };
1933 let Some(backing_file) = self
1934 .active_window()
1935 .terminal_backing_files
1936 .get(&terminal_id)
1937 .cloned()
1938 else {
1939 continue;
1940 };
1941 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
1942 if let Ok(mut state) = handle.state.lock() {
1943 if let Ok(metadata) = self.authority.filesystem.metadata(&backing_file) {
1944 state.set_backing_file_history_end(metadata.size);
1945 }
1946 if let Ok(mut file) = self
1947 .authority
1948 .filesystem
1949 .open_file_for_append(&backing_file)
1950 {
1951 use std::io::BufWriter;
1952 let mut writer = BufWriter::new(&mut *file);
1953 if let Err(e) = state.append_visible_screen(&mut writer) {
1954 tracing::error!(
1955 "preview: failed to append visible screen for terminal buffer {bid:?}: {e}"
1956 );
1957 }
1958 }
1959 }
1960 }
1961 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1962 if let Ok(new_state) = crate::state::EditorState::from_file_with_languages(
1963 &backing_file,
1964 self.terminal_width,
1965 self.terminal_height,
1966 large_file_threshold,
1967 &self.grammar_registry,
1968 &self.config.languages,
1969 std::sync::Arc::clone(&self.authority.filesystem),
1970 ) {
1971 if let Some(state) = self
1972 .windows
1973 .get_mut(&sid)
1974 .map(|w| &mut w.buffers)
1975 .expect("preview window present")
1976 .get_mut(&bid)
1977 {
1978 *state = new_state;
1979 state.buffer.set_modified(false);
1980 state.editing_disabled = true;
1981 }
1982 }
1983 }
1984
1985 let __win_for_preview = self.windows.get_mut(&sid).expect("preview window present");
2002 let __preview_metadata = &__win_for_preview.buffer_metadata;
2003 let __preview_event_logs = &mut __win_for_preview.event_logs;
2004 let __preview_composite_buffers = &mut __win_for_preview.composite_buffers;
2005 let __preview_composite_view_states = &mut __win_for_preview.composite_view_states;
2006 let preview_tab_bar_visible = __win_for_preview.tab_bar_visible;
2007
2008 let mut scratch_cell_theme_map: Vec<crate::app::types::CellThemeInfo> = Vec::new();
2012 let mut scratch_pending_cursor: Option<(u16, u16)> = None;
2013 let lsp_waiting = false; let no_grouped_subtrees: std::collections::HashMap<
2015 crate::model::event::LeafId,
2016 crate::view::split::SplitNode,
2017 > = std::collections::HashMap::new();
2018
2019 __win_for_preview
2020 .buffers
2021 .with_all_mut(|preview_buffers, mgr, view_states| {
2022 let _ = crate::view::ui::SplitRenderer::render_content(
2023 frame,
2024 inner,
2025 &*mgr,
2026 preview_buffers,
2027 __preview_metadata,
2028 __preview_event_logs,
2029 __preview_composite_buffers,
2030 __preview_composite_view_states,
2031 theme,
2032 self.ansi_background.as_ref(),
2033 self.background_fade,
2034 lsp_waiting,
2035 self.config.editor.large_file_threshold_bytes,
2036 self.config.editor.line_wrap,
2037 self.config.editor.estimated_line_length,
2038 self.config.editor.highlight_context_bytes,
2039 Some(view_states),
2040 &no_grouped_subtrees,
2041 true, None, None,
2044 None,
2045 false, self.config.editor.relative_line_numbers,
2047 preview_tab_bar_visible,
2048 self.config.editor.use_terminal_bg,
2049 self.session_mode || !self.software_cursor_only,
2050 self.software_cursor_only,
2051 false,
2054 false,
2055 self.config.editor.diagnostics_inline_text,
2056 false, self.config.editor.highlight_current_column,
2058 &mut scratch_cell_theme_map,
2059 inner.width,
2060 &mut scratch_pending_cursor,
2061 );
2062 });
2063 }
2064
2065 fn prepare_overlay_preview(&mut self) {
2066 use crate::input::quick_open::parse_path_line_col;
2067
2068 let (path_str, line, col) = {
2069 let Some(prompt) = self.active_window().prompt.as_ref() else {
2070 return;
2071 };
2072 let Some(idx) = prompt.selected_suggestion else {
2073 return;
2074 };
2075 let Some(s) = prompt.suggestions.get(idx) else {
2076 return;
2077 };
2078 let from_text = parse_path_line_col(&s.text);
2083 if !from_text.0.is_empty() && from_text.1.is_some() {
2084 from_text
2085 } else if let Some(v) = s.value.as_deref() {
2086 parse_path_line_col(v)
2087 } else {
2088 from_text
2089 }
2090 };
2091 if path_str.is_empty() {
2092 return;
2093 }
2094 let line = line.unwrap_or(1).saturating_sub(1);
2095 let col = col.unwrap_or(1).saturating_sub(1);
2096
2097 let path_buf = std::path::PathBuf::from(&path_str);
2099 let abs_path = if path_buf.is_absolute() {
2100 path_buf
2101 } else {
2102 self.working_dir.join(&path_buf)
2103 };
2104 let abs_path = self
2106 .authority
2107 .filesystem
2108 .canonicalize(&abs_path)
2109 .unwrap_or(abs_path);
2110
2111 let already_target = self
2114 .active_window()
2115 .overlay_preview_state
2116 .as_ref()
2117 .is_some_and(|st| {
2118 self.windows
2119 .get(&self.active_window)
2120 .map(|w| &w.buffers)
2121 .expect("active window present")
2122 .get(&st.buffer_id)
2123 .and_then(|s| s.buffer.file_path())
2124 .is_some_and(|p| p == abs_path.as_path())
2125 });
2126
2127 let buffer_id = if already_target {
2128 self.active_window_mut()
2129 .overlay_preview_state
2130 .as_ref()
2131 .unwrap()
2132 .buffer_id
2133 } else {
2134 let was_open = self
2138 .buffers()
2139 .iter()
2140 .any(|(_, s)| s.buffer.file_path() == Some(abs_path.as_path()));
2141 let source_split = self
2146 .windows
2147 .get(&self.active_window)
2148 .and_then(|w| w.buffers.splits())
2149 .map(|(mgr, _)| mgr)
2150 .expect("active window must have a populated split layout")
2151 .active_split();
2152 let buffer_id = match self.open_file_for_preview(abs_path.as_path()) {
2157 Ok(id) => id,
2158 Err(_e) => return,
2159 };
2160 if !was_open {
2161 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2162 meta.hidden_from_tabs = true;
2163 }
2164 let leaf_ids: Vec<_> = self
2170 .windows
2171 .get(&self.active_window)
2172 .and_then(|w| w.buffers.splits())
2173 .map(|(_, vs)| vs)
2174 .expect("active window must have a populated split layout")
2175 .keys()
2176 .copied()
2177 .collect();
2178 for leaf_id in leaf_ids {
2179 if let Some(view_state) = self
2180 .windows
2181 .get_mut(&self.active_window)
2182 .and_then(|w| w.split_view_states_mut())
2183 .expect("active window must have a populated split layout")
2184 .get_mut(&leaf_id)
2185 {
2186 view_state.remove_buffer(buffer_id);
2187 }
2188 }
2189 let preview_loaded: std::collections::HashSet<BufferId> = self
2192 .active_window_mut()
2193 .overlay_preview_state
2194 .as_ref()
2195 .map(|st| st.loaded_buffers.clone())
2196 .unwrap_or_default();
2197 let __active_id = self.active_window;
2198 let __win = self
2199 .windows
2200 .get_mut(&__active_id)
2201 .expect("active window must exist");
2202 let __buffer_keys: Vec<BufferId> = __win.buffers.ids();
2203 let (__mgr, __vs_map) = __win
2204 .buffers
2205 .splits_mut()
2206 .expect("active window must have a populated split layout");
2207 if let Some(source_state) = __vs_map.get_mut(&source_split) {
2208 if source_state.active_buffer == buffer_id {
2209 let fallback = source_state
2210 .open_buffers
2211 .iter()
2212 .find_map(|t| t.as_buffer())
2213 .or_else(|| {
2214 __buffer_keys
2215 .iter()
2216 .copied()
2217 .find(|b| *b != buffer_id && !preview_loaded.contains(b))
2218 });
2219 if let Some(fb) = fallback {
2220 source_state.switch_buffer(fb);
2221 __mgr.set_split_buffer(source_split, fb);
2222 }
2223 }
2224 }
2225 self.windows
2226 .get_mut(&self.active_window)
2227 .and_then(|w| w.split_manager_mut())
2228 .expect("active window must have a populated split layout")
2229 .set_active_split(source_split);
2230 }
2231 buffer_id
2232 };
2233
2234 let need_init = self.active_window_mut().overlay_preview_state.is_none();
2238 if need_init {
2239 let mut view_state = crate::view::split::SplitViewState::with_buffer(
2240 self.terminal_width,
2241 self.terminal_height,
2242 buffer_id,
2243 );
2244 view_state.apply_config_defaults(
2245 self.config.editor.line_numbers,
2246 self.config.editor.highlight_current_line,
2247 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
2248 self.config.editor.wrap_indent,
2249 self.active_window()
2250 .resolve_wrap_column_for_buffer(buffer_id),
2251 self.config.editor.rulers.clone(),
2252 );
2253 let mut loaded_buffers = std::collections::HashSet::new();
2254 if let Some(meta) = self.active_window().buffer_metadata.get(&buffer_id) {
2263 if meta.hidden_from_tabs {
2264 loaded_buffers.insert(buffer_id);
2265 }
2266 }
2267 self.active_window_mut().overlay_preview_state =
2268 Some(crate::app::types::OverlayPreviewState {
2269 buffer_id,
2270 view_state,
2271 loaded_buffers,
2272 });
2273 } else {
2274 let hidden_from_tabs = self
2277 .windows
2278 .get(&self.active_window)
2279 .and_then(|w| w.buffer_metadata.get(&buffer_id))
2280 .is_some_and(|meta| meta.hidden_from_tabs);
2281 if let Some(state) = self.active_window_mut().overlay_preview_state.as_mut() {
2282 if state.buffer_id != buffer_id {
2283 state.view_state.switch_buffer(buffer_id);
2284 if hidden_from_tabs {
2285 state.loaded_buffers.insert(buffer_id);
2286 }
2287 }
2288 }
2289 }
2290
2291 let byte_offset = self
2293 .buffers()
2294 .get(&buffer_id)
2295 .map(|s| {
2296 s.buffer
2297 .position_to_offset(crate::model::piece_tree::Position { line, column: col })
2298 })
2299 .unwrap_or(0);
2300 let line_start = self
2301 .buffers()
2302 .get(&buffer_id)
2303 .and_then(|s| s.buffer.line_start_offset(line))
2304 .unwrap_or(byte_offset);
2305 let h_for_preview = self
2308 .active_window_mut()
2309 .overlay_preview_state
2310 .as_ref()
2311 .map(|s| s.view_state.viewport.height.max(1) as usize)
2312 .unwrap_or(1);
2313 let half = h_for_preview / 2;
2314 let target_top_line = line.saturating_sub(half);
2315 let top_byte = self
2316 .windows
2317 .get(&self.active_window)
2318 .map(|w| &w.buffers)
2319 .expect("active window present")
2320 .get(&buffer_id)
2321 .and_then(|s| s.buffer.line_start_offset(target_top_line))
2322 .unwrap_or(line_start);
2323 if let Some(state) = self.active_window_mut().overlay_preview_state.as_mut() {
2324 state.view_state.cursors.primary_mut().position = byte_offset;
2325 state.view_state.viewport.top_byte = top_byte;
2326 }
2327 }
2328
2329 fn render_overlay_prompt(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
2346 use ratatui::layout::Rect;
2347 use ratatui::style::{Modifier, Style};
2348 use ratatui::text::{Line, Span};
2349 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2350
2351 let overlay_rect = Self::centered_overlay_rect(area, 80, 80);
2354
2355 let theme = self.theme.read().unwrap().clone();
2357 let toolbar_visible = self
2373 .active_window()
2374 .prompt
2375 .as_ref()
2376 .map(|p| !p.title.is_empty())
2377 .unwrap_or(false);
2378 let chrome_rows: usize = 4 + if toolbar_visible { 1 } else { 0 };
2379 let suggestions_visible_rows = (overlay_rect.height as usize).saturating_sub(chrome_rows);
2380 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2381 prompt.ensure_selected_visible_within(suggestions_visible_rows);
2382 }
2383 let Some(prompt) = self.active_window().prompt.as_ref() else {
2384 return;
2385 };
2386 let prompt = prompt.clone();
2387
2388 crate::view::dimming::apply_dimming_excluding(frame, frame.area(), Some(overlay_rect));
2393
2394 frame.render_widget(Clear, overlay_rect);
2400 let default_title: Vec<fresh_core::api::StyledText> = {
2401 use crate::input::keybindings::KeyContext;
2410 use fresh_core::api::{OverlayColorSpec, OverlayOptions, StyledText};
2411 let keybindings = self.keybindings.read().unwrap();
2412 let mut hints: Vec<(String, &str)> = Vec::new();
2413 if let Some(k) = keybindings
2414 .find_keybinding_for_action("cycle_live_grep_provider", KeyContext::Prompt)
2415 {
2416 hints.push((k, "switch grep provider"));
2417 }
2418 if let Some(k) = keybindings
2419 .find_keybinding_for_action("live_grep_export_quickfix", KeyContext::Prompt)
2420 {
2421 hints.push((k, "save matches"));
2422 }
2423 if hints.is_empty() {
2424 Vec::new()
2425 } else {
2426 let hint_style = Some(OverlayOptions {
2427 fg: Some(OverlayColorSpec::ThemeKey("ui.help_key_fg".into())),
2428 ..OverlayOptions::default()
2429 });
2430 let sep_style = Some(OverlayOptions {
2431 fg: Some(OverlayColorSpec::ThemeKey("ui.popup_border_fg".into())),
2432 ..OverlayOptions::default()
2433 });
2434 let mut segs: Vec<StyledText> = Vec::new();
2435 for (i, (k, verb)) in hints.into_iter().enumerate() {
2436 if i > 0 {
2437 segs.push(StyledText {
2438 text: " · ".into(),
2439 style: sep_style.clone(),
2440 });
2441 }
2442 segs.push(StyledText {
2443 text: k,
2444 style: hint_style.clone(),
2445 });
2446 segs.push(StyledText {
2447 text: format!(" {verb}"),
2448 style: None,
2449 });
2450 }
2451 segs
2452 }
2453 };
2454 let title_segs: &[fresh_core::api::StyledText] = if prompt.title.is_empty() {
2455 &default_title
2456 } else {
2457 &prompt.title
2458 };
2459 let normal_title_style = Style::default()
2460 .fg(theme.prompt_fg)
2461 .add_modifier(Modifier::BOLD);
2462 let title_spans: Vec<Span> = title_segs
2463 .iter()
2464 .map(|seg| {
2465 let style = match &seg.style {
2466 Some(opts) => Self::resolve_overlay_style(opts, &theme),
2467 None => normal_title_style,
2468 };
2469 Span::styled(seg.text.clone(), style)
2470 })
2471 .collect();
2472 let block = Block::default()
2473 .borders(Borders::ALL)
2474 .border_style(Style::default().fg(theme.popup_border_fg))
2475 .style(Style::default().bg(theme.suggestion_bg));
2476 let inner = block.inner(overlay_rect);
2477 frame.render_widget(block, overlay_rect);
2478
2479 if inner.height == 0 || inner.width == 0 {
2480 return;
2481 }
2482
2483 let preview_min_cols: u16 = 120;
2487 let show_preview = overlay_rect.width >= preview_min_cols;
2488 let (results_area, preview_area) = if show_preview {
2489 let results_w = inner.width / 2;
2490 (
2491 Rect {
2492 x: inner.x,
2493 y: inner.y,
2494 width: results_w,
2495 height: inner.height,
2496 },
2497 Some(Rect {
2498 x: inner.x + results_w,
2499 y: inner.y,
2500 width: inner.width - results_w,
2501 height: inner.height,
2502 }),
2503 )
2504 } else {
2505 (inner, None)
2506 };
2507
2508 let input_row = Rect {
2510 x: results_area.x,
2511 y: results_area.y,
2512 width: results_area.width,
2513 height: 1,
2514 };
2515 let title_style = Style::default().fg(theme.prompt_fg).bg(theme.suggestion_bg);
2522 let input_style = Style::default().fg(theme.prompt_fg).bg(theme.editor_bg);
2523 let count_str = if prompt.suggestions.is_empty() {
2524 String::new()
2525 } else {
2526 format!(
2527 "{} / {}",
2528 prompt.selected_suggestion.map(|i| i + 1).unwrap_or(0),
2529 prompt.suggestions.len()
2530 )
2531 };
2532 use crate::primitives::display_width::str_width;
2533 let count_w = str_width(&count_str);
2534 let right_gap: usize = if count_w > 0 { 1 } else { 0 };
2537 let visible_input_width = (results_area.width as usize).saturating_sub(count_w + right_gap);
2538 let truncated_input: String = prompt
2539 .input
2540 .chars()
2541 .take(visible_input_width.saturating_sub(str_width(&prompt.message)))
2542 .collect();
2543 let used = str_width(&prompt.message) + str_width(&truncated_input) + count_w;
2547 let pad = (results_area.width as usize).saturating_sub(used + right_gap);
2548 let line = Line::from(vec![
2549 Span::styled(prompt.message.clone(), title_style),
2550 Span::styled(truncated_input, input_style),
2551 Span::styled(" ".repeat(pad), input_style),
2552 Span::styled(
2553 count_str,
2554 Style::default()
2555 .fg(theme.popup_border_fg)
2556 .bg(theme.editor_bg),
2557 ),
2558 ]);
2559 frame.render_widget(Paragraph::new(line).style(input_style), input_row);
2560
2561 let cursor_x = (str_width(&prompt.message)
2563 + str_width(&prompt.input[..prompt.cursor_pos.min(prompt.input.len())]))
2564 as u16;
2565 if cursor_x < input_row.width {
2566 frame.set_cursor_position((input_row.x + cursor_x, input_row.y));
2567 }
2568
2569 let toolbar_h: u16 = if toolbar_visible { 1 } else { 0 };
2576 if toolbar_visible && results_area.height >= 2 {
2577 let toolbar = Rect {
2578 x: results_area.x,
2579 y: results_area.y + 1,
2580 width: results_area.width,
2581 height: 1,
2582 };
2583 frame.render_widget(
2584 Paragraph::new(Line::from(title_spans))
2585 .style(Style::default().bg(theme.suggestion_bg)),
2586 toolbar,
2587 );
2588 }
2589
2590 if results_area.height >= 2 + toolbar_h {
2592 let sep = Rect {
2593 x: results_area.x,
2594 y: results_area.y + 1 + toolbar_h,
2595 width: results_area.width,
2596 height: 1,
2597 };
2598 let sep_style = Style::default()
2599 .fg(theme.popup_border_fg)
2600 .bg(theme.suggestion_bg);
2601 let sep_text = "─".repeat(results_area.width as usize);
2602 frame.render_widget(Paragraph::new(sep_text).style(sep_style), sep);
2603 }
2604
2605 let chrome_above_list: u16 = 2 + toolbar_h;
2613 let footer_h: u16 = if prompt.footer.is_empty() { 0 } else { 1 };
2619 if results_area.height > chrome_above_list + footer_h {
2620 let inner_rows = (results_area.height - chrome_above_list - footer_h) as usize;
2624 let needs_scrollbar = prompt.suggestions.len() > inner_rows.max(1);
2625 let scrollbar_w: u16 = if needs_scrollbar { 1 } else { 0 };
2626 let list_area = Rect {
2627 x: results_area.x,
2628 y: results_area.y + chrome_above_list,
2629 width: results_area.width.saturating_sub(scrollbar_w),
2630 height: results_area.height - chrome_above_list - footer_h,
2631 };
2632 self.active_chrome_mut().suggestions_area = SuggestionsRenderer::render_with_hover(
2633 frame,
2634 list_area,
2635 &prompt,
2636 &theme,
2637 self.active_window_mut().mouse_state.hover_target.as_ref(),
2638 false,
2639 );
2640 if self.active_chrome_mut().suggestions_area.is_some() {
2641 self.active_chrome_mut().suggestions_outer_area = Some(list_area);
2642 }
2643 if needs_scrollbar {
2648 use crate::view::ui::scrollbar::{
2649 render_scrollbar, ScrollbarColors, ScrollbarState,
2650 };
2651 let scrollbar_rect = Rect {
2655 x: results_area.x + results_area.width - 1,
2656 y: list_area.y,
2657 width: 1,
2658 height: list_area.height,
2659 };
2660 let state = ScrollbarState::new(
2661 prompt.suggestions.len(),
2662 inner_rows.max(1),
2663 prompt.scroll_offset,
2664 );
2665 render_scrollbar(
2666 frame,
2667 scrollbar_rect,
2668 &state,
2669 &ScrollbarColors::from_theme(&theme),
2670 );
2671 self.active_chrome_mut().suggestions_scrollbar_rect = Some(scrollbar_rect);
2674 } else {
2675 self.active_chrome_mut().suggestions_scrollbar_rect = None;
2676 }
2677 } else {
2678 self.active_chrome_mut().suggestions_scrollbar_rect = None;
2679 }
2680
2681 if footer_h == 1 && results_area.height >= 1 {
2687 let footer_row = Rect {
2688 x: results_area.x,
2689 y: results_area.y + results_area.height - 1,
2690 width: results_area.width,
2691 height: 1,
2692 };
2693 let footer_default_style = Style::default().fg(theme.prompt_fg).bg(theme.suggestion_bg);
2694 let footer_spans: Vec<Span> = prompt
2695 .footer
2696 .iter()
2697 .map(|seg| {
2698 let style = match &seg.style {
2699 Some(opts) => Self::resolve_overlay_style(opts, &theme),
2700 None => footer_default_style,
2701 };
2702 Span::styled(seg.text.clone(), style)
2703 })
2704 .collect();
2705 frame.render_widget(
2706 Paragraph::new(Line::from(footer_spans))
2707 .style(Style::default().bg(theme.suggestion_bg)),
2708 footer_row,
2709 );
2710 }
2711
2712 if let Some(preview_rect) = preview_area {
2719 use ratatui::widgets::{Block, Borders, Clear};
2722 frame.render_widget(Clear, preview_rect);
2723 let block = Block::default()
2724 .borders(Borders::LEFT)
2725 .border_style(Style::default().fg(theme.popup_border_fg))
2726 .style(Style::default().bg(theme.suggestion_bg));
2727 let inner = block.inner(preview_rect);
2728 frame.render_widget(block, preview_rect);
2729
2730 if inner.height > 0
2737 && inner.width > 0
2738 && self
2739 .preview_window_id
2740 .is_some_and(|sid| sid != self.active_window && self.windows.contains_key(&sid))
2741 {
2742 self.render_session_preview_into_rect(frame, inner, &theme);
2743 } else if inner.height > 0 && inner.width > 0 {
2744 let bg_fade = self.background_fade;
2751 let estimated_line_length = self.config.editor.estimated_line_length;
2752 let highlight_context_bytes = self.config.editor.highlight_context_bytes;
2753 let relative_line_numbers = self.config.editor.relative_line_numbers;
2754 let use_terminal_bg = self.config.editor.use_terminal_bg;
2755 let session_mode = self.session_mode || !self.software_cursor_only;
2756 let software_cursor_only = self.software_cursor_only;
2757 let diagnostics_inline_text = self.config.editor.diagnostics_inline_text;
2758 let show_tilde = false; let highlight_current_column = self.config.editor.highlight_current_column;
2760 let screen_width = frame.area().width;
2761
2762 let ansi_ref = self.ansi_background.as_ref();
2763 let __win = self
2764 .windows
2765 .get_mut(&self.active_window)
2766 .expect("active window present");
2767 let buffers = &mut __win.buffers;
2768 let event_logs = &mut __win.event_logs;
2769 let cell_theme_map = &mut __win.chrome_layout.cell_theme_map;
2770 let Some(preview_state) = __win.overlay_preview_state.as_mut() else {
2771 return;
2772 };
2773 preview_state
2774 .view_state
2775 .viewport
2776 .resize(inner.width, inner.height);
2777 let buffer_id = preview_state.buffer_id;
2778
2779 if let Some(state) = buffers.get_mut(&buffer_id) {
2780 let buf_state = preview_state.view_state.active_state_mut();
2785 let cursors = buf_state.cursors.clone();
2786 let view_mode = buf_state.view_mode.clone();
2787 let compose_width = buf_state.compose_width;
2788 let compose_column_guides = buf_state.compose_column_guides.clone();
2789 let view_transform = buf_state.view_transform.clone();
2790 let rulers = buf_state.rulers.clone();
2791 let show_line_numbers = buf_state.show_line_numbers;
2792 let highlight_current_line = buf_state.highlight_current_line;
2793 let viewport_ref = &mut buf_state.viewport;
2794 let folds_ref = &mut buf_state.folds;
2795 let event_log = event_logs.get_mut(&buffer_id);
2796 let _ = crate::view::ui::SplitRenderer::render_phantom_leaf(
2797 frame,
2798 state,
2799 &cursors,
2800 viewport_ref,
2801 folds_ref,
2802 event_log,
2803 inner,
2804 &theme,
2805 ansi_ref,
2806 bg_fade,
2807 view_mode,
2808 compose_width,
2809 compose_column_guides,
2810 view_transform,
2811 estimated_line_length,
2812 highlight_context_bytes,
2813 buffer_id,
2814 relative_line_numbers,
2815 use_terminal_bg,
2816 session_mode,
2817 software_cursor_only,
2818 &rulers,
2819 show_line_numbers,
2820 highlight_current_line,
2821 diagnostics_inline_text,
2822 show_tilde,
2823 highlight_current_column,
2824 cell_theme_map,
2825 screen_width,
2826 );
2827 }
2828 }
2829 }
2830 }
2831
2832 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
2834 use ratatui::style::Style;
2835 use ratatui::text::Span;
2836 use ratatui::widgets::Paragraph;
2837
2838 match &self.active_window().mouse_state.hover_target {
2839 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
2840 for (sid, dir, x, y, length) in &self.active_layout().separator_areas {
2842 if sid == split_id && dir == direction {
2843 let hover_style = Style::default().fg(self
2844 .theme
2845 .read()
2846 .unwrap()
2847 .split_separator_hover_fg);
2848 match dir {
2849 SplitDirection::Horizontal => {
2850 let line_text = "─".repeat(*length as usize);
2851 let paragraph =
2852 Paragraph::new(Span::styled(line_text, hover_style));
2853 frame.render_widget(
2854 paragraph,
2855 ratatui::layout::Rect::new(*x, *y, *length, 1),
2856 );
2857 }
2858 SplitDirection::Vertical => {
2859 for offset in 0..*length {
2860 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2861 frame.render_widget(
2862 paragraph,
2863 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
2864 );
2865 }
2866 }
2867 }
2868 }
2869 }
2870 }
2871 Some(HoverTarget::ScrollbarThumb(split_id)) => {
2872 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
2874 &self.active_layout().split_areas
2875 {
2876 if sid == split_id {
2877 let hover_style = Style::default().bg(self
2878 .theme
2879 .read()
2880 .unwrap()
2881 .scrollbar_thumb_hover_fg);
2882 for row_offset in *thumb_start..*thumb_end {
2883 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
2884 frame.render_widget(
2885 paragraph,
2886 ratatui::layout::Rect::new(
2887 scrollbar_rect.x,
2888 scrollbar_rect.y + row_offset as u16,
2889 1,
2890 1,
2891 ),
2892 );
2893 }
2894 }
2895 }
2896 }
2897 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
2898 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2900 &self.active_layout().split_areas
2901 {
2902 if sid == split_id {
2903 let track_hover_style = Style::default().bg(self
2904 .theme
2905 .read()
2906 .unwrap()
2907 .scrollbar_track_hover_fg);
2908 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
2909 frame.render_widget(
2910 paragraph,
2911 ratatui::layout::Rect::new(
2912 scrollbar_rect.x,
2913 scrollbar_rect.y + hovered_row,
2914 1,
2915 1,
2916 ),
2917 );
2918 }
2919 }
2920 }
2921 Some(HoverTarget::FileExplorerBorder) => {
2922 if let Some(explorer_area) = self.active_layout().file_explorer_area {
2924 let hover_style =
2925 Style::default().fg(self.theme.read().unwrap().split_separator_hover_fg);
2926 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
2927 for row_offset in 0..explorer_area.height {
2928 let paragraph = Paragraph::new(Span::styled("│", hover_style));
2929 frame.render_widget(
2930 paragraph,
2931 ratatui::layout::Rect::new(
2932 border_x,
2933 explorer_area.y + row_offset,
2934 1,
2935 1,
2936 ),
2937 );
2938 }
2939 }
2940 }
2941 _ => {}
2943 }
2944 }
2945
2946 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
2948 use ratatui::style::Style;
2949 use ratatui::text::{Line, Span};
2950 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
2951
2952 let items = super::types::TabContextMenuItem::all();
2953 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
2958 let screen_height = frame.area().height;
2959
2960 let menu_x = if menu.position.0 + menu_width > screen_width {
2961 screen_width.saturating_sub(menu_width)
2962 } else {
2963 menu.position.0
2964 };
2965
2966 let menu_y = if menu.position.1 + menu_height > screen_height {
2967 screen_height.saturating_sub(menu_height)
2968 } else {
2969 menu.position.1
2970 };
2971
2972 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
2973
2974 frame.render_widget(Clear, area);
2976
2977 let mut lines = Vec::new();
2979 for (idx, item) in items.iter().enumerate() {
2980 let is_highlighted = idx == menu.highlighted;
2981
2982 let style = if is_highlighted {
2983 Style::default()
2984 .fg(self.theme.read().unwrap().menu_highlight_fg)
2985 .bg(self.theme.read().unwrap().menu_highlight_bg)
2986 } else {
2987 Style::default()
2988 .fg(self.theme.read().unwrap().menu_dropdown_fg)
2989 .bg(self.theme.read().unwrap().menu_dropdown_bg)
2990 };
2991
2992 let label = item.label();
2994 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
2996
2997 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
2998 }
2999
3000 let block = Block::default()
3001 .borders(Borders::ALL)
3002 .border_style(Style::default().fg(self.theme.read().unwrap().menu_border_fg))
3003 .style(Style::default().bg(self.theme.read().unwrap().menu_dropdown_bg));
3004
3005 let paragraph = Paragraph::new(lines).block(block);
3006 frame.render_widget(paragraph, area);
3007 }
3008
3009 fn render_file_explorer_context_menu(
3011 &self,
3012 frame: &mut Frame,
3013 menu: &super::types::FileExplorerContextMenu,
3014 ) {
3015 use ratatui::style::Style;
3016 use ratatui::text::{Line, Span};
3017 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
3018
3019 let items = menu.items();
3020 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3021 let menu_height = menu.height();
3022 let (menu_x, menu_y) = menu.clamped_position(frame.area().width, frame.area().height);
3023
3024 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
3025
3026 frame.render_widget(Clear, area);
3027
3028 let mut lines = Vec::new();
3029 for (idx, item) in items.iter().enumerate() {
3030 let is_highlighted = idx == menu.highlighted;
3031
3032 let style = if is_highlighted {
3033 Style::default()
3034 .fg(self.theme.read().unwrap().menu_highlight_fg)
3035 .bg(self.theme.read().unwrap().menu_highlight_bg)
3036 } else {
3037 Style::default()
3038 .fg(self.theme.read().unwrap().menu_dropdown_fg)
3039 .bg(self.theme.read().unwrap().menu_dropdown_bg)
3040 };
3041
3042 let label = item.label();
3043 let content_width = (menu_width as usize).saturating_sub(2);
3044 let padded_label = format!(" {:<width$}", label, width = content_width - 1);
3045
3046 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
3047 }
3048
3049 let block = Block::default()
3050 .borders(Borders::ALL)
3051 .border_style(Style::default().fg(self.theme.read().unwrap().menu_border_fg))
3052 .style(Style::default().bg(self.theme.read().unwrap().menu_dropdown_bg));
3053
3054 let paragraph = Paragraph::new(lines).block(block);
3055 frame.render_widget(paragraph, area);
3056 }
3057
3058 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
3060 use ratatui::style::Modifier;
3061
3062 let Some(ref drop_zone) = drag_state.drop_zone else {
3063 return;
3064 };
3065
3066 let split_id = drop_zone.split_id();
3067
3068 let split_area = self
3070 .active_layout()
3071 .split_areas
3072 .iter()
3073 .find(|(sid, _, _, _, _, _)| *sid == split_id)
3074 .map(|(_, _, content_rect, _, _, _)| *content_rect);
3075
3076 let Some(content_rect) = split_area else {
3077 return;
3078 };
3079
3080 use super::types::TabDropZone;
3082
3083 let highlight_area = match drop_zone {
3084 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
3085 content_rect
3088 }
3089 TabDropZone::SplitLeft(_) => {
3090 let width = (content_rect.width / 2).max(3);
3092 ratatui::layout::Rect::new(
3093 content_rect.x,
3094 content_rect.y,
3095 width,
3096 content_rect.height,
3097 )
3098 }
3099 TabDropZone::SplitRight(_) => {
3100 let width = (content_rect.width / 2).max(3);
3102 let x = content_rect.x + content_rect.width - width;
3103 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
3104 }
3105 TabDropZone::SplitTop(_) => {
3106 let height = (content_rect.height / 2).max(2);
3108 ratatui::layout::Rect::new(
3109 content_rect.x,
3110 content_rect.y,
3111 content_rect.width,
3112 height,
3113 )
3114 }
3115 TabDropZone::SplitBottom(_) => {
3116 let height = (content_rect.height / 2).max(2);
3118 let y = content_rect.y + content_rect.height - height;
3119 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
3120 }
3121 };
3122
3123 let buf = frame.buffer_mut();
3126 let drop_zone_bg = self.theme.read().unwrap().tab_drop_zone_bg;
3127 let drop_zone_border = self.theme.read().unwrap().tab_drop_zone_border;
3128
3129 for y in highlight_area.y..highlight_area.y + highlight_area.height {
3131 for x in highlight_area.x..highlight_area.x + highlight_area.width {
3132 if let Some(cell) = buf.cell_mut((x, y)) {
3133 cell.set_bg(drop_zone_bg);
3136
3137 let is_border = x == highlight_area.x
3139 || x == highlight_area.x + highlight_area.width - 1
3140 || y == highlight_area.y
3141 || y == highlight_area.y + highlight_area.height - 1;
3142
3143 if is_border {
3144 cell.set_fg(drop_zone_border);
3145 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
3146 }
3147 }
3148 }
3149 }
3150
3151 match drop_zone {
3153 TabDropZone::SplitLeft(_) => {
3154 for y in highlight_area.y..highlight_area.y + highlight_area.height {
3156 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
3157 cell.set_symbol("▌");
3158 cell.set_fg(drop_zone_border);
3159 }
3160 }
3161 }
3162 TabDropZone::SplitRight(_) => {
3163 let x = highlight_area.x + highlight_area.width - 1;
3165 for y in highlight_area.y..highlight_area.y + highlight_area.height {
3166 if let Some(cell) = buf.cell_mut((x, y)) {
3167 cell.set_symbol("▐");
3168 cell.set_fg(drop_zone_border);
3169 }
3170 }
3171 }
3172 TabDropZone::SplitTop(_) => {
3173 for x in highlight_area.x..highlight_area.x + highlight_area.width {
3175 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
3176 cell.set_symbol("▀");
3177 cell.set_fg(drop_zone_border);
3178 }
3179 }
3180 }
3181 TabDropZone::SplitBottom(_) => {
3182 let y = highlight_area.y + highlight_area.height - 1;
3184 for x in highlight_area.x..highlight_area.x + highlight_area.width {
3185 if let Some(cell) = buf.cell_mut((x, y)) {
3186 cell.set_symbol("▄");
3187 cell.set_fg(drop_zone_border);
3188 }
3189 }
3190 }
3191 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
3192 }
3194 }
3195 }
3196
3197 pub fn recompute_layout(&mut self, width: u16, height: u16) {
3202 let size = ratatui::layout::Rect::new(0, 0, width, height);
3203
3204 let active_split = self
3206 .windows
3207 .get(&self.active_window)
3208 .and_then(|w| w.buffers.splits())
3209 .map(|(mgr, _)| mgr)
3210 .expect("active window must have a populated split layout")
3211 .active_split();
3212 self.active_window_mut()
3213 .pre_sync_ensure_visible(active_split);
3214 self.active_window_mut().sync_scroll_groups();
3215
3216 let constraints = vec![
3219 Constraint::Length(if self.active_window_mut().menu_bar_visible {
3220 1
3221 } else {
3222 0
3223 }),
3224 Constraint::Min(0),
3225 Constraint::Length(if self.active_window_mut().status_bar_visible {
3226 1
3227 } else {
3228 0
3229 }), Constraint::Length(0), Constraint::Length(if self.active_window_mut().prompt_line_visible {
3232 1
3233 } else {
3234 0
3235 }), ];
3237 let main_chunks = Layout::default()
3238 .direction(Direction::Vertical)
3239 .constraints(constraints)
3240 .split(size);
3241 let main_content_area = main_chunks[1];
3242
3243 let file_explorer_should_show = self.file_explorer_visible()
3245 && (self.file_explorer().is_some()
3246 || self.active_window().file_explorer_sync_in_progress);
3247 let editor_content_area = if file_explorer_should_show {
3248 let explorer_cols = self
3249 .active_window()
3250 .file_explorer_width
3251 .to_cols(main_content_area.width);
3252 let horizontal_chunks = Layout::default()
3253 .direction(Direction::Horizontal)
3254 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
3255 .split(main_content_area);
3256 horizontal_chunks[1]
3257 } else {
3258 main_content_area
3259 };
3260
3261 let active_window_id = self.active_window;
3266 let __win_l = self
3267 .windows
3268 .get_mut(&active_window_id)
3269 .expect("active window must exist");
3270 let tab_bar_visible = __win_l.tab_bar_visible;
3271 let theme = self.theme.read().unwrap().clone();
3272 let view_line_mappings = __win_l
3273 .buffers
3274 .with_all_mut(|buffers, mgr, vs_map| {
3275 SplitRenderer::compute_content_layout(
3276 editor_content_area,
3277 &*mgr,
3278 buffers,
3279 vs_map,
3280 &theme,
3281 false, self.config.editor.estimated_line_length,
3283 self.config.editor.highlight_context_bytes,
3284 self.config.editor.relative_line_numbers,
3285 self.config.editor.use_terminal_bg,
3286 self.session_mode || !self.software_cursor_only,
3287 self.software_cursor_only,
3288 tab_bar_visible,
3289 self.config.editor.show_vertical_scrollbar,
3290 self.config.editor.show_horizontal_scrollbar,
3291 self.config.editor.diagnostics_inline_text,
3292 self.config.editor.show_tilde,
3293 )
3294 })
3295 .expect("active window must have a populated split layout");
3296
3297 self.active_layout_mut().view_line_mappings = view_line_mappings;
3298 }
3299
3300 pub fn clear_search_history(&mut self) {
3303 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("search") {
3304 history.clear();
3305 }
3306 }
3307
3308 fn update_terminal_title(&mut self, display_name: &str) {
3316 if !self.config.editor.set_window_title {
3317 return;
3318 }
3319 let project_name = self.working_dir.file_name().and_then(|s| s.to_str());
3320 let new_title =
3321 crate::services::terminal_title::build_window_title(display_name, project_name);
3322 if self.last_window_title.as_deref() == Some(new_title.as_str()) {
3323 return;
3324 }
3325 crate::services::terminal_title::write_terminal_title(&new_title);
3326 self.last_window_title = Some(new_title);
3327 }
3328
3329 pub fn save_histories(&self) {
3332 if let Err(e) = self
3334 .authority
3335 .filesystem
3336 .create_dir_all(&self.dir_context.data_dir)
3337 {
3338 tracing::warn!("Failed to create data directory: {}", e);
3339 return;
3340 }
3341
3342 for (key, history) in &self.active_window().prompt_histories {
3344 let path = self.dir_context.prompt_history_path(key);
3345 if let Err(e) = history.save_to_file(&path) {
3346 tracing::warn!("Failed to save {} history: {}", key, e);
3347 } else {
3348 tracing::debug!("Saved {} history to {:?}", key, path);
3349 }
3350 }
3351 }
3352
3353 pub(super) fn centered_overlay_rect(
3365 area: ratatui::layout::Rect,
3366 width_pct: u8,
3367 height_pct: u8,
3368 ) -> ratatui::layout::Rect {
3369 let w_pct = width_pct.clamp(1, 100) as u32;
3370 let h_pct = height_pct.clamp(1, 100) as u32;
3371 let w = ((area.width as u32 * w_pct) / 100) as u16;
3372 let h = ((area.height as u32 * h_pct) / 100) as u16;
3373 let w = w.max(20).min(area.width);
3374 let h = h.max(8).min(area.height);
3375 ratatui::layout::Rect {
3376 x: area.x + (area.width.saturating_sub(w)) / 2,
3377 y: area.y + (area.height.saturating_sub(h)) / 2,
3378 width: w,
3379 height: h,
3380 }
3381 }
3382
3383 pub(super) fn render_floating_widget_panel(
3390 &mut self,
3391 frame: &mut Frame,
3392 area: ratatui::layout::Rect,
3393 ) {
3394 use ratatui::widgets::{Block, Borders, Clear};
3395
3396 let (width_pct, height_pct, entries, focus_cursor, embeds) =
3397 match self.floating_widget_panel.as_ref() {
3398 Some(fwp) => (
3399 fwp.width_pct,
3400 fwp.height_pct,
3401 fwp.entries.clone(),
3402 fwp.focus_cursor,
3403 fwp.embeds.clone(),
3404 ),
3405 None => return,
3406 };
3407 let theme = self.theme.read().unwrap().clone();
3408 let overlay_rect = Self::centered_overlay_rect(area, width_pct, height_pct);
3409
3410 crate::view::dimming::apply_dimming_excluding(frame, area, Some(overlay_rect));
3411 frame.render_widget(Clear, overlay_rect);
3412 let block = Block::default()
3413 .borders(Borders::ALL)
3414 .border_style(ratatui::style::Style::default().fg(theme.popup_border_fg))
3415 .style(ratatui::style::Style::default().bg(theme.suggestion_bg));
3416 let inner = block.inner(overlay_rect);
3417 frame.render_widget(block, overlay_rect);
3418
3419 if inner.width == 0 || inner.height == 0 {
3420 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3421 fwp.last_inner_rect = Some(inner);
3422 }
3423 return;
3424 }
3425
3426 let max_rows = inner.height as usize;
3427 for (i, entry) in entries.iter().take(max_rows).enumerate() {
3428 paint_text_property_entry(
3429 frame,
3430 entry,
3431 inner.x,
3432 inner.y + i as u16,
3433 inner.width,
3434 &theme,
3435 );
3436 }
3437
3438 let saved_preview = self.preview_window_id;
3445 for emb in &embeds {
3446 if emb.window_id == 0 {
3447 continue;
3448 }
3449 let ex = inner.x.saturating_add(emb.col_in_row as u16);
3450 let ey = inner.y.saturating_add(emb.buffer_row as u16);
3451 let max_w = inner.x.saturating_add(inner.width).saturating_sub(ex);
3455 let max_h = inner.y.saturating_add(inner.height).saturating_sub(ey);
3456 let w = (emb.width_cols as u16).min(max_w);
3457 let h = (emb.height_rows as u16).min(max_h);
3458 if w == 0 || h == 0 {
3459 continue;
3460 }
3461 let rect = ratatui::layout::Rect {
3462 x: ex,
3463 y: ey,
3464 width: w,
3465 height: h,
3466 };
3467 self.preview_window_id = Some(fresh_core::WindowId(emb.window_id as u64));
3468 self.render_session_preview_into_rect(frame, rect, &theme);
3469 }
3470 self.preview_window_id = saved_preview;
3471
3472 if let Some(fc) = focus_cursor {
3473 let cx = inner.x.saturating_add(byte_to_screen_col(
3474 entries
3475 .get(fc.buffer_row as usize)
3476 .map(|e| e.text.as_str())
3477 .unwrap_or(""),
3478 fc.byte_in_row as usize,
3479 ) as u16);
3480 let cy = inner.y.saturating_add(fc.buffer_row as u16);
3481 if cx < inner.x + inner.width && cy < inner.y + inner.height {
3482 frame.set_cursor_position((cx, cy));
3483 }
3484 }
3485
3486 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3487 fwp.last_inner_rect = Some(inner);
3488 }
3489 }
3490
3491 fn resolve_overlay_style(
3492 opts: &fresh_core::api::OverlayOptions,
3493 theme: &crate::view::theme::Theme,
3494 ) -> ratatui::style::Style {
3495 use crate::view::theme::named_color_from_str;
3496 use fresh_core::api::OverlayColorSpec;
3497 use ratatui::style::{Color, Modifier, Style};
3498
3499 let resolve = |spec: &OverlayColorSpec| -> Option<Color> {
3500 match spec {
3501 OverlayColorSpec::Rgb(r, g, b) => Some(Color::Rgb(*r, *g, *b)),
3502 OverlayColorSpec::ThemeKey(k) => {
3503 named_color_from_str(k).or_else(|| theme.resolve_theme_key(k))
3504 }
3505 }
3506 };
3507
3508 let mut style = Style::default();
3509 if let Some(ref fg) = opts.fg {
3510 if let Some(c) = resolve(fg) {
3511 style = style.fg(c);
3512 }
3513 }
3514 if let Some(ref bg) = opts.bg {
3515 if let Some(c) = resolve(bg) {
3516 style = style.bg(c);
3517 }
3518 }
3519 let mut m = Modifier::empty();
3520 if opts.bold {
3521 m |= Modifier::BOLD;
3522 }
3523 if opts.italic {
3524 m |= Modifier::ITALIC;
3525 }
3526 if opts.underline {
3527 m |= Modifier::UNDERLINED;
3528 }
3529 if opts.strikethrough {
3530 m |= Modifier::CROSSED_OUT;
3531 }
3532 if !m.is_empty() {
3533 style = style.add_modifier(m);
3534 }
3535 style
3536 }
3537}
3538
3539fn paint_text_property_entry(
3545 frame: &mut ratatui::Frame,
3546 entry: &fresh_core::text_property::TextPropertyEntry,
3547 x: u16,
3548 y: u16,
3549 width: u16,
3550 theme: &crate::view::theme::Theme,
3551) {
3552 use ratatui::style::Style;
3553 use ratatui::text::{Line, Span};
3554 use ratatui::widgets::Paragraph;
3555
3556 let mut normalized = entry.clone();
3557 normalized.normalize_widths();
3558 let mut text = normalized.text.clone();
3559 while text.ends_with('\n') {
3560 text.pop();
3561 }
3562
3563 let base_bg = theme.suggestion_bg;
3564 let base_style = if let Some(opts) = normalized.style.as_ref() {
3565 Editor::resolve_overlay_style(opts, theme).bg(base_bg)
3566 } else {
3567 Style::default().bg(base_bg)
3568 };
3569
3570 let boundaries: std::collections::BTreeSet<usize> = std::iter::once(0)
3575 .chain(std::iter::once(text.len()))
3576 .chain(
3577 normalized
3578 .inline_overlays
3579 .iter()
3580 .flat_map(|o| [o.start.min(text.len()), o.end.min(text.len())]),
3581 )
3582 .collect();
3583 let bounds: Vec<usize> = boundaries.into_iter().collect();
3584
3585 let mut spans: Vec<Span<'_>> = Vec::new();
3586 for win in bounds.windows(2) {
3587 let (a, b) = (win[0], win[1]);
3588 if a >= b {
3589 continue;
3590 }
3591 let slice = text[a..b].to_string();
3592 let mut style = base_style;
3593 for o in &normalized.inline_overlays {
3594 let os = o.start.min(text.len());
3595 let oe = o.end.min(text.len());
3596 if a >= os && b <= oe && oe > os {
3597 style = Editor::resolve_overlay_style(&o.style, theme).bg(
3598 Editor::resolve_overlay_style(&o.style, theme)
3599 .bg
3600 .unwrap_or(base_bg),
3601 );
3602 }
3603 }
3604 spans.push(Span::styled(slice, style));
3605 }
3606
3607 let line = Line::from(spans);
3608 let rect = ratatui::layout::Rect {
3609 x,
3610 y,
3611 width,
3612 height: 1,
3613 };
3614 frame.render_widget(Paragraph::new(line).style(base_style), rect);
3615}
3616
3617fn byte_to_screen_col(text: &str, target_byte: usize) -> usize {
3622 use unicode_width::UnicodeWidthChar;
3623 let mut byte = 0;
3624 let mut col = 0usize;
3625 for ch in text.chars() {
3626 if byte >= target_byte {
3627 break;
3628 }
3629 col += UnicodeWidthChar::width(ch).unwrap_or(0);
3630 byte += ch.len_utf8();
3631 }
3632 col
3633}