1use super::lsp_status::compose_lsp_status;
2use super::*;
3
4impl Editor {
5 pub fn render(&mut self, frame: &mut Frame) {
7 let _span = tracing::info_span!("render").entered();
8 let size = frame.area();
9
10 self.animations.capture_before_all();
15
16 self.cached_layout.last_frame_width = size.width;
18 self.cached_layout.last_frame_height = size.height;
19
20 self.cached_layout.reset_cell_theme_map();
22
23 self.drain_pending_lsp_prompt_for_active_buffer();
29
30 let active_split = self.split_manager.active_split();
35 {
36 let _span = tracing::info_span!("pre_sync_ensure_visible").entered();
37 self.pre_sync_ensure_visible(active_split);
38 }
39
40 {
43 let _span = tracing::info_span!("sync_scroll_groups").entered();
44 self.sync_scroll_groups();
45 }
46
47 let mut semantic_ranges: std::collections::HashMap<BufferId, (usize, usize)> =
53 std::collections::HashMap::new();
54 {
55 let _span = tracing::info_span!("compute_semantic_ranges").entered();
56 for (split_id, view_state) in &self.split_view_states {
57 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
58 if let Some(state) = self.buffers.get(&buffer_id) {
59 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
60 let visible_lines =
61 view_state.viewport.visible_line_count().saturating_sub(1);
62 let end_line = start_line.saturating_add(visible_lines);
63 semantic_ranges
64 .entry(buffer_id)
65 .and_modify(|(min_start, max_end)| {
66 *min_start = (*min_start).min(start_line);
67 *max_end = (*max_end).max(end_line);
68 })
69 .or_insert((start_line, end_line));
70 }
71 }
72 }
73 }
74 for (buffer_id, (start_line, end_line)) in semantic_ranges {
75 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
76 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
77 self.maybe_request_folding_ranges_debounced(buffer_id);
78 }
79
80 {
81 let _span = tracing::info_span!("prepare_for_render").entered();
82 for (split_id, view_state) in &self.split_view_states {
83 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
84 if let Some(state) = self.buffers.get_mut(&buffer_id) {
85 let top_byte = view_state.viewport.top_byte;
86 let height = view_state.viewport.height;
87 if let Err(e) = state.prepare_for_render(top_byte, height) {
88 tracing::error!("Failed to prepare buffer for render: {}", e);
89 }
91 }
92 }
93 }
94 }
95
96 let is_search_prompt_active = self.prompt.as_ref().is_some_and(|p| {
99 matches!(
100 p.prompt_type,
101 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
102 )
103 });
104 if is_search_prompt_active {
105 if let Some(ref search_state) = self.search_state {
106 let query = search_state.query.clone();
107 self.update_search_highlights(&query);
108 }
109 }
110
111 let show_search_options = self.prompt.as_ref().is_some_and(|p| {
113 matches!(
114 p.prompt_type,
115 PromptType::Search
116 | PromptType::ReplaceSearch
117 | PromptType::Replace { .. }
118 | PromptType::QueryReplaceSearch
119 | PromptType::QueryReplace { .. }
120 )
121 });
122
123 let has_suggestions = self
125 .prompt
126 .as_ref()
127 .is_some_and(|p| !p.suggestions.is_empty());
128 let has_file_browser = self.prompt.as_ref().is_some_and(|p| {
129 matches!(
130 p.prompt_type,
131 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
132 )
133 }) && self.file_open_state.is_some();
134
135 let constraints = vec![
139 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }), Constraint::Min(0), Constraint::Length(
142 if !self.status_bar_visible || has_suggestions || has_file_browser {
143 0
144 } else {
145 1
146 },
147 ), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(if self.prompt_line_visible || self.prompt.is_some() {
150 1
151 } else {
152 0
153 }), ];
155
156 let main_chunks = Layout::default()
157 .direction(Direction::Vertical)
158 .constraints(constraints)
159 .split(size);
160
161 let menu_bar_area = main_chunks[0];
162 let main_content_area = main_chunks[1];
163 let status_bar_idx = 2;
164 let search_options_idx = 3;
165 let prompt_line_idx = 4;
166
167 let editor_content_area;
170 let file_explorer_should_show = self.file_explorer_visible
171 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
172
173 if file_explorer_should_show {
174 tracing::trace!(
176 "render: file explorer layout active (present={}, sync_in_progress={})",
177 self.file_explorer.is_some(),
178 self.file_explorer_sync_in_progress
179 );
180 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
181 let horizontal_chunks = Layout::default()
182 .direction(Direction::Horizontal)
183 .constraints([
184 Constraint::Length(explorer_cols), Constraint::Min(0), ])
187 .split(main_content_area);
188
189 self.cached_layout.file_explorer_area = Some(horizontal_chunks[0]);
190 editor_content_area = horizontal_chunks[1];
191
192 let remote_connection = self.connection_display_string();
194
195 if let Some(ref mut explorer) = self.file_explorer {
197 let is_focused = self.key_context == KeyContext::FileExplorer;
198
199 let mut files_with_unsaved_changes = std::collections::HashSet::new();
201 for (buffer_id, state) in &self.buffers {
202 if state.buffer.is_modified() {
203 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
204 if let Some(file_path) = metadata.file_path() {
205 files_with_unsaved_changes.insert(file_path.clone());
206 }
207 }
208 }
209 }
210
211 let close_button_hovered = matches!(
212 &self.mouse_state.hover_target,
213 Some(HoverTarget::FileExplorerCloseButton)
214 );
215 let keybindings = self.keybindings.read().unwrap();
216 let empty: Vec<std::path::PathBuf> = Vec::new();
217 let cut_paths = self
218 .file_explorer_clipboard
219 .as_ref()
220 .filter(|cb| cb.is_cut)
221 .map(|cb| cb.paths.as_slice())
222 .unwrap_or(empty.as_slice());
223 FileExplorerRenderer::render(
224 explorer,
225 frame,
226 horizontal_chunks[0],
227 is_focused,
228 &files_with_unsaved_changes,
229 &self.file_explorer_decoration_cache,
230 &keybindings,
231 self.key_context.clone(),
232 &self.theme,
233 close_button_hovered,
234 remote_connection.as_deref(),
235 cut_paths,
236 );
237 }
238 } else {
241 self.cached_layout.file_explorer_area = None;
243 editor_content_area = main_content_area;
244 }
245
246 if self.plugin_manager.is_active() {
253 let hooks_start = std::time::Instant::now();
254 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
256
257 let mut total_new_lines = 0usize;
258 for (split_id, buffer_id, split_area) in visible_buffers {
259 let viewport_top_byte = self
261 .split_view_states
262 .get(&split_id)
263 .map(|vs| vs.viewport.top_byte)
264 .unwrap_or(0);
265
266 if let Some(state) = self.buffers.get_mut(&buffer_id) {
267 self.plugin_manager.run_hook(
269 "render_start",
270 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
271 );
272
273 let visible_count = split_area.height as usize;
276 let is_binary = state.buffer.is_binary();
277 let line_ending = state.buffer.line_ending();
278 let base_tokens =
279 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
280 &mut state.buffer,
281 viewport_top_byte,
282 self.config.editor.estimated_line_length,
283 visible_count,
284 is_binary,
285 line_ending,
286 );
287 let viewport_start = viewport_top_byte;
288 let viewport_end = base_tokens
289 .last()
290 .and_then(|t| t.source_offset)
291 .unwrap_or(viewport_start);
292 let cursor_positions: Vec<usize> = self
293 .split_view_states
294 .get(&split_id)
295 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
296 .unwrap_or_default();
297 self.plugin_manager.run_hook(
298 "view_transform_request",
299 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
300 buffer_id,
301 split_id: split_id.into(),
302 viewport_start,
303 viewport_end,
304 tokens: base_tokens,
305 cursor_positions,
306 },
307 );
308
309 if let Some(vs) = self.split_view_states.get_mut(&split_id) {
313 vs.view_transform_stale = false;
314 }
315
316 let visible_count = split_area.height as usize;
318 let top_byte = viewport_top_byte;
319
320 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
322
323 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
325 let mut line_number = state.buffer.get_line_number(top_byte);
326 let mut iter = state
327 .buffer
328 .line_iterator(top_byte, self.config.editor.estimated_line_length);
329
330 for _ in 0..visible_count {
331 if let Some((line_start, line_content)) = iter.next_line() {
332 let byte_end = line_start + line_content.len();
333 let byte_range = (line_start, byte_end);
334
335 if !seen_byte_ranges.contains(&byte_range) {
337 new_lines.push(crate::services::plugins::hooks::LineInfo {
338 line_number,
339 byte_start: line_start,
340 byte_end,
341 content: line_content,
342 });
343 seen_byte_ranges.insert(byte_range);
344 }
345 line_number += 1;
346 } else {
347 break;
348 }
349 }
350
351 if !new_lines.is_empty() {
353 total_new_lines += new_lines.len();
354 self.plugin_manager.run_hook(
355 "lines_changed",
356 crate::services::plugins::hooks::HookArgs::LinesChanged {
357 buffer_id,
358 lines: new_lines,
359 },
360 );
361 }
362 }
363 }
364 let hooks_elapsed = hooks_start.elapsed();
365 tracing::trace!(
366 new_lines = total_new_lines,
367 elapsed_ms = hooks_elapsed.as_millis(),
368 elapsed_us = hooks_elapsed.as_micros(),
369 "lines_changed hooks total"
370 );
371
372 let commands = self.plugin_manager.process_commands();
384 if !commands.is_empty() {
385 let cmd_names: Vec<String> =
386 commands.iter().map(|c| c.debug_variant_name()).collect();
387 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
388 }
389 for command in commands {
390 if let Err(e) = self.handle_plugin_command(command) {
391 tracing::error!("Error handling plugin command: {}", e);
392 }
393 }
394
395 self.flush_pending_grammars();
397 }
398
399 let lsp_waiting = !self.pending_completion_requests.is_empty()
401 || self.pending_goto_definition_request.is_some();
402
403 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
410 let hide_cursor = self.menu_state.active_menu.is_some()
411 || self.key_context == KeyContext::FileExplorer
412 || self.terminal_mode
413 || settings_visible
414 || self.keybinding_editor.is_some();
415
416 let hovered_tab = match &self.mouse_state.hover_target {
418 Some(HoverTarget::TabName(target, split_id)) => Some((*target, *split_id, false)),
419 Some(HoverTarget::TabCloseButton(target, split_id)) => Some((*target, *split_id, true)),
420 _ => None,
421 };
422
423 let hovered_close_split = match &self.mouse_state.hover_target {
425 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
426 _ => None,
427 };
428
429 let hovered_maximize_split = match &self.mouse_state.hover_target {
431 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
432 _ => None,
433 };
434
435 let is_maximized = self.split_manager.is_maximized();
436
437 let mut pending_hardware_cursor: Option<(u16, u16)> = None;
444
445 let _content_span = tracing::info_span!("render_content").entered();
446 let (
447 split_areas,
448 tab_layouts,
449 close_split_areas,
450 maximize_split_areas,
451 view_line_mappings,
452 horizontal_scrollbar_areas,
453 grouped_separator_areas,
454 ) = SplitRenderer::render_content(
455 frame,
456 editor_content_area,
457 &self.split_manager,
458 &mut self.buffers,
459 &self.buffer_metadata,
460 &mut self.event_logs,
461 &mut self.composite_buffers,
462 &mut self.composite_view_states,
463 &self.theme,
464 self.ansi_background.as_ref(),
465 self.background_fade,
466 lsp_waiting,
467 self.config.editor.large_file_threshold_bytes,
468 self.config.editor.line_wrap,
469 self.config.editor.estimated_line_length,
470 self.config.editor.highlight_context_bytes,
471 Some(&mut self.split_view_states),
472 &self.grouped_subtrees,
473 hide_cursor,
474 hovered_tab,
475 hovered_close_split,
476 hovered_maximize_split,
477 is_maximized,
478 self.config.editor.relative_line_numbers,
479 self.tab_bar_visible,
480 self.config.editor.use_terminal_bg,
481 self.session_mode || !self.software_cursor_only,
482 self.software_cursor_only,
483 self.config.editor.show_vertical_scrollbar,
484 self.config.editor.show_horizontal_scrollbar,
485 self.config.editor.diagnostics_inline_text,
486 self.config.editor.show_tilde,
487 self.config.editor.highlight_current_column,
488 &mut self.cached_layout.cell_theme_map,
489 size.width,
490 &mut pending_hardware_cursor,
491 );
492
493 drop(_content_span);
494
495 self.maybe_start_cursor_jump_animation(pending_hardware_cursor, active_split);
501
502 if self.plugin_manager.is_active() {
506 for (split_id, view_state) in &self.split_view_states {
507 let current = (
508 view_state.viewport.top_byte,
509 view_state.viewport.width,
510 view_state.viewport.height,
511 );
512 let (changed, previous) = match self.previous_viewports.get(split_id) {
517 Some(previous) => (*previous != current, Some(*previous)),
518 None => (false, None), };
520 tracing::trace!(
521 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
522 split_id,
523 current,
524 previous,
525 changed
526 );
527 if changed {
528 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
529 let top_line = self.buffers.get(&buffer_id).and_then(|state| {
531 if state.buffer.line_count().is_some() {
532 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
533 } else {
534 None
535 }
536 });
537 tracing::debug!(
538 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
539 split_id,
540 buffer_id,
541 view_state.viewport.top_byte,
542 top_line
543 );
544 self.plugin_manager.run_hook(
545 "viewport_changed",
546 crate::services::plugins::hooks::HookArgs::ViewportChanged {
547 split_id: (*split_id).into(),
548 buffer_id,
549 top_byte: view_state.viewport.top_byte,
550 top_line,
551 width: view_state.viewport.width,
552 height: view_state.viewport.height,
553 },
554 );
555 }
556 }
557 }
558 }
559
560 self.previous_viewports.clear();
562 for (split_id, view_state) in &self.split_view_states {
563 self.previous_viewports.insert(
564 *split_id,
565 (
566 view_state.viewport.top_byte,
567 view_state.viewport.width,
568 view_state.viewport.height,
569 ),
570 );
571 }
572
573 self.render_terminal_splits(frame, &split_areas);
575
576 self.cached_layout.split_areas = split_areas;
577 self.cached_layout.horizontal_scrollbar_areas = horizontal_scrollbar_areas;
578 self.cached_layout.tab_layouts = tab_layouts;
579 self.cached_layout.close_split_areas = close_split_areas;
580 self.cached_layout.maximize_split_areas = maximize_split_areas;
581 self.cached_layout.view_line_mappings = view_line_mappings;
582
583 self.drain_pending_vb_animations();
588 let mut separator_areas = self
589 .split_manager
590 .get_separators_with_ids(editor_content_area);
591 separator_areas.extend(grouped_separator_areas);
596 self.cached_layout.separator_areas = separator_areas;
597 self.cached_layout.editor_content_area = Some(editor_content_area);
598
599 self.render_hover_highlights(frame);
601
602 self.cached_layout.suggestions_area = None;
604 self.file_browser_layout = None;
605
606 let display_name = self
608 .buffer_metadata
609 .get(&self.active_buffer())
610 .map(|m| m.display_name.clone())
611 .unwrap_or_else(|| "[No Name]".to_string());
612
613 self.update_terminal_title(&display_name);
617
618 let status_message = self.status_message.clone();
619 let plugin_status_message = self.plugin_status_message.clone();
620 let prompt = self.prompt.clone();
621 let current_language = self
644 .buffers
645 .get(&self.active_buffer())
646 .map(|s| s.language.clone())
647 .unwrap_or_default();
648 let buffer_lsp_disabled_reason = self
649 .buffer_metadata
650 .get(&self.active_buffer())
651 .filter(|m| !m.lsp_enabled)
652 .and_then(|m| m.lsp_disabled_reason.as_deref());
653 let (lsp_status, lsp_indicator_state) = compose_lsp_status(
654 ¤t_language,
655 buffer_lsp_disabled_reason,
656 &self.lsp_progress,
657 &self.lsp_server_statuses,
658 &self.config.lsp,
659 &self.user_dismissed_lsp_languages,
660 );
661 let theme = self.theme.clone();
662 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());
667
668 if self.status_bar_visible && !has_suggestions && !has_file_browser {
670 let (warning_level, general_warning_count) =
673 if self.config.warnings.show_status_indicator {
674 let lsp_level = {
675 use crate::services::async_bridge::LspServerStatus;
676 let mut level = WarningLevel::None;
677 for ((lang, _), status) in &self.lsp_server_statuses {
678 if lang == ¤t_language {
679 match status {
680 LspServerStatus::Error => {
681 level = WarningLevel::Error;
682 break;
683 }
684 LspServerStatus::Starting | LspServerStatus::Initializing => {
685 if level != WarningLevel::Error {
686 level = WarningLevel::Warning;
687 }
688 }
689 _ => {}
690 }
691 }
692 }
693 level
694 };
695 (lsp_level, self.get_general_warning_count())
696 } else {
697 (WarningLevel::None, 0)
698 };
699
700 use crate::view::ui::status_bar::StatusBarHover;
702 let status_bar_hover = match &self.mouse_state.hover_target {
703 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
704 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
705 Some(HoverTarget::StatusBarLineEndingIndicator) => {
706 StatusBarHover::LineEndingIndicator
707 }
708 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
709 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
710 Some(HoverTarget::StatusBarRemoteIndicator) => StatusBarHover::RemoteIndicator,
711 _ => StatusBarHover::None,
712 };
713
714 let remote_connection = self.connection_display_string();
715
716 let session_name = self.session_name().map(|s| s.to_string());
718
719 let active_split = self.effective_active_split();
720 let active_buf = self.active_buffer();
721 let default_cursors = crate::model::cursor::Cursors::new();
722 let status_cursors = self
723 .split_view_states
724 .get(&active_split)
725 .map(|vs| &vs.cursors)
726 .unwrap_or(&default_cursors);
727 let is_read_only = self
728 .buffer_metadata
729 .get(&active_buf)
730 .map(|m| m.read_only)
731 .unwrap_or(false);
732 let mut status_ctx = crate::view::ui::status_bar::StatusBarContext {
733 state: self.buffers.get_mut(&active_buf).unwrap(),
734 cursors: status_cursors,
735 status_message: &status_message,
736 plugin_status_message: &plugin_status_message,
737 lsp_status: &lsp_status,
738 lsp_indicator_state,
739 theme: &theme,
740 display_name: &display_name,
741 keybindings: &keybindings_cloned,
742 chord_state: &chord_state_cloned,
743 update_available: update_available.as_deref(),
744 warning_level,
745 general_warning_count,
746 hover: status_bar_hover,
747 remote_connection: remote_connection.as_deref(),
748 session_name: session_name.as_deref(),
749 read_only: is_read_only,
750 remote_state_override: self.remote_indicator_override.as_ref(),
751 remote_indicator_on_bar: false,
756 };
757 let status_bar_layout = StatusBarRenderer::render_status_bar(
758 frame,
759 main_chunks[status_bar_idx],
760 &mut status_ctx,
761 &self.config.editor.status_bar,
762 );
763
764 let status_bar_area = main_chunks[status_bar_idx];
766 self.cached_layout.status_bar_area =
767 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
768 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
769 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
770 self.cached_layout.status_bar_line_ending_area =
771 status_bar_layout.line_ending_indicator;
772 self.cached_layout.status_bar_encoding_area = status_bar_layout.encoding_indicator;
773 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
774 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
775 self.cached_layout.status_bar_remote_area = status_bar_layout.remote_indicator;
776 }
777
778 if show_search_options {
780 let confirm_each = self.prompt.as_ref().and_then(|p| {
782 if matches!(
783 p.prompt_type,
784 PromptType::ReplaceSearch
785 | PromptType::Replace { .. }
786 | PromptType::QueryReplaceSearch
787 | PromptType::QueryReplace { .. }
788 ) {
789 Some(self.search_confirm_each)
790 } else {
791 None
792 }
793 });
794
795 use crate::view::ui::status_bar::SearchOptionsHover;
797 let search_options_hover = match &self.mouse_state.hover_target {
798 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
799 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
800 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
801 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
802 _ => SearchOptionsHover::None,
803 };
804
805 let search_options_layout = StatusBarRenderer::render_search_options(
806 frame,
807 main_chunks[search_options_idx],
808 self.search_case_sensitive,
809 self.search_whole_word,
810 self.search_use_regex,
811 confirm_each,
812 &theme,
813 &keybindings_cloned,
814 search_options_hover,
815 );
816 self.cached_layout.search_options_layout = Some(search_options_layout);
817 } else {
818 self.cached_layout.search_options_layout = None;
819 }
820
821 if let Some(prompt) = &prompt {
823 if matches!(
825 prompt.prompt_type,
826 crate::view::prompt::PromptType::OpenFile
827 | crate::view::prompt::PromptType::SwitchProject
828 ) {
829 if let Some(file_open_state) = &self.file_open_state {
830 StatusBarRenderer::render_file_open_prompt(
831 frame,
832 main_chunks[prompt_line_idx],
833 prompt,
834 file_open_state,
835 &theme,
836 );
837 } else {
838 StatusBarRenderer::render_prompt(
839 frame,
840 main_chunks[prompt_line_idx],
841 prompt,
842 &theme,
843 );
844 }
845 } else {
846 StatusBarRenderer::render_prompt(
847 frame,
848 main_chunks[prompt_line_idx],
849 prompt,
850 &theme,
851 );
852 }
853 }
854
855 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
858
859 let theme_clone = self.theme.clone();
862 let hover_target = self.mouse_state.hover_target.clone();
863
864 self.cached_layout.popup_areas.clear();
866
867 let popup_info: Vec<_> = {
869 let active_split = self.split_manager.active_split();
871 let viewport = self
872 .split_view_states
873 .get(&active_split)
874 .map(|vs| vs.viewport.clone());
875
876 let content_rect = self
881 .cached_layout
882 .split_areas
883 .iter()
884 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
885 .map(|(_, _, rect, _, _, _)| *rect);
886
887 let primary_cursor = self
888 .split_view_states
889 .get(&active_split)
890 .map(|vs| *vs.cursors.primary());
891 let state = self.active_state_mut();
892 if state.popups.is_visible() {
893 let primary_cursor =
895 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
896
897 let gutter_width = viewport
899 .as_ref()
900 .map(|vp| vp.gutter_width(&state.buffer) as u16)
901 .unwrap_or(0);
902
903 let cursor_screen_pos = viewport
904 .as_ref()
905 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
906 .unwrap_or((0, 0));
907
908 let word_start_screen_pos = {
912 use crate::primitives::word_navigation::find_completion_word_start;
913 let word_start =
914 find_completion_word_start(&state.buffer, primary_cursor.position);
915 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
916 viewport
917 .as_ref()
918 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
919 .unwrap_or((0, 0))
920 };
921
922 let (base_x, base_y) = content_rect
927 .map(|r| (r.x + gutter_width, r.y))
928 .unwrap_or((gutter_width, 1));
929
930 let cursor_screen_pos =
931 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
932 let word_start_screen_pos = (
933 word_start_screen_pos.0 + base_x,
934 word_start_screen_pos.1 + base_y,
935 );
936
937 state
939 .popups
940 .all()
941 .iter()
942 .enumerate()
943 .map(|(popup_idx, popup)| {
944 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
946 (word_start_screen_pos.0, cursor_screen_pos.1)
947 } else {
948 cursor_screen_pos
949 };
950 let popup_area = popup.calculate_area(size, Some(popup_pos));
951
952 let desc_height = popup.description_height();
955 let inner_area = if popup.bordered {
956 ratatui::layout::Rect {
957 x: popup_area.x + 1,
958 y: popup_area.y + 1 + desc_height,
959 width: popup_area.width.saturating_sub(2),
960 height: popup_area.height.saturating_sub(2 + desc_height),
961 }
962 } else {
963 ratatui::layout::Rect {
964 x: popup_area.x,
965 y: popup_area.y + desc_height,
966 width: popup_area.width,
967 height: popup_area.height.saturating_sub(desc_height),
968 }
969 };
970
971 let num_items = match &popup.content {
972 crate::view::popup::PopupContent::List { items, .. } => items.len(),
973 _ => 0,
974 };
975
976 let total_lines = popup.item_count();
978 let visible_lines = inner_area.height as usize;
979 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
980 {
981 Some(ratatui::layout::Rect {
982 x: inner_area.x + inner_area.width - 1,
983 y: inner_area.y,
984 width: 1,
985 height: inner_area.height,
986 })
987 } else {
988 None
989 };
990
991 (
992 popup_idx,
993 popup_area,
994 inner_area,
995 popup.scroll_offset,
996 num_items,
997 scrollbar_rect,
998 total_lines,
999 )
1000 })
1001 .collect()
1002 } else {
1003 Vec::new()
1004 }
1005 };
1006
1007 self.cached_layout.popup_areas = popup_info.clone();
1009
1010 let state = self.active_state_mut();
1012 if state.popups.is_visible() {
1013 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
1014 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
1015 popup.render_with_hover(
1016 frame,
1017 *popup_area,
1018 &theme_clone,
1019 hover_target.as_ref(),
1020 );
1021 }
1022 }
1023 }
1024
1025 self.cached_layout.global_popup_areas.clear();
1036 if let Some(popup) = self.global_popups.top() {
1037 let top_idx = self.global_popups.all().len() - 1;
1038 let popup_area = popup.calculate_area(size, None);
1039 let desc_height = popup.description_height();
1040 let inner_area = if popup.bordered {
1041 ratatui::layout::Rect {
1042 x: popup_area.x + 1,
1043 y: popup_area.y + 1 + desc_height,
1044 width: popup_area.width.saturating_sub(2),
1045 height: popup_area.height.saturating_sub(2 + desc_height),
1046 }
1047 } else {
1048 ratatui::layout::Rect {
1049 x: popup_area.x,
1050 y: popup_area.y + desc_height,
1051 width: popup_area.width,
1052 height: popup_area.height.saturating_sub(desc_height),
1053 }
1054 };
1055 let num_items = match &popup.content {
1056 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1057 _ => 0,
1058 };
1059 self.cached_layout.global_popup_areas.push((
1060 top_idx,
1061 popup_area,
1062 inner_area,
1063 popup.scroll_offset,
1064 num_items,
1065 ));
1066 popup.render_with_hover(frame, popup_area, &theme_clone, hover_target.as_ref());
1067 }
1068
1069 self.update_menu_context();
1072
1073 let settings_visible = self
1076 .settings_state
1077 .as_ref()
1078 .map(|s| s.visible)
1079 .unwrap_or(false);
1080 if settings_visible {
1081 crate::view::dimming::apply_dimming(frame, size);
1083 }
1084 if let Some(ref mut settings_state) = self.settings_state {
1085 if settings_state.visible {
1086 settings_state.update_focus_states();
1087 let settings_layout = crate::view::settings::render_settings(
1088 frame,
1089 size,
1090 settings_state,
1091 &self.theme,
1092 );
1093 self.cached_layout.settings_layout = Some(settings_layout);
1094 }
1095 }
1096
1097 if let Some(ref wizard) = self.calibration_wizard {
1099 crate::view::dimming::apply_dimming(frame, size);
1101 crate::view::calibration_wizard::render_calibration_wizard(
1102 frame,
1103 size,
1104 wizard,
1105 &self.theme,
1106 );
1107 }
1108
1109 if let Some(ref mut kb_editor) = self.keybinding_editor {
1111 crate::view::dimming::apply_dimming(frame, size);
1112 crate::view::keybinding_editor::render_keybinding_editor(
1113 frame,
1114 size,
1115 kb_editor,
1116 &self.theme,
1117 );
1118 }
1119
1120 if let Some(ref debug) = self.event_debug {
1122 crate::view::dimming::apply_dimming(frame, size);
1124 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
1125 }
1126
1127 if self.menu_bar_visible {
1128 let keybindings = self.keybindings.read().unwrap();
1129 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
1130 frame,
1131 menu_bar_area,
1132 &self.menus,
1133 &self.menu_state,
1134 &keybindings,
1135 &self.theme,
1136 self.mouse_state.hover_target.as_ref(),
1137 self.config.editor.menu_bar_mnemonics,
1138 ));
1139 } else {
1140 self.cached_layout.menu_layout = None;
1141 }
1142
1143 if let Some(ref menu) = self.tab_context_menu {
1145 self.render_tab_context_menu(frame, menu);
1146 }
1147
1148 if let Some(ref menu) = self.file_explorer_context_menu {
1149 self.render_file_explorer_context_menu(frame, menu);
1150 }
1151
1152 self.record_non_editor_theme_regions();
1154
1155 self.render_theme_info_popup(frame);
1157
1158 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
1160 if drag_state.is_dragging() {
1161 self.render_tab_drop_zone(frame, drag_state);
1162 }
1163 }
1164
1165 if self.gpm_active {
1171 if let Some((col, row)) = self.mouse_cursor_position {
1172 use ratatui::style::Modifier;
1173
1174 if col < size.width && row < size.height {
1176 let buf = frame.buffer_mut();
1178 if let Some(cell) = buf.cell_mut((col, row)) {
1179 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1180 }
1181 }
1182 }
1183 }
1184
1185 if self.keyboard_capture && self.terminal_mode {
1188 let active_split = self.split_manager.active_split();
1190 let active_split_area = self
1191 .cached_layout
1192 .split_areas
1193 .iter()
1194 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1195 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1196
1197 if let Some(terminal_area) = active_split_area {
1198 self.apply_keyboard_capture_dimming(frame, terminal_area);
1199 }
1200 }
1201
1202 if let Some((cx, cy)) = pending_hardware_cursor {
1213 if self.prompt.is_none() && !self.cursor_obscured_by_overlay(cx, cy) {
1214 frame.set_cursor_position((cx, cy));
1215 }
1216 }
1217
1218 crate::view::color_support::convert_buffer_colors(
1220 frame.buffer_mut(),
1221 self.color_capability,
1222 );
1223
1224 self.animations.apply_all(frame.buffer_mut());
1226 }
1227
1228 fn maybe_start_cursor_jump_animation(
1243 &mut self,
1244 current_pos: Option<(u16, u16)>,
1245 active_split: crate::model::event::LeafId,
1246 ) {
1247 if !self.config.editor.animations {
1252 self.previous_cursor_screen_pos = current_pos.map(|p| (p, active_split));
1253 return;
1254 }
1255
1256 let Some(current) = current_pos else {
1257 self.previous_cursor_screen_pos = None;
1261 return;
1262 };
1263
1264 let prev_entry = self.previous_cursor_screen_pos;
1265 self.previous_cursor_screen_pos = Some((current, active_split));
1267
1268 let Some((prev, prev_split)) = prev_entry else {
1269 return;
1270 };
1271 if prev == current && prev_split == active_split {
1272 return;
1273 }
1274
1275 let dx = (current.0 as i32 - prev.0 as i32).abs();
1276 let dy = (current.1 as i32 - prev.1 as i32).abs();
1277 let crossed_panes = prev_split != active_split;
1283 let row_jump = dy > 2;
1284 let col_jump = dx >= 10;
1285 if !crossed_panes && !row_jump && !col_jump {
1286 return;
1287 }
1288
1289 if let Some(prev_anim) = self.cursor_jump_animation.take() {
1291 self.animations.cancel(prev_anim);
1292 }
1293
1294 let id = self.animations.start(
1295 ratatui::layout::Rect {
1298 x: prev.0.min(current.0),
1299 y: prev.1.min(current.1),
1300 width: dx as u16 + 1,
1301 height: dy as u16 + 1,
1302 },
1303 crate::view::animation::AnimationKind::CursorJump {
1304 from: prev,
1305 to: current,
1306 duration: std::time::Duration::from_millis(140),
1307 cursor_color: self.theme.cursor,
1308 bg_color: self.theme.editor_bg,
1309 },
1310 );
1311 self.cursor_jump_animation = Some(id);
1312 }
1313
1314 fn cursor_obscured_by_overlay(&self, x: u16, y: u16) -> bool {
1318 let inside = |rect: ratatui::layout::Rect| -> bool {
1319 x >= rect.x
1320 && x < rect.x.saturating_add(rect.width)
1321 && y >= rect.y
1322 && y < rect.y.saturating_add(rect.height)
1323 };
1324
1325 if self
1326 .cached_layout
1327 .popup_areas
1328 .iter()
1329 .any(|entry| inside(entry.1))
1330 {
1331 return true;
1332 }
1333 if self
1334 .cached_layout
1335 .global_popup_areas
1336 .iter()
1337 .any(|entry| inside(entry.1))
1338 {
1339 return true;
1340 }
1341 if let Some((rect, _, _, _)) = self.cached_layout.suggestions_area {
1342 if inside(rect) {
1343 return true;
1344 }
1345 }
1346 if let Some(ref fb) = self.file_browser_layout {
1347 if inside(fb.popup_area) {
1348 return true;
1349 }
1350 }
1351 false
1352 }
1353
1354 fn render_quick_open_hints(
1356 frame: &mut Frame,
1357 area: ratatui::layout::Rect,
1358 theme: &crate::view::theme::Theme,
1359 ) {
1360 use ratatui::style::{Modifier, Style};
1361 use ratatui::text::{Line, Span};
1362 use ratatui::widgets::Paragraph;
1363 use rust_i18n::t;
1364
1365 let hints_style = Style::default()
1366 .fg(theme.line_number_fg)
1367 .bg(theme.suggestion_selected_bg)
1368 .add_modifier(Modifier::DIM);
1369 let hints_text = t!("quick_open.mode_hints");
1370 let left_margin = 2;
1372 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1373 let mut spans = Vec::new();
1374 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1375 spans.push(Span::styled(hints_text.to_string(), hints_style));
1376 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1377 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1378
1379 let paragraph = Paragraph::new(Line::from(spans));
1380 frame.render_widget(paragraph, area);
1381 }
1382
1383 fn apply_keyboard_capture_dimming(
1386 &self,
1387 frame: &mut Frame,
1388 terminal_area: ratatui::layout::Rect,
1389 ) {
1390 let size = frame.area();
1391 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1392 }
1393
1394 fn render_prompt_popups(
1397 &mut self,
1398 frame: &mut Frame,
1399 prompt_area: ratatui::layout::Rect,
1400 width: u16,
1401 ) {
1402 let Some(prompt) = &self.prompt else { return };
1403
1404 if matches!(
1405 prompt.prompt_type,
1406 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1407 ) {
1408 let Some(file_open_state) = &self.file_open_state else {
1409 return;
1410 };
1411 let max_height = prompt_area.y.saturating_sub(1).min(20);
1412 let popup_area = ratatui::layout::Rect {
1413 x: 0,
1414 y: prompt_area.y.saturating_sub(max_height),
1415 width,
1416 height: max_height,
1417 };
1418 let keybindings = self.keybindings.read().unwrap();
1419 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1420 frame,
1421 popup_area,
1422 file_open_state,
1423 &self.theme,
1424 &self.mouse_state.hover_target,
1425 Some(&*keybindings),
1426 );
1427 return;
1428 }
1429
1430 if prompt.suggestions.is_empty() {
1431 return;
1432 }
1433
1434 let suggestion_count = prompt.suggestions.len().min(10);
1435 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1436 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1437 let height = suggestion_count as u16 + 2 + hints_height;
1438
1439 let suggestions_area = ratatui::layout::Rect {
1440 x: 0,
1441 y: prompt_area.y.saturating_sub(height),
1442 width,
1443 height: height - hints_height,
1444 };
1445
1446 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1447
1448 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
1449 frame,
1450 suggestions_area,
1451 prompt,
1452 &self.theme,
1453 self.mouse_state.hover_target.as_ref(),
1454 );
1455
1456 if is_quick_open {
1457 let hints_area = ratatui::layout::Rect {
1458 x: 0,
1459 y: prompt_area.y.saturating_sub(hints_height),
1460 width,
1461 height: hints_height,
1462 };
1463 frame.render_widget(ratatui::widgets::Clear, hints_area);
1464 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1465 }
1466 }
1467
1468 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
1470 use ratatui::style::Style;
1471 use ratatui::text::Span;
1472 use ratatui::widgets::Paragraph;
1473
1474 match &self.mouse_state.hover_target {
1475 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
1476 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
1478 if sid == split_id && dir == direction {
1479 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1480 match dir {
1481 SplitDirection::Horizontal => {
1482 let line_text = "─".repeat(*length as usize);
1483 let paragraph =
1484 Paragraph::new(Span::styled(line_text, hover_style));
1485 frame.render_widget(
1486 paragraph,
1487 ratatui::layout::Rect::new(*x, *y, *length, 1),
1488 );
1489 }
1490 SplitDirection::Vertical => {
1491 for offset in 0..*length {
1492 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1493 frame.render_widget(
1494 paragraph,
1495 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
1496 );
1497 }
1498 }
1499 }
1500 }
1501 }
1502 }
1503 Some(HoverTarget::ScrollbarThumb(split_id)) => {
1504 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1506 &self.cached_layout.split_areas
1507 {
1508 if sid == split_id {
1509 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
1510 for row_offset in *thumb_start..*thumb_end {
1511 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
1512 frame.render_widget(
1513 paragraph,
1514 ratatui::layout::Rect::new(
1515 scrollbar_rect.x,
1516 scrollbar_rect.y + row_offset as u16,
1517 1,
1518 1,
1519 ),
1520 );
1521 }
1522 }
1523 }
1524 }
1525 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
1526 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
1528 &self.cached_layout.split_areas
1529 {
1530 if sid == split_id {
1531 let track_hover_style =
1532 Style::default().bg(self.theme.scrollbar_track_hover_fg);
1533 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
1534 frame.render_widget(
1535 paragraph,
1536 ratatui::layout::Rect::new(
1537 scrollbar_rect.x,
1538 scrollbar_rect.y + hovered_row,
1539 1,
1540 1,
1541 ),
1542 );
1543 }
1544 }
1545 }
1546 Some(HoverTarget::FileExplorerBorder) => {
1547 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1549 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1550 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1551 for row_offset in 0..explorer_area.height {
1552 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1553 frame.render_widget(
1554 paragraph,
1555 ratatui::layout::Rect::new(
1556 border_x,
1557 explorer_area.y + row_offset,
1558 1,
1559 1,
1560 ),
1561 );
1562 }
1563 }
1564 }
1565 _ => {}
1567 }
1568 }
1569
1570 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
1572 use ratatui::style::Style;
1573 use ratatui::text::{Line, Span};
1574 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1575
1576 let items = super::types::TabContextMenuItem::all();
1577 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
1582 let screen_height = frame.area().height;
1583
1584 let menu_x = if menu.position.0 + menu_width > screen_width {
1585 screen_width.saturating_sub(menu_width)
1586 } else {
1587 menu.position.0
1588 };
1589
1590 let menu_y = if menu.position.1 + menu_height > screen_height {
1591 screen_height.saturating_sub(menu_height)
1592 } else {
1593 menu.position.1
1594 };
1595
1596 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1597
1598 frame.render_widget(Clear, area);
1600
1601 let mut lines = Vec::new();
1603 for (idx, item) in items.iter().enumerate() {
1604 let is_highlighted = idx == menu.highlighted;
1605
1606 let style = if is_highlighted {
1607 Style::default()
1608 .fg(self.theme.menu_highlight_fg)
1609 .bg(self.theme.menu_highlight_bg)
1610 } else {
1611 Style::default()
1612 .fg(self.theme.menu_dropdown_fg)
1613 .bg(self.theme.menu_dropdown_bg)
1614 };
1615
1616 let label = item.label();
1618 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1620
1621 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1622 }
1623
1624 let block = Block::default()
1625 .borders(Borders::ALL)
1626 .border_style(Style::default().fg(self.theme.menu_border_fg))
1627 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1628
1629 let paragraph = Paragraph::new(lines).block(block);
1630 frame.render_widget(paragraph, area);
1631 }
1632
1633 fn render_file_explorer_context_menu(
1635 &self,
1636 frame: &mut Frame,
1637 menu: &super::types::FileExplorerContextMenu,
1638 ) {
1639 use ratatui::style::Style;
1640 use ratatui::text::{Line, Span};
1641 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1642
1643 let items = menu.items();
1644 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
1645 let menu_height = menu.height();
1646 let (menu_x, menu_y) = menu.clamped_position(frame.area().width, frame.area().height);
1647
1648 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1649
1650 frame.render_widget(Clear, area);
1651
1652 let mut lines = Vec::new();
1653 for (idx, item) in items.iter().enumerate() {
1654 let is_highlighted = idx == menu.highlighted;
1655
1656 let style = if is_highlighted {
1657 Style::default()
1658 .fg(self.theme.menu_highlight_fg)
1659 .bg(self.theme.menu_highlight_bg)
1660 } else {
1661 Style::default()
1662 .fg(self.theme.menu_dropdown_fg)
1663 .bg(self.theme.menu_dropdown_bg)
1664 };
1665
1666 let label = item.label();
1667 let content_width = (menu_width as usize).saturating_sub(2);
1668 let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1669
1670 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1671 }
1672
1673 let block = Block::default()
1674 .borders(Borders::ALL)
1675 .border_style(Style::default().fg(self.theme.menu_border_fg))
1676 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1677
1678 let paragraph = Paragraph::new(lines).block(block);
1679 frame.render_widget(paragraph, area);
1680 }
1681
1682 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
1684 use ratatui::style::Modifier;
1685
1686 let Some(ref drop_zone) = drag_state.drop_zone else {
1687 return;
1688 };
1689
1690 let split_id = drop_zone.split_id();
1691
1692 let split_area = self
1694 .cached_layout
1695 .split_areas
1696 .iter()
1697 .find(|(sid, _, _, _, _, _)| *sid == split_id)
1698 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1699
1700 let Some(content_rect) = split_area else {
1701 return;
1702 };
1703
1704 use super::types::TabDropZone;
1706
1707 let highlight_area = match drop_zone {
1708 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
1709 content_rect
1712 }
1713 TabDropZone::SplitLeft(_) => {
1714 let width = (content_rect.width / 2).max(3);
1716 ratatui::layout::Rect::new(
1717 content_rect.x,
1718 content_rect.y,
1719 width,
1720 content_rect.height,
1721 )
1722 }
1723 TabDropZone::SplitRight(_) => {
1724 let width = (content_rect.width / 2).max(3);
1726 let x = content_rect.x + content_rect.width - width;
1727 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
1728 }
1729 TabDropZone::SplitTop(_) => {
1730 let height = (content_rect.height / 2).max(2);
1732 ratatui::layout::Rect::new(
1733 content_rect.x,
1734 content_rect.y,
1735 content_rect.width,
1736 height,
1737 )
1738 }
1739 TabDropZone::SplitBottom(_) => {
1740 let height = (content_rect.height / 2).max(2);
1742 let y = content_rect.y + content_rect.height - height;
1743 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
1744 }
1745 };
1746
1747 let buf = frame.buffer_mut();
1750 let drop_zone_bg = self.theme.tab_drop_zone_bg;
1751 let drop_zone_border = self.theme.tab_drop_zone_border;
1752
1753 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1755 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1756 if let Some(cell) = buf.cell_mut((x, y)) {
1757 cell.set_bg(drop_zone_bg);
1760
1761 let is_border = x == highlight_area.x
1763 || x == highlight_area.x + highlight_area.width - 1
1764 || y == highlight_area.y
1765 || y == highlight_area.y + highlight_area.height - 1;
1766
1767 if is_border {
1768 cell.set_fg(drop_zone_border);
1769 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
1770 }
1771 }
1772 }
1773 }
1774
1775 match drop_zone {
1777 TabDropZone::SplitLeft(_) => {
1778 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1780 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
1781 cell.set_symbol("▌");
1782 cell.set_fg(drop_zone_border);
1783 }
1784 }
1785 }
1786 TabDropZone::SplitRight(_) => {
1787 let x = highlight_area.x + highlight_area.width - 1;
1789 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1790 if let Some(cell) = buf.cell_mut((x, y)) {
1791 cell.set_symbol("▐");
1792 cell.set_fg(drop_zone_border);
1793 }
1794 }
1795 }
1796 TabDropZone::SplitTop(_) => {
1797 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1799 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
1800 cell.set_symbol("▀");
1801 cell.set_fg(drop_zone_border);
1802 }
1803 }
1804 }
1805 TabDropZone::SplitBottom(_) => {
1806 let y = highlight_area.y + highlight_area.height - 1;
1808 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1809 if let Some(cell) = buf.cell_mut((x, y)) {
1810 cell.set_symbol("▄");
1811 cell.set_fg(drop_zone_border);
1812 }
1813 }
1814 }
1815 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
1816 }
1818 }
1819 }
1820
1821 pub fn recompute_layout(&mut self, width: u16, height: u16) {
1826 let size = ratatui::layout::Rect::new(0, 0, width, height);
1827
1828 let active_split = self.split_manager.active_split();
1830 self.pre_sync_ensure_visible(active_split);
1831 self.sync_scroll_groups();
1832
1833 let constraints = vec![
1836 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
1837 Constraint::Min(0),
1838 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(if self.prompt_line_visible { 1 } else { 0 }), ];
1842 let main_chunks = Layout::default()
1843 .direction(Direction::Vertical)
1844 .constraints(constraints)
1845 .split(size);
1846 let main_content_area = main_chunks[1];
1847
1848 let file_explorer_should_show = self.file_explorer_visible
1850 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
1851 let editor_content_area = if file_explorer_should_show {
1852 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
1853 let horizontal_chunks = Layout::default()
1854 .direction(Direction::Horizontal)
1855 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
1856 .split(main_content_area);
1857 horizontal_chunks[1]
1858 } else {
1859 main_content_area
1860 };
1861
1862 let view_line_mappings = SplitRenderer::compute_content_layout(
1864 editor_content_area,
1865 &self.split_manager,
1866 &mut self.buffers,
1867 &mut self.split_view_states,
1868 &self.theme,
1869 false, self.config.editor.estimated_line_length,
1871 self.config.editor.highlight_context_bytes,
1872 self.config.editor.relative_line_numbers,
1873 self.config.editor.use_terminal_bg,
1874 self.session_mode || !self.software_cursor_only,
1875 self.software_cursor_only,
1876 self.tab_bar_visible,
1877 self.config.editor.show_vertical_scrollbar,
1878 self.config.editor.show_horizontal_scrollbar,
1879 self.config.editor.diagnostics_inline_text,
1880 self.config.editor.show_tilde,
1881 );
1882
1883 self.cached_layout.view_line_mappings = view_line_mappings;
1884 }
1885
1886 pub fn clear_search_history(&mut self) {
1889 if let Some(history) = self.prompt_histories.get_mut("search") {
1890 history.clear();
1891 }
1892 }
1893
1894 fn update_terminal_title(&mut self, display_name: &str) {
1901 if !self.config.editor.set_window_title {
1902 return;
1903 }
1904 let new_title = format!("{} \u{2014} Fresh", display_name);
1905 if self.last_window_title.as_deref() == Some(new_title.as_str()) {
1906 return;
1907 }
1908 crate::services::terminal_title::write_terminal_title(&new_title);
1909 self.last_window_title = Some(new_title);
1910 }
1911
1912 pub fn save_histories(&self) {
1915 if let Err(e) = self
1917 .authority
1918 .filesystem
1919 .create_dir_all(&self.dir_context.data_dir)
1920 {
1921 tracing::warn!("Failed to create data directory: {}", e);
1922 return;
1923 }
1924
1925 for (key, history) in &self.prompt_histories {
1927 let path = self.dir_context.prompt_history_path(key);
1928 if let Err(e) = history.save_to_file(&path) {
1929 tracing::warn!("Failed to save {} history: {}", key, e);
1930 } else {
1931 tracing::debug!("Saved {} history to {:?}", key, path);
1932 }
1933 }
1934 }
1935}