1use super::lsp_status::compose_lsp_status;
2use super::*;
3use crate::config::FileExplorerSide;
4
5impl Editor {
6 pub fn render(&mut self, frame: &mut Frame) {
8 let _span = tracing::info_span!("render").entered();
9 let size = frame.area();
10
11 self.animations.capture_before_all();
16
17 self.cached_layout.last_frame_width = size.width;
19 self.cached_layout.last_frame_height = size.height;
20
21 self.cached_layout.reset_cell_theme_map();
23
24 self.drain_pending_lsp_prompt_for_active_buffer();
30
31 let active_split = self.split_manager.active_split();
36 {
37 let _span = tracing::info_span!("pre_sync_ensure_visible").entered();
38 self.pre_sync_ensure_visible(active_split);
39 }
40
41 {
44 let _span = tracing::info_span!("sync_scroll_groups").entered();
45 self.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.split_view_states {
58 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
59 if let Some(state) = self.buffers.get(&buffer_id) {
60 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
61 let visible_lines =
62 view_state.viewport.visible_line_count().saturating_sub(1);
63 let end_line = start_line.saturating_add(visible_lines);
64 semantic_ranges
65 .entry(buffer_id)
66 .and_modify(|(min_start, max_end)| {
67 *min_start = (*min_start).min(start_line);
68 *max_end = (*max_end).max(end_line);
69 })
70 .or_insert((start_line, end_line));
71 }
72 }
73 }
74 }
75 for (buffer_id, (start_line, end_line)) in semantic_ranges {
76 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
77 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
78 self.maybe_request_folding_ranges_debounced(buffer_id);
79 }
80
81 {
82 let _span = tracing::info_span!("prepare_for_render").entered();
83 for (split_id, view_state) in &self.split_view_states {
84 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
85 if let Some(state) = self.buffers.get_mut(&buffer_id) {
86 let top_byte = view_state.viewport.top_byte;
87 let height = view_state.viewport.height;
88 if let Err(e) = state.prepare_for_render(top_byte, height) {
89 tracing::error!("Failed to prepare buffer for render: {}", e);
90 }
92 }
93 }
94 }
95 }
96
97 let is_search_prompt_active = self.prompt.as_ref().is_some_and(|p| {
100 matches!(
101 p.prompt_type,
102 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
103 )
104 });
105 if is_search_prompt_active {
106 if let Some(ref search_state) = self.search_state {
107 let query = search_state.query.clone();
108 self.update_search_highlights(&query);
109 }
110 }
111
112 let show_search_options = self.prompt.as_ref().is_some_and(|p| {
114 matches!(
115 p.prompt_type,
116 PromptType::Search
117 | PromptType::ReplaceSearch
118 | PromptType::Replace { .. }
119 | PromptType::QueryReplaceSearch
120 | PromptType::QueryReplace { .. }
121 )
122 });
123
124 let has_suggestions = self
126 .prompt
127 .as_ref()
128 .is_some_and(|p| !p.suggestions.is_empty());
129 let has_file_browser = self.prompt.as_ref().is_some_and(|p| {
130 matches!(
131 p.prompt_type,
132 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
133 )
134 }) && self.file_open_state.is_some();
135
136 let constraints = vec![
140 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }), Constraint::Min(0), Constraint::Length(
143 if !self.status_bar_visible || has_suggestions || has_file_browser {
144 0
145 } else {
146 1
147 },
148 ), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(if self.prompt_line_visible || self.prompt.is_some() {
151 1
152 } else {
153 0
154 }), ];
156
157 let main_chunks = Layout::default()
158 .direction(Direction::Vertical)
159 .constraints(constraints)
160 .split(size);
161
162 let menu_bar_area = main_chunks[0];
163 let main_content_area = main_chunks[1];
164 let status_bar_idx = 2;
165 let search_options_idx = 3;
166 let prompt_line_idx = 4;
167
168 let editor_content_area;
171 let file_explorer_should_show = self.file_explorer_visible
172 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
173
174 if file_explorer_should_show {
175 tracing::trace!(
177 "render: file explorer layout active (present={}, sync_in_progress={}, side={:?})",
178 self.file_explorer.is_some(),
179 self.file_explorer_sync_in_progress,
180 self.file_explorer_side
181 );
182 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
183
184 let (explorer_area, editor_area) = match self.file_explorer_side {
185 FileExplorerSide::Left => {
186 let chunks = Layout::default()
187 .direction(Direction::Horizontal)
188 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
189 .split(main_content_area);
190 (chunks[0], chunks[1])
191 }
192 FileExplorerSide::Right => {
193 let chunks = Layout::default()
194 .direction(Direction::Horizontal)
195 .constraints([Constraint::Min(0), Constraint::Length(explorer_cols)])
196 .split(main_content_area);
197 (chunks[1], chunks[0])
198 }
199 };
200
201 self.cached_layout.file_explorer_area = Some(explorer_area);
202 editor_content_area = editor_area;
203
204 let remote_connection = self.connection_display_string();
206
207 if let Some(ref mut explorer) = self.file_explorer {
209 let is_focused = self.key_context == KeyContext::FileExplorer;
210
211 let mut files_with_unsaved_changes = std::collections::HashSet::new();
213 for (buffer_id, state) in &self.buffers {
214 if state.buffer.is_modified() {
215 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
216 if let Some(file_path) = metadata.file_path() {
217 files_with_unsaved_changes.insert(file_path.clone());
218 }
219 }
220 }
221 }
222
223 let close_button_hovered = matches!(
224 &self.mouse_state.hover_target,
225 Some(HoverTarget::FileExplorerCloseButton)
226 );
227 let keybindings = self.keybindings.read().unwrap();
228 let empty: Vec<std::path::PathBuf> = Vec::new();
229 let cut_paths = self
230 .file_explorer_clipboard
231 .as_ref()
232 .filter(|cb| cb.is_cut)
233 .map(|cb| cb.paths.as_slice())
234 .unwrap_or(empty.as_slice());
235 FileExplorerRenderer::render(
236 explorer,
237 frame,
238 explorer_area,
239 is_focused,
240 &files_with_unsaved_changes,
241 &self.file_explorer_decoration_cache,
242 &keybindings,
243 self.key_context.clone(),
244 &self.theme,
245 close_button_hovered,
246 remote_connection.as_deref(),
247 cut_paths,
248 );
249 }
250 } else {
253 self.cached_layout.file_explorer_area = None;
255 editor_content_area = main_content_area;
256 }
257
258 if self.plugin_manager.is_active() {
265 let hooks_start = std::time::Instant::now();
266 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
268
269 let mut total_new_lines = 0usize;
270 for (split_id, buffer_id, split_area) in visible_buffers {
271 let viewport_top_byte = self
273 .split_view_states
274 .get(&split_id)
275 .map(|vs| vs.viewport.top_byte)
276 .unwrap_or(0);
277
278 if let Some(state) = self.buffers.get_mut(&buffer_id) {
279 self.plugin_manager.run_hook(
281 "render_start",
282 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
283 );
284
285 let visible_count = split_area.height as usize;
288 let is_binary = state.buffer.is_binary();
289 let line_ending = state.buffer.line_ending();
290 let base_tokens =
291 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
292 &mut state.buffer,
293 viewport_top_byte,
294 self.config.editor.estimated_line_length,
295 visible_count,
296 is_binary,
297 line_ending,
298 );
299 let viewport_start = viewport_top_byte;
300 let viewport_end = base_tokens
301 .last()
302 .and_then(|t| t.source_offset)
303 .unwrap_or(viewport_start);
304 let cursor_positions: Vec<usize> = self
305 .split_view_states
306 .get(&split_id)
307 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
308 .unwrap_or_default();
309 self.plugin_manager.run_hook(
310 "view_transform_request",
311 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
312 buffer_id,
313 split_id: split_id.into(),
314 viewport_start,
315 viewport_end,
316 tokens: base_tokens,
317 cursor_positions,
318 },
319 );
320
321 if let Some(vs) = self.split_view_states.get_mut(&split_id) {
325 vs.view_transform_stale = false;
326 }
327
328 let visible_count = split_area.height as usize;
330 let top_byte = viewport_top_byte;
331
332 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
334
335 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
337 let mut line_number = state.buffer.get_line_number(top_byte);
338 let mut iter = state
339 .buffer
340 .line_iterator(top_byte, self.config.editor.estimated_line_length);
341
342 for _ in 0..visible_count {
343 if let Some((line_start, line_content)) = iter.next_line() {
344 let byte_end = line_start + line_content.len();
345 let byte_range = (line_start, byte_end);
346
347 if !seen_byte_ranges.contains(&byte_range) {
349 new_lines.push(crate::services::plugins::hooks::LineInfo {
350 line_number,
351 byte_start: line_start,
352 byte_end,
353 content: line_content,
354 });
355 seen_byte_ranges.insert(byte_range);
356 }
357 line_number += 1;
358 } else {
359 break;
360 }
361 }
362
363 if !new_lines.is_empty() {
365 total_new_lines += new_lines.len();
366 self.plugin_manager.run_hook(
367 "lines_changed",
368 crate::services::plugins::hooks::HookArgs::LinesChanged {
369 buffer_id,
370 lines: new_lines,
371 },
372 );
373 }
374 }
375 }
376 let hooks_elapsed = hooks_start.elapsed();
377 tracing::trace!(
378 new_lines = total_new_lines,
379 elapsed_ms = hooks_elapsed.as_millis(),
380 elapsed_us = hooks_elapsed.as_micros(),
381 "lines_changed hooks total"
382 );
383
384 let commands = self.plugin_manager.process_commands();
396 if !commands.is_empty() {
397 let cmd_names: Vec<String> =
398 commands.iter().map(|c| c.debug_variant_name()).collect();
399 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
400 }
401 for command in commands {
402 if let Err(e) = self.handle_plugin_command(command) {
403 tracing::error!("Error handling plugin command: {}", e);
404 }
405 }
406
407 self.flush_pending_grammars();
409 }
410
411 let lsp_waiting = !self.pending_completion_requests.is_empty()
413 || self.pending_goto_definition_request.is_some();
414
415 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
422 let hide_cursor = self.menu_state.active_menu.is_some()
423 || self.key_context == KeyContext::FileExplorer
424 || self.terminal_mode
425 || settings_visible
426 || self.keybinding_editor.is_some();
427
428 let hovered_tab = match &self.mouse_state.hover_target {
430 Some(HoverTarget::TabName(target, split_id)) => Some((*target, *split_id, false)),
431 Some(HoverTarget::TabCloseButton(target, split_id)) => Some((*target, *split_id, true)),
432 _ => None,
433 };
434
435 let hovered_close_split = match &self.mouse_state.hover_target {
437 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
438 _ => None,
439 };
440
441 let hovered_maximize_split = match &self.mouse_state.hover_target {
443 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
444 _ => None,
445 };
446
447 let is_maximized = self.split_manager.is_maximized();
448
449 let mut pending_hardware_cursor: Option<(u16, u16)> = None;
456
457 let _content_span = tracing::info_span!("render_content").entered();
458 let (
459 split_areas,
460 tab_layouts,
461 close_split_areas,
462 maximize_split_areas,
463 view_line_mappings,
464 horizontal_scrollbar_areas,
465 grouped_separator_areas,
466 ) = SplitRenderer::render_content(
467 frame,
468 editor_content_area,
469 &self.split_manager,
470 &mut self.buffers,
471 &self.buffer_metadata,
472 &mut self.event_logs,
473 &mut self.composite_buffers,
474 &mut self.composite_view_states,
475 &self.theme,
476 self.ansi_background.as_ref(),
477 self.background_fade,
478 lsp_waiting,
479 self.config.editor.large_file_threshold_bytes,
480 self.config.editor.line_wrap,
481 self.config.editor.estimated_line_length,
482 self.config.editor.highlight_context_bytes,
483 Some(&mut self.split_view_states),
484 &self.grouped_subtrees,
485 hide_cursor,
486 hovered_tab,
487 hovered_close_split,
488 hovered_maximize_split,
489 is_maximized,
490 self.config.editor.relative_line_numbers,
491 self.tab_bar_visible,
492 self.config.editor.use_terminal_bg,
493 self.session_mode || !self.software_cursor_only,
494 self.software_cursor_only,
495 self.config.editor.show_vertical_scrollbar,
496 self.config.editor.show_horizontal_scrollbar,
497 self.config.editor.diagnostics_inline_text,
498 self.config.editor.show_tilde,
499 self.config.editor.highlight_current_column,
500 &mut self.cached_layout.cell_theme_map,
501 size.width,
502 &mut pending_hardware_cursor,
503 );
504
505 drop(_content_span);
506
507 self.maybe_start_cursor_jump_animation(pending_hardware_cursor, active_split);
513
514 if self.plugin_manager.is_active() {
518 for (split_id, view_state) in &self.split_view_states {
519 let current = (
520 view_state.viewport.top_byte,
521 view_state.viewport.width,
522 view_state.viewport.height,
523 );
524 let (changed, previous) = match self.previous_viewports.get(split_id) {
529 Some(previous) => (*previous != current, Some(*previous)),
530 None => (false, None), };
532 tracing::trace!(
533 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
534 split_id,
535 current,
536 previous,
537 changed
538 );
539 if changed {
540 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
541 let top_line = self.buffers.get(&buffer_id).and_then(|state| {
543 if state.buffer.line_count().is_some() {
544 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
545 } else {
546 None
547 }
548 });
549 tracing::debug!(
550 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
551 split_id,
552 buffer_id,
553 view_state.viewport.top_byte,
554 top_line
555 );
556 self.plugin_manager.run_hook(
557 "viewport_changed",
558 crate::services::plugins::hooks::HookArgs::ViewportChanged {
559 split_id: (*split_id).into(),
560 buffer_id,
561 top_byte: view_state.viewport.top_byte,
562 top_line,
563 width: view_state.viewport.width,
564 height: view_state.viewport.height,
565 },
566 );
567 }
568 }
569 }
570 }
571
572 self.previous_viewports.clear();
574 for (split_id, view_state) in &self.split_view_states {
575 self.previous_viewports.insert(
576 *split_id,
577 (
578 view_state.viewport.top_byte,
579 view_state.viewport.width,
580 view_state.viewport.height,
581 ),
582 );
583 }
584
585 self.render_terminal_splits(frame, &split_areas);
587
588 self.cached_layout.split_areas = split_areas;
589 self.cached_layout.horizontal_scrollbar_areas = horizontal_scrollbar_areas;
590 self.cached_layout.tab_layouts = tab_layouts;
591 self.cached_layout.close_split_areas = close_split_areas;
592 self.cached_layout.maximize_split_areas = maximize_split_areas;
593 self.cached_layout.view_line_mappings = view_line_mappings;
594
595 self.drain_pending_vb_animations();
600 let mut separator_areas = self
601 .split_manager
602 .get_separators_with_ids(editor_content_area);
603 separator_areas.extend(grouped_separator_areas);
608 self.cached_layout.separator_areas = separator_areas;
609 self.cached_layout.editor_content_area = Some(editor_content_area);
610
611 self.render_hover_highlights(frame);
613
614 self.cached_layout.suggestions_area = None;
616 self.cached_layout.suggestions_outer_area = None;
617 self.file_browser_layout = None;
618
619 let display_name = self
621 .buffer_metadata
622 .get(&self.active_buffer())
623 .map(|m| m.display_name.clone())
624 .unwrap_or_else(|| "[No Name]".to_string());
625
626 self.update_terminal_title(&display_name);
630
631 let status_message = self.status_message.clone();
632 let plugin_status_message = self.plugin_status_message.clone();
633 let prompt = self.prompt.clone();
634 let current_language = self
657 .buffers
658 .get(&self.active_buffer())
659 .map(|s| s.language.clone())
660 .unwrap_or_default();
661 let buffer_lsp_disabled_reason = self
662 .buffer_metadata
663 .get(&self.active_buffer())
664 .filter(|m| !m.lsp_enabled)
665 .and_then(|m| m.lsp_disabled_reason.as_deref());
666 let (lsp_status, lsp_indicator_state) = compose_lsp_status(
667 ¤t_language,
668 buffer_lsp_disabled_reason,
669 &self.lsp_progress,
670 &self.lsp_server_statuses,
671 &self.config.lsp,
672 &self.user_dismissed_lsp_languages,
673 );
674 let theme = self.theme.clone();
675 let keybindings_cloned = self.keybindings.read().unwrap().clone(); let chord_state_cloned = self.chord_state.clone(); let update_available = self.latest_version().map(|v| v.to_string());
680
681 if self.status_bar_visible && !has_suggestions && !has_file_browser {
683 let (warning_level, general_warning_count) =
686 if self.config.warnings.show_status_indicator {
687 let lsp_level = {
688 use crate::services::async_bridge::LspServerStatus;
689 let mut level = WarningLevel::None;
690 for ((lang, _), status) in &self.lsp_server_statuses {
691 if lang == ¤t_language {
692 match status {
693 LspServerStatus::Error => {
694 level = WarningLevel::Error;
695 break;
696 }
697 LspServerStatus::Starting | LspServerStatus::Initializing => {
698 if level != WarningLevel::Error {
699 level = WarningLevel::Warning;
700 }
701 }
702 _ => {}
703 }
704 }
705 }
706 level
707 };
708 (lsp_level, self.get_general_warning_count())
709 } else {
710 (WarningLevel::None, 0)
711 };
712
713 use crate::view::ui::status_bar::StatusBarHover;
715 let status_bar_hover = match &self.mouse_state.hover_target {
716 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
717 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
718 Some(HoverTarget::StatusBarLineEndingIndicator) => {
719 StatusBarHover::LineEndingIndicator
720 }
721 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
722 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
723 Some(HoverTarget::StatusBarRemoteIndicator) => StatusBarHover::RemoteIndicator,
724 _ => StatusBarHover::None,
725 };
726
727 let remote_connection = self.connection_display_string();
728
729 let session_name = self.session_name().map(|s| s.to_string());
731
732 let active_split = self.effective_active_split();
733 let active_buf = self.active_buffer();
734 let default_cursors = crate::model::cursor::Cursors::new();
735 let status_cursors = self
736 .split_view_states
737 .get(&active_split)
738 .map(|vs| &vs.cursors)
739 .unwrap_or(&default_cursors);
740 let is_read_only = self
741 .buffer_metadata
742 .get(&active_buf)
743 .map(|m| m.read_only)
744 .unwrap_or(false);
745 let is_synthetic_placeholder = self
746 .buffer_metadata
747 .get(&active_buf)
748 .map(|m| m.synthetic_placeholder)
749 .unwrap_or(false);
750 let mut status_ctx = crate::view::ui::status_bar::StatusBarContext {
751 state: self.buffers.get_mut(&active_buf).unwrap(),
752 cursors: status_cursors,
753 status_message: &status_message,
754 plugin_status_message: &plugin_status_message,
755 lsp_status: &lsp_status,
756 lsp_indicator_state,
757 theme: &theme,
758 display_name: &display_name,
759 keybindings: &keybindings_cloned,
760 chord_state: &chord_state_cloned,
761 update_available: update_available.as_deref(),
762 warning_level,
763 general_warning_count,
764 hover: status_bar_hover,
765 remote_connection: remote_connection.as_deref(),
766 session_name: session_name.as_deref(),
767 read_only: is_read_only,
768 remote_state_override: self.remote_indicator_override.as_ref(),
769 is_synthetic_placeholder,
770 remote_indicator_on_bar: false,
775 };
776 let status_bar_layout = StatusBarRenderer::render_status_bar(
777 frame,
778 main_chunks[status_bar_idx],
779 &mut status_ctx,
780 &self.config.editor.status_bar,
781 );
782
783 let status_bar_area = main_chunks[status_bar_idx];
785 self.cached_layout.status_bar_area =
786 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
787 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
788 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
789 self.cached_layout.status_bar_line_ending_area =
790 status_bar_layout.line_ending_indicator;
791 self.cached_layout.status_bar_encoding_area = status_bar_layout.encoding_indicator;
792 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
793 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
794 self.cached_layout.status_bar_remote_area = status_bar_layout.remote_indicator;
795 }
796
797 if show_search_options {
799 let confirm_each = self.prompt.as_ref().and_then(|p| {
801 if matches!(
802 p.prompt_type,
803 PromptType::ReplaceSearch
804 | PromptType::Replace { .. }
805 | PromptType::QueryReplaceSearch
806 | PromptType::QueryReplace { .. }
807 ) {
808 Some(self.search_confirm_each)
809 } else {
810 None
811 }
812 });
813
814 use crate::view::ui::status_bar::SearchOptionsHover;
816 let search_options_hover = match &self.mouse_state.hover_target {
817 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
818 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
819 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
820 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
821 _ => SearchOptionsHover::None,
822 };
823
824 let search_options_layout = StatusBarRenderer::render_search_options(
825 frame,
826 main_chunks[search_options_idx],
827 self.search_case_sensitive,
828 self.search_whole_word,
829 self.search_use_regex,
830 confirm_each,
831 &theme,
832 &keybindings_cloned,
833 search_options_hover,
834 );
835 self.cached_layout.search_options_layout = Some(search_options_layout);
836 } else {
837 self.cached_layout.search_options_layout = None;
838 }
839
840 if let Some(prompt) = &prompt {
842 if matches!(
844 prompt.prompt_type,
845 crate::view::prompt::PromptType::OpenFile
846 | crate::view::prompt::PromptType::SwitchProject
847 ) {
848 if let Some(file_open_state) = &self.file_open_state {
849 StatusBarRenderer::render_file_open_prompt(
850 frame,
851 main_chunks[prompt_line_idx],
852 prompt,
853 file_open_state,
854 &theme,
855 );
856 } else {
857 StatusBarRenderer::render_prompt(
858 frame,
859 main_chunks[prompt_line_idx],
860 prompt,
861 &theme,
862 );
863 }
864 } else {
865 StatusBarRenderer::render_prompt(
866 frame,
867 main_chunks[prompt_line_idx],
868 prompt,
869 &theme,
870 );
871 }
872 }
873
874 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
877
878 let theme_clone = self.theme.clone();
881 let hover_target = self.mouse_state.hover_target.clone();
882
883 self.cached_layout.popup_areas.clear();
885
886 let popup_info: Vec<_> = {
888 let active_split = self.split_manager.active_split();
890 let viewport = self
891 .split_view_states
892 .get(&active_split)
893 .map(|vs| vs.viewport.clone());
894
895 let content_rect = self
900 .cached_layout
901 .split_areas
902 .iter()
903 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
904 .map(|(_, _, rect, _, _, _)| *rect);
905
906 let primary_cursor = self
907 .split_view_states
908 .get(&active_split)
909 .map(|vs| *vs.cursors.primary());
910 let state = self.active_state_mut();
911 if state.popups.is_visible() {
912 let primary_cursor =
914 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
915
916 let gutter_width = viewport
918 .as_ref()
919 .map(|vp| vp.gutter_width(&state.buffer) as u16)
920 .unwrap_or(0);
921
922 let cursor_screen_pos = viewport
923 .as_ref()
924 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
925 .unwrap_or((0, 0));
926
927 let word_start_screen_pos = {
931 use crate::primitives::word_navigation::find_completion_word_start;
932 let word_start =
933 find_completion_word_start(&state.buffer, primary_cursor.position);
934 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
935 viewport
936 .as_ref()
937 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
938 .unwrap_or((0, 0))
939 };
940
941 let (base_x, base_y) = content_rect
946 .map(|r| (r.x + gutter_width, r.y))
947 .unwrap_or((gutter_width, 1));
948
949 let cursor_screen_pos =
950 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
951 let word_start_screen_pos = (
952 word_start_screen_pos.0 + base_x,
953 word_start_screen_pos.1 + base_y,
954 );
955
956 state
958 .popups
959 .all()
960 .iter()
961 .enumerate()
962 .map(|(popup_idx, popup)| {
963 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
965 (word_start_screen_pos.0, cursor_screen_pos.1)
966 } else {
967 cursor_screen_pos
968 };
969 let popup_area = popup.calculate_area(size, Some(popup_pos));
970
971 let desc_height = popup.description_height();
974 let inner_area = if popup.bordered {
975 ratatui::layout::Rect {
976 x: popup_area.x + 1,
977 y: popup_area.y + 1 + desc_height,
978 width: popup_area.width.saturating_sub(2),
979 height: popup_area.height.saturating_sub(2 + desc_height),
980 }
981 } else {
982 ratatui::layout::Rect {
983 x: popup_area.x,
984 y: popup_area.y + desc_height,
985 width: popup_area.width,
986 height: popup_area.height.saturating_sub(desc_height),
987 }
988 };
989
990 let num_items = match &popup.content {
991 crate::view::popup::PopupContent::List { items, .. } => items.len(),
992 _ => 0,
993 };
994
995 let total_lines = popup.item_count();
997 let visible_lines = inner_area.height as usize;
998 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
999 {
1000 Some(ratatui::layout::Rect {
1001 x: inner_area.x + inner_area.width - 1,
1002 y: inner_area.y,
1003 width: 1,
1004 height: inner_area.height,
1005 })
1006 } else {
1007 None
1008 };
1009
1010 (
1011 popup_idx,
1012 popup_area,
1013 inner_area,
1014 popup.scroll_offset,
1015 num_items,
1016 scrollbar_rect,
1017 total_lines,
1018 )
1019 })
1020 .collect()
1021 } else {
1022 Vec::new()
1023 }
1024 };
1025
1026 self.cached_layout.popup_areas = popup_info.clone();
1028
1029 let state = self.active_state_mut();
1031 if state.popups.is_visible() {
1032 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
1033 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
1034 popup.render_with_hover(
1035 frame,
1036 *popup_area,
1037 &theme_clone,
1038 hover_target.as_ref(),
1039 );
1040 }
1041 }
1042 }
1043
1044 self.cached_layout.global_popup_areas.clear();
1055 if let Some(popup) = self.global_popups.top() {
1056 let top_idx = self.global_popups.all().len() - 1;
1057 let popup_area = popup.calculate_area(size, None);
1058 let desc_height = popup.description_height();
1059 let inner_area = if popup.bordered {
1060 ratatui::layout::Rect {
1061 x: popup_area.x + 1,
1062 y: popup_area.y + 1 + desc_height,
1063 width: popup_area.width.saturating_sub(2),
1064 height: popup_area.height.saturating_sub(2 + desc_height),
1065 }
1066 } else {
1067 ratatui::layout::Rect {
1068 x: popup_area.x,
1069 y: popup_area.y + desc_height,
1070 width: popup_area.width,
1071 height: popup_area.height.saturating_sub(desc_height),
1072 }
1073 };
1074 let num_items = match &popup.content {
1075 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1076 _ => 0,
1077 };
1078 self.cached_layout.global_popup_areas.push((
1079 top_idx,
1080 popup_area,
1081 inner_area,
1082 popup.scroll_offset,
1083 num_items,
1084 ));
1085 popup.render_with_hover(frame, popup_area, &theme_clone, hover_target.as_ref());
1086 }
1087
1088 self.update_menu_context();
1091
1092 let settings_visible = self
1095 .settings_state
1096 .as_ref()
1097 .map(|s| s.visible)
1098 .unwrap_or(false);
1099 if settings_visible {
1100 crate::view::dimming::apply_dimming(frame, size);
1102 }
1103 if let Some(ref mut settings_state) = self.settings_state {
1104 if settings_state.visible {
1105 settings_state.update_focus_states();
1106 let settings_layout = crate::view::settings::render_settings(
1107 frame,
1108 size,
1109 settings_state,
1110 &self.theme,
1111 );
1112 self.cached_layout.settings_layout = Some(settings_layout);
1113 }
1114 }
1115
1116 if let Some(ref wizard) = self.calibration_wizard {
1118 crate::view::dimming::apply_dimming(frame, size);
1120 crate::view::calibration_wizard::render_calibration_wizard(
1121 frame,
1122 size,
1123 wizard,
1124 &self.theme,
1125 );
1126 }
1127
1128 if let Some(ref mut kb_editor) = self.keybinding_editor {
1130 crate::view::dimming::apply_dimming(frame, size);
1131 crate::view::keybinding_editor::render_keybinding_editor(
1132 frame,
1133 size,
1134 kb_editor,
1135 &self.theme,
1136 );
1137 }
1138
1139 if let Some(ref debug) = self.event_debug {
1141 crate::view::dimming::apply_dimming(frame, size);
1143 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
1144 }
1145
1146 if self.menu_bar_visible {
1147 self.expanded_menus_cache.update(
1151 &self.theme_registry,
1152 &self.menus,
1153 &self.menu_state.themes_dir,
1154 );
1155 let expanded = self.expanded_menus_cache.get().expect("just updated");
1156 let keybindings = self.keybindings.read().unwrap();
1157 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
1158 frame,
1159 menu_bar_area,
1160 expanded,
1161 &self.menu_state,
1162 &keybindings,
1163 &self.theme,
1164 self.mouse_state.hover_target.as_ref(),
1165 self.config.editor.menu_bar_mnemonics,
1166 ));
1167 } else {
1168 self.cached_layout.menu_layout = None;
1169 }
1170
1171 if let Some(ref menu) = self.tab_context_menu {
1173 self.render_tab_context_menu(frame, menu);
1174 }
1175
1176 if let Some(ref menu) = self.file_explorer_context_menu {
1177 self.render_file_explorer_context_menu(frame, menu);
1178 }
1179
1180 self.record_non_editor_theme_regions();
1182
1183 self.render_theme_info_popup(frame);
1185
1186 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
1188 if drag_state.is_dragging() {
1189 self.render_tab_drop_zone(frame, drag_state);
1190 }
1191 }
1192
1193 if self.gpm_active {
1199 if let Some((col, row)) = self.mouse_cursor_position {
1200 use ratatui::style::Modifier;
1201
1202 if col < size.width && row < size.height {
1204 let buf = frame.buffer_mut();
1206 if let Some(cell) = buf.cell_mut((col, row)) {
1207 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1208 }
1209 }
1210 }
1211 }
1212
1213 if self.keyboard_capture && self.terminal_mode {
1216 let active_split = self.split_manager.active_split();
1218 let active_split_area = self
1219 .cached_layout
1220 .split_areas
1221 .iter()
1222 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1223 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1224
1225 if let Some(terminal_area) = active_split_area {
1226 self.apply_keyboard_capture_dimming(frame, terminal_area);
1227 }
1228 }
1229
1230 if let Some((cx, cy)) = pending_hardware_cursor {
1241 if self.prompt.is_none() && !self.cursor_obscured_by_overlay(cx, cy) {
1242 frame.set_cursor_position((cx, cy));
1243 }
1244 }
1245
1246 crate::view::color_support::convert_buffer_colors(
1248 frame.buffer_mut(),
1249 self.color_capability,
1250 );
1251
1252 self.animations.apply_all(frame.buffer_mut());
1254 }
1255
1256 fn maybe_start_cursor_jump_animation(
1271 &mut self,
1272 current_pos: Option<(u16, u16)>,
1273 active_split: crate::model::event::LeafId,
1274 ) {
1275 if !self.config.editor.animations {
1280 self.previous_cursor_screen_pos = current_pos.map(|p| (p, active_split));
1281 return;
1282 }
1283
1284 let Some(current) = current_pos else {
1285 self.previous_cursor_screen_pos = None;
1289 return;
1290 };
1291
1292 let prev_entry = self.previous_cursor_screen_pos;
1293 self.previous_cursor_screen_pos = Some((current, active_split));
1295
1296 let Some((prev, prev_split)) = prev_entry else {
1297 return;
1298 };
1299 if prev == current && prev_split == active_split {
1300 return;
1301 }
1302
1303 let dx = (current.0 as i32 - prev.0 as i32).abs();
1304 let dy = (current.1 as i32 - prev.1 as i32).abs();
1305 let crossed_panes = prev_split != active_split;
1311 let row_jump = dy > 2;
1312 let col_jump = dx >= 10;
1313 if !crossed_panes && !row_jump && !col_jump {
1314 return;
1315 }
1316
1317 if let Some(prev_anim) = self.cursor_jump_animation.take() {
1319 self.animations.cancel(prev_anim);
1320 }
1321
1322 let id = self.animations.start(
1323 ratatui::layout::Rect {
1326 x: prev.0.min(current.0),
1327 y: prev.1.min(current.1),
1328 width: dx as u16 + 1,
1329 height: dy as u16 + 1,
1330 },
1331 crate::view::animation::AnimationKind::CursorJump {
1332 from: prev,
1333 to: current,
1334 duration: std::time::Duration::from_millis(140),
1335 cursor_color: self.theme.cursor,
1336 bg_color: self.theme.editor_bg,
1337 },
1338 );
1339 self.cursor_jump_animation = Some(id);
1340 }
1341
1342 fn cursor_obscured_by_overlay(&self, x: u16, y: u16) -> bool {
1346 let inside = |rect: ratatui::layout::Rect| -> bool {
1347 x >= rect.x
1348 && x < rect.x.saturating_add(rect.width)
1349 && y >= rect.y
1350 && y < rect.y.saturating_add(rect.height)
1351 };
1352
1353 if self
1354 .cached_layout
1355 .popup_areas
1356 .iter()
1357 .any(|entry| inside(entry.1))
1358 {
1359 return true;
1360 }
1361 if self
1362 .cached_layout
1363 .global_popup_areas
1364 .iter()
1365 .any(|entry| inside(entry.1))
1366 {
1367 return true;
1368 }
1369 if let Some((rect, _, _, _)) = self.cached_layout.suggestions_area {
1370 if inside(rect) {
1371 return true;
1372 }
1373 }
1374 if let Some(ref fb) = self.file_browser_layout {
1375 if inside(fb.popup_area) {
1376 return true;
1377 }
1378 }
1379 false
1380 }
1381
1382 fn render_quick_open_hints(
1384 frame: &mut Frame,
1385 area: ratatui::layout::Rect,
1386 theme: &crate::view::theme::Theme,
1387 ) {
1388 use ratatui::style::{Modifier, Style};
1389 use ratatui::text::{Line, Span};
1390 use ratatui::widgets::Paragraph;
1391 use rust_i18n::t;
1392
1393 let hints_style = Style::default()
1394 .fg(theme.line_number_fg)
1395 .bg(theme.suggestion_selected_bg)
1396 .add_modifier(Modifier::DIM);
1397 let hints_text = t!("quick_open.mode_hints");
1398 let left_margin = 2;
1400 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1401 let mut spans = Vec::new();
1402 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1403 spans.push(Span::styled(hints_text.to_string(), hints_style));
1404 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1405 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1406
1407 let paragraph = Paragraph::new(Line::from(spans));
1408 frame.render_widget(paragraph, area);
1409 }
1410
1411 fn apply_keyboard_capture_dimming(
1414 &self,
1415 frame: &mut Frame,
1416 terminal_area: ratatui::layout::Rect,
1417 ) {
1418 let size = frame.area();
1419 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1420 }
1421
1422 fn render_prompt_popups(
1425 &mut self,
1426 frame: &mut Frame,
1427 prompt_area: ratatui::layout::Rect,
1428 width: u16,
1429 ) {
1430 let Some(prompt) = &self.prompt else { return };
1431
1432 if matches!(
1433 prompt.prompt_type,
1434 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1435 ) {
1436 let Some(file_open_state) = &self.file_open_state else {
1437 return;
1438 };
1439 let max_height = prompt_area.y.saturating_sub(1).min(20);
1440 let popup_area = ratatui::layout::Rect {
1441 x: 0,
1442 y: prompt_area.y.saturating_sub(max_height),
1443 width,
1444 height: max_height,
1445 };
1446 let keybindings = self.keybindings.read().unwrap();
1447 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1448 frame,
1449 popup_area,
1450 file_open_state,
1451 &self.theme,
1452 &self.mouse_state.hover_target,
1453 Some(&*keybindings),
1454 );
1455 return;
1456 }
1457
1458 if prompt.suggestions.is_empty() {
1459 return;
1460 }
1461
1462 let suggestion_count = prompt.suggestions.len().min(10);
1463 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1464 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1465 let height = suggestion_count as u16 + 2 + hints_height;
1466
1467 let suggestions_area = ratatui::layout::Rect {
1468 x: 0,
1469 y: prompt_area.y.saturating_sub(height),
1470 width,
1471 height: height - hints_height,
1472 };
1473
1474 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1475
1476 if let Some(prompt) = self.prompt.as_mut() {
1479 prompt.ensure_selected_visible();
1480 }
1481 let Some(prompt) = &self.prompt else { return };
1482
1483 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
1484 frame,
1485 suggestions_area,
1486 prompt,
1487 &self.theme,
1488 self.mouse_state.hover_target.as_ref(),
1489 );
1490 if self.cached_layout.suggestions_area.is_some() {
1491 self.cached_layout.suggestions_outer_area = Some(suggestions_area);
1492 }
1493
1494 if is_quick_open {
1495 let hints_area = ratatui::layout::Rect {
1496 x: 0,
1497 y: prompt_area.y.saturating_sub(hints_height),
1498 width,
1499 height: hints_height,
1500 };
1501 frame.render_widget(ratatui::widgets::Clear, hints_area);
1502 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1503 }
1504 }
1505
1506 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
1508 use ratatui::style::Style;
1509 use ratatui::text::Span;
1510 use ratatui::widgets::Paragraph;
1511
1512 match &self.mouse_state.hover_target {
1513 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
1514 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
1516 if sid == split_id && dir == direction {
1517 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1518 match dir {
1519 SplitDirection::Horizontal => {
1520 let line_text = "─".repeat(*length as usize);
1521 let paragraph =
1522 Paragraph::new(Span::styled(line_text, hover_style));
1523 frame.render_widget(
1524 paragraph,
1525 ratatui::layout::Rect::new(*x, *y, *length, 1),
1526 );
1527 }
1528 SplitDirection::Vertical => {
1529 for offset in 0..*length {
1530 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1531 frame.render_widget(
1532 paragraph,
1533 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
1534 );
1535 }
1536 }
1537 }
1538 }
1539 }
1540 }
1541 Some(HoverTarget::ScrollbarThumb(split_id)) => {
1542 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1544 &self.cached_layout.split_areas
1545 {
1546 if sid == split_id {
1547 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
1548 for row_offset in *thumb_start..*thumb_end {
1549 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
1550 frame.render_widget(
1551 paragraph,
1552 ratatui::layout::Rect::new(
1553 scrollbar_rect.x,
1554 scrollbar_rect.y + row_offset as u16,
1555 1,
1556 1,
1557 ),
1558 );
1559 }
1560 }
1561 }
1562 }
1563 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
1564 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
1566 &self.cached_layout.split_areas
1567 {
1568 if sid == split_id {
1569 let track_hover_style =
1570 Style::default().bg(self.theme.scrollbar_track_hover_fg);
1571 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
1572 frame.render_widget(
1573 paragraph,
1574 ratatui::layout::Rect::new(
1575 scrollbar_rect.x,
1576 scrollbar_rect.y + hovered_row,
1577 1,
1578 1,
1579 ),
1580 );
1581 }
1582 }
1583 }
1584 Some(HoverTarget::FileExplorerBorder) => {
1585 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1587 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1588 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1589 for row_offset in 0..explorer_area.height {
1590 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1591 frame.render_widget(
1592 paragraph,
1593 ratatui::layout::Rect::new(
1594 border_x,
1595 explorer_area.y + row_offset,
1596 1,
1597 1,
1598 ),
1599 );
1600 }
1601 }
1602 }
1603 _ => {}
1605 }
1606 }
1607
1608 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
1610 use ratatui::style::Style;
1611 use ratatui::text::{Line, Span};
1612 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1613
1614 let items = super::types::TabContextMenuItem::all();
1615 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
1620 let screen_height = frame.area().height;
1621
1622 let menu_x = if menu.position.0 + menu_width > screen_width {
1623 screen_width.saturating_sub(menu_width)
1624 } else {
1625 menu.position.0
1626 };
1627
1628 let menu_y = if menu.position.1 + menu_height > screen_height {
1629 screen_height.saturating_sub(menu_height)
1630 } else {
1631 menu.position.1
1632 };
1633
1634 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1635
1636 frame.render_widget(Clear, area);
1638
1639 let mut lines = Vec::new();
1641 for (idx, item) in items.iter().enumerate() {
1642 let is_highlighted = idx == menu.highlighted;
1643
1644 let style = if is_highlighted {
1645 Style::default()
1646 .fg(self.theme.menu_highlight_fg)
1647 .bg(self.theme.menu_highlight_bg)
1648 } else {
1649 Style::default()
1650 .fg(self.theme.menu_dropdown_fg)
1651 .bg(self.theme.menu_dropdown_bg)
1652 };
1653
1654 let label = item.label();
1656 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1658
1659 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1660 }
1661
1662 let block = Block::default()
1663 .borders(Borders::ALL)
1664 .border_style(Style::default().fg(self.theme.menu_border_fg))
1665 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1666
1667 let paragraph = Paragraph::new(lines).block(block);
1668 frame.render_widget(paragraph, area);
1669 }
1670
1671 fn render_file_explorer_context_menu(
1673 &self,
1674 frame: &mut Frame,
1675 menu: &super::types::FileExplorerContextMenu,
1676 ) {
1677 use ratatui::style::Style;
1678 use ratatui::text::{Line, Span};
1679 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1680
1681 let items = menu.items();
1682 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
1683 let menu_height = menu.height();
1684 let (menu_x, menu_y) = menu.clamped_position(frame.area().width, frame.area().height);
1685
1686 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1687
1688 frame.render_widget(Clear, area);
1689
1690 let mut lines = Vec::new();
1691 for (idx, item) in items.iter().enumerate() {
1692 let is_highlighted = idx == menu.highlighted;
1693
1694 let style = if is_highlighted {
1695 Style::default()
1696 .fg(self.theme.menu_highlight_fg)
1697 .bg(self.theme.menu_highlight_bg)
1698 } else {
1699 Style::default()
1700 .fg(self.theme.menu_dropdown_fg)
1701 .bg(self.theme.menu_dropdown_bg)
1702 };
1703
1704 let label = item.label();
1705 let content_width = (menu_width as usize).saturating_sub(2);
1706 let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1707
1708 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1709 }
1710
1711 let block = Block::default()
1712 .borders(Borders::ALL)
1713 .border_style(Style::default().fg(self.theme.menu_border_fg))
1714 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1715
1716 let paragraph = Paragraph::new(lines).block(block);
1717 frame.render_widget(paragraph, area);
1718 }
1719
1720 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
1722 use ratatui::style::Modifier;
1723
1724 let Some(ref drop_zone) = drag_state.drop_zone else {
1725 return;
1726 };
1727
1728 let split_id = drop_zone.split_id();
1729
1730 let split_area = self
1732 .cached_layout
1733 .split_areas
1734 .iter()
1735 .find(|(sid, _, _, _, _, _)| *sid == split_id)
1736 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1737
1738 let Some(content_rect) = split_area else {
1739 return;
1740 };
1741
1742 use super::types::TabDropZone;
1744
1745 let highlight_area = match drop_zone {
1746 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
1747 content_rect
1750 }
1751 TabDropZone::SplitLeft(_) => {
1752 let width = (content_rect.width / 2).max(3);
1754 ratatui::layout::Rect::new(
1755 content_rect.x,
1756 content_rect.y,
1757 width,
1758 content_rect.height,
1759 )
1760 }
1761 TabDropZone::SplitRight(_) => {
1762 let width = (content_rect.width / 2).max(3);
1764 let x = content_rect.x + content_rect.width - width;
1765 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
1766 }
1767 TabDropZone::SplitTop(_) => {
1768 let height = (content_rect.height / 2).max(2);
1770 ratatui::layout::Rect::new(
1771 content_rect.x,
1772 content_rect.y,
1773 content_rect.width,
1774 height,
1775 )
1776 }
1777 TabDropZone::SplitBottom(_) => {
1778 let height = (content_rect.height / 2).max(2);
1780 let y = content_rect.y + content_rect.height - height;
1781 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
1782 }
1783 };
1784
1785 let buf = frame.buffer_mut();
1788 let drop_zone_bg = self.theme.tab_drop_zone_bg;
1789 let drop_zone_border = self.theme.tab_drop_zone_border;
1790
1791 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1793 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1794 if let Some(cell) = buf.cell_mut((x, y)) {
1795 cell.set_bg(drop_zone_bg);
1798
1799 let is_border = x == highlight_area.x
1801 || x == highlight_area.x + highlight_area.width - 1
1802 || y == highlight_area.y
1803 || y == highlight_area.y + highlight_area.height - 1;
1804
1805 if is_border {
1806 cell.set_fg(drop_zone_border);
1807 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
1808 }
1809 }
1810 }
1811 }
1812
1813 match drop_zone {
1815 TabDropZone::SplitLeft(_) => {
1816 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1818 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
1819 cell.set_symbol("▌");
1820 cell.set_fg(drop_zone_border);
1821 }
1822 }
1823 }
1824 TabDropZone::SplitRight(_) => {
1825 let x = highlight_area.x + highlight_area.width - 1;
1827 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1828 if let Some(cell) = buf.cell_mut((x, y)) {
1829 cell.set_symbol("▐");
1830 cell.set_fg(drop_zone_border);
1831 }
1832 }
1833 }
1834 TabDropZone::SplitTop(_) => {
1835 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1837 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
1838 cell.set_symbol("▀");
1839 cell.set_fg(drop_zone_border);
1840 }
1841 }
1842 }
1843 TabDropZone::SplitBottom(_) => {
1844 let y = highlight_area.y + highlight_area.height - 1;
1846 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1847 if let Some(cell) = buf.cell_mut((x, y)) {
1848 cell.set_symbol("▄");
1849 cell.set_fg(drop_zone_border);
1850 }
1851 }
1852 }
1853 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
1854 }
1856 }
1857 }
1858
1859 pub fn recompute_layout(&mut self, width: u16, height: u16) {
1864 let size = ratatui::layout::Rect::new(0, 0, width, height);
1865
1866 let active_split = self.split_manager.active_split();
1868 self.pre_sync_ensure_visible(active_split);
1869 self.sync_scroll_groups();
1870
1871 let constraints = vec![
1874 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
1875 Constraint::Min(0),
1876 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(if self.prompt_line_visible { 1 } else { 0 }), ];
1880 let main_chunks = Layout::default()
1881 .direction(Direction::Vertical)
1882 .constraints(constraints)
1883 .split(size);
1884 let main_content_area = main_chunks[1];
1885
1886 let file_explorer_should_show = self.file_explorer_visible
1888 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
1889 let editor_content_area = if file_explorer_should_show {
1890 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
1891 let horizontal_chunks = Layout::default()
1892 .direction(Direction::Horizontal)
1893 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
1894 .split(main_content_area);
1895 horizontal_chunks[1]
1896 } else {
1897 main_content_area
1898 };
1899
1900 let view_line_mappings = SplitRenderer::compute_content_layout(
1902 editor_content_area,
1903 &self.split_manager,
1904 &mut self.buffers,
1905 &mut self.split_view_states,
1906 &self.theme,
1907 false, self.config.editor.estimated_line_length,
1909 self.config.editor.highlight_context_bytes,
1910 self.config.editor.relative_line_numbers,
1911 self.config.editor.use_terminal_bg,
1912 self.session_mode || !self.software_cursor_only,
1913 self.software_cursor_only,
1914 self.tab_bar_visible,
1915 self.config.editor.show_vertical_scrollbar,
1916 self.config.editor.show_horizontal_scrollbar,
1917 self.config.editor.diagnostics_inline_text,
1918 self.config.editor.show_tilde,
1919 );
1920
1921 self.cached_layout.view_line_mappings = view_line_mappings;
1922 }
1923
1924 pub fn clear_search_history(&mut self) {
1927 if let Some(history) = self.prompt_histories.get_mut("search") {
1928 history.clear();
1929 }
1930 }
1931
1932 fn update_terminal_title(&mut self, display_name: &str) {
1939 if !self.config.editor.set_window_title {
1940 return;
1941 }
1942 let new_title = format!("{} \u{2014} Fresh", display_name);
1943 if self.last_window_title.as_deref() == Some(new_title.as_str()) {
1944 return;
1945 }
1946 crate::services::terminal_title::write_terminal_title(&new_title);
1947 self.last_window_title = Some(new_title);
1948 }
1949
1950 pub fn save_histories(&self) {
1953 if let Err(e) = self
1955 .authority
1956 .filesystem
1957 .create_dir_all(&self.dir_context.data_dir)
1958 {
1959 tracing::warn!("Failed to create data directory: {}", e);
1960 return;
1961 }
1962
1963 for (key, history) in &self.prompt_histories {
1965 let path = self.dir_context.prompt_history_path(key);
1966 if let Err(e) = history.save_to_file(&path) {
1967 tracing::warn!("Failed to save {} history: {}", key, e);
1968 } else {
1969 tracing::debug!("Saved {} history to {:?}", key, path);
1970 }
1971 }
1972 }
1973}