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.cached_layout.last_frame_width = size.width;
12 self.cached_layout.last_frame_height = size.height;
13
14 self.cached_layout.reset_cell_theme_map();
16
17 self.drain_pending_lsp_prompt_for_active_buffer();
23
24 let active_split = self.split_manager.active_split();
29 {
30 let _span = tracing::info_span!("pre_sync_ensure_visible").entered();
31 self.pre_sync_ensure_visible(active_split);
32 }
33
34 {
37 let _span = tracing::info_span!("sync_scroll_groups").entered();
38 self.sync_scroll_groups();
39 }
40
41 let mut semantic_ranges: std::collections::HashMap<BufferId, (usize, usize)> =
47 std::collections::HashMap::new();
48 {
49 let _span = tracing::info_span!("compute_semantic_ranges").entered();
50 for (split_id, view_state) in &self.split_view_states {
51 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
52 if let Some(state) = self.buffers.get(&buffer_id) {
53 let start_line = state.buffer.get_line_number(view_state.viewport.top_byte);
54 let visible_lines =
55 view_state.viewport.visible_line_count().saturating_sub(1);
56 let end_line = start_line.saturating_add(visible_lines);
57 semantic_ranges
58 .entry(buffer_id)
59 .and_modify(|(min_start, max_end)| {
60 *min_start = (*min_start).min(start_line);
61 *max_end = (*max_end).max(end_line);
62 })
63 .or_insert((start_line, end_line));
64 }
65 }
66 }
67 }
68 for (buffer_id, (start_line, end_line)) in semantic_ranges {
69 self.maybe_request_semantic_tokens_range(buffer_id, start_line, end_line);
70 self.maybe_request_semantic_tokens_full_debounced(buffer_id);
71 self.maybe_request_folding_ranges_debounced(buffer_id);
72 }
73
74 {
75 let _span = tracing::info_span!("prepare_for_render").entered();
76 for (split_id, view_state) in &self.split_view_states {
77 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
78 if let Some(state) = self.buffers.get_mut(&buffer_id) {
79 let top_byte = view_state.viewport.top_byte;
80 let height = view_state.viewport.height;
81 if let Err(e) = state.prepare_for_render(top_byte, height) {
82 tracing::error!("Failed to prepare buffer for render: {}", e);
83 }
85 }
86 }
87 }
88 }
89
90 let is_search_prompt_active = self.prompt.as_ref().is_some_and(|p| {
93 matches!(
94 p.prompt_type,
95 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
96 )
97 });
98 if is_search_prompt_active {
99 if let Some(ref search_state) = self.search_state {
100 let query = search_state.query.clone();
101 self.update_search_highlights(&query);
102 }
103 }
104
105 let show_search_options = self.prompt.as_ref().is_some_and(|p| {
107 matches!(
108 p.prompt_type,
109 PromptType::Search
110 | PromptType::ReplaceSearch
111 | PromptType::Replace { .. }
112 | PromptType::QueryReplaceSearch
113 | PromptType::QueryReplace { .. }
114 )
115 });
116
117 let has_suggestions = self
119 .prompt
120 .as_ref()
121 .is_some_and(|p| !p.suggestions.is_empty());
122 let has_file_browser = self.prompt.as_ref().is_some_and(|p| {
123 matches!(
124 p.prompt_type,
125 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
126 )
127 }) && self.file_open_state.is_some();
128
129 let constraints = vec![
133 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }), Constraint::Min(0), Constraint::Length(
136 if !self.status_bar_visible || has_suggestions || has_file_browser {
137 0
138 } else {
139 1
140 },
141 ), Constraint::Length(if show_search_options { 1 } else { 0 }), Constraint::Length(if self.prompt_line_visible || self.prompt.is_some() {
144 1
145 } else {
146 0
147 }), ];
149
150 let main_chunks = Layout::default()
151 .direction(Direction::Vertical)
152 .constraints(constraints)
153 .split(size);
154
155 let menu_bar_area = main_chunks[0];
156 let main_content_area = main_chunks[1];
157 let status_bar_idx = 2;
158 let search_options_idx = 3;
159 let prompt_line_idx = 4;
160
161 let editor_content_area;
164 let file_explorer_should_show = self.file_explorer_visible
165 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
166
167 if file_explorer_should_show {
168 tracing::trace!(
170 "render: file explorer layout active (present={}, sync_in_progress={})",
171 self.file_explorer.is_some(),
172 self.file_explorer_sync_in_progress
173 );
174 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
175 let horizontal_chunks = Layout::default()
176 .direction(Direction::Horizontal)
177 .constraints([
178 Constraint::Length(explorer_cols), Constraint::Min(0), ])
181 .split(main_content_area);
182
183 self.cached_layout.file_explorer_area = Some(horizontal_chunks[0]);
184 editor_content_area = horizontal_chunks[1];
185
186 let remote_connection = self.connection_display_string();
188
189 if let Some(ref mut explorer) = self.file_explorer {
191 let is_focused = self.key_context == KeyContext::FileExplorer;
192
193 let mut files_with_unsaved_changes = std::collections::HashSet::new();
195 for (buffer_id, state) in &self.buffers {
196 if state.buffer.is_modified() {
197 if let Some(metadata) = self.buffer_metadata.get(buffer_id) {
198 if let Some(file_path) = metadata.file_path() {
199 files_with_unsaved_changes.insert(file_path.clone());
200 }
201 }
202 }
203 }
204
205 let close_button_hovered = matches!(
206 &self.mouse_state.hover_target,
207 Some(HoverTarget::FileExplorerCloseButton)
208 );
209 let keybindings = self.keybindings.read().unwrap();
210 let empty: Vec<std::path::PathBuf> = Vec::new();
211 let cut_paths = self
212 .file_explorer_clipboard
213 .as_ref()
214 .filter(|cb| cb.is_cut)
215 .map(|cb| cb.paths.as_slice())
216 .unwrap_or(empty.as_slice());
217 FileExplorerRenderer::render(
218 explorer,
219 frame,
220 horizontal_chunks[0],
221 is_focused,
222 &files_with_unsaved_changes,
223 &self.file_explorer_decoration_cache,
224 &keybindings,
225 self.key_context.clone(),
226 &self.theme,
227 close_button_hovered,
228 remote_connection.as_deref(),
229 cut_paths,
230 );
231 }
232 } else {
235 self.cached_layout.file_explorer_area = None;
237 editor_content_area = main_content_area;
238 }
239
240 if self.plugin_manager.is_active() {
247 let hooks_start = std::time::Instant::now();
248 let visible_buffers = self.split_manager.get_visible_buffers(editor_content_area);
250
251 let mut total_new_lines = 0usize;
252 for (split_id, buffer_id, split_area) in visible_buffers {
253 let viewport_top_byte = self
255 .split_view_states
256 .get(&split_id)
257 .map(|vs| vs.viewport.top_byte)
258 .unwrap_or(0);
259
260 if let Some(state) = self.buffers.get_mut(&buffer_id) {
261 self.plugin_manager.run_hook(
263 "render_start",
264 crate::services::plugins::hooks::HookArgs::RenderStart { buffer_id },
265 );
266
267 let visible_count = split_area.height as usize;
270 let is_binary = state.buffer.is_binary();
271 let line_ending = state.buffer.line_ending();
272 let base_tokens =
273 crate::view::ui::split_rendering::SplitRenderer::build_base_tokens_for_hook(
274 &mut state.buffer,
275 viewport_top_byte,
276 self.config.editor.estimated_line_length,
277 visible_count,
278 is_binary,
279 line_ending,
280 );
281 let viewport_start = viewport_top_byte;
282 let viewport_end = base_tokens
283 .last()
284 .and_then(|t| t.source_offset)
285 .unwrap_or(viewport_start);
286 let cursor_positions: Vec<usize> = self
287 .split_view_states
288 .get(&split_id)
289 .map(|vs| vs.cursors.iter().map(|(_, c)| c.position).collect())
290 .unwrap_or_default();
291 self.plugin_manager.run_hook(
292 "view_transform_request",
293 crate::services::plugins::hooks::HookArgs::ViewTransformRequest {
294 buffer_id,
295 split_id: split_id.into(),
296 viewport_start,
297 viewport_end,
298 tokens: base_tokens,
299 cursor_positions,
300 },
301 );
302
303 if let Some(vs) = self.split_view_states.get_mut(&split_id) {
307 vs.view_transform_stale = false;
308 }
309
310 let visible_count = split_area.height as usize;
312 let top_byte = viewport_top_byte;
313
314 let seen_byte_ranges = self.seen_byte_ranges.entry(buffer_id).or_default();
316
317 let mut new_lines: Vec<crate::services::plugins::hooks::LineInfo> = Vec::new();
319 let mut line_number = state.buffer.get_line_number(top_byte);
320 let mut iter = state
321 .buffer
322 .line_iterator(top_byte, self.config.editor.estimated_line_length);
323
324 for _ in 0..visible_count {
325 if let Some((line_start, line_content)) = iter.next_line() {
326 let byte_end = line_start + line_content.len();
327 let byte_range = (line_start, byte_end);
328
329 if !seen_byte_ranges.contains(&byte_range) {
331 new_lines.push(crate::services::plugins::hooks::LineInfo {
332 line_number,
333 byte_start: line_start,
334 byte_end,
335 content: line_content,
336 });
337 seen_byte_ranges.insert(byte_range);
338 }
339 line_number += 1;
340 } else {
341 break;
342 }
343 }
344
345 if !new_lines.is_empty() {
347 total_new_lines += new_lines.len();
348 self.plugin_manager.run_hook(
349 "lines_changed",
350 crate::services::plugins::hooks::HookArgs::LinesChanged {
351 buffer_id,
352 lines: new_lines,
353 },
354 );
355 }
356 }
357 }
358 let hooks_elapsed = hooks_start.elapsed();
359 tracing::trace!(
360 new_lines = total_new_lines,
361 elapsed_ms = hooks_elapsed.as_millis(),
362 elapsed_us = hooks_elapsed.as_micros(),
363 "lines_changed hooks total"
364 );
365
366 let commands = self.plugin_manager.process_commands();
378 if !commands.is_empty() {
379 let cmd_names: Vec<String> =
380 commands.iter().map(|c| c.debug_variant_name()).collect();
381 tracing::trace!(count = commands.len(), cmds = ?cmd_names, "process_commands during render");
382 }
383 for command in commands {
384 if let Err(e) = self.handle_plugin_command(command) {
385 tracing::error!("Error handling plugin command: {}", e);
386 }
387 }
388
389 self.flush_pending_grammars();
391 }
392
393 let lsp_waiting = !self.pending_completion_requests.is_empty()
395 || self.pending_goto_definition_request.is_some();
396
397 let settings_visible = self.settings_state.as_ref().is_some_and(|s| s.visible);
404 let hide_cursor = self.menu_state.active_menu.is_some()
405 || self.key_context == KeyContext::FileExplorer
406 || self.terminal_mode
407 || settings_visible
408 || self.keybinding_editor.is_some();
409
410 let hovered_tab = match &self.mouse_state.hover_target {
412 Some(HoverTarget::TabName(target, split_id)) => Some((*target, *split_id, false)),
413 Some(HoverTarget::TabCloseButton(target, split_id)) => Some((*target, *split_id, true)),
414 _ => None,
415 };
416
417 let hovered_close_split = match &self.mouse_state.hover_target {
419 Some(HoverTarget::CloseSplitButton(split_id)) => Some(*split_id),
420 _ => None,
421 };
422
423 let hovered_maximize_split = match &self.mouse_state.hover_target {
425 Some(HoverTarget::MaximizeSplitButton(split_id)) => Some(*split_id),
426 _ => None,
427 };
428
429 let is_maximized = self.split_manager.is_maximized();
430
431 let mut pending_hardware_cursor: Option<(u16, u16)> = None;
438
439 let _content_span = tracing::info_span!("render_content").entered();
440 let (
441 split_areas,
442 tab_layouts,
443 close_split_areas,
444 maximize_split_areas,
445 view_line_mappings,
446 horizontal_scrollbar_areas,
447 grouped_separator_areas,
448 ) = SplitRenderer::render_content(
449 frame,
450 editor_content_area,
451 &self.split_manager,
452 &mut self.buffers,
453 &self.buffer_metadata,
454 &mut self.event_logs,
455 &mut self.composite_buffers,
456 &mut self.composite_view_states,
457 &self.theme,
458 self.ansi_background.as_ref(),
459 self.background_fade,
460 lsp_waiting,
461 self.config.editor.large_file_threshold_bytes,
462 self.config.editor.line_wrap,
463 self.config.editor.estimated_line_length,
464 self.config.editor.highlight_context_bytes,
465 Some(&mut self.split_view_states),
466 &self.grouped_subtrees,
467 hide_cursor,
468 hovered_tab,
469 hovered_close_split,
470 hovered_maximize_split,
471 is_maximized,
472 self.config.editor.relative_line_numbers,
473 self.tab_bar_visible,
474 self.config.editor.use_terminal_bg,
475 self.session_mode || !self.software_cursor_only,
476 self.software_cursor_only,
477 self.config.editor.show_vertical_scrollbar,
478 self.config.editor.show_horizontal_scrollbar,
479 self.config.editor.diagnostics_inline_text,
480 self.config.editor.show_tilde,
481 self.config.editor.highlight_current_column,
482 &mut self.cached_layout.cell_theme_map,
483 size.width,
484 &mut pending_hardware_cursor,
485 );
486
487 drop(_content_span);
488
489 if self.plugin_manager.is_active() {
493 for (split_id, view_state) in &self.split_view_states {
494 let current = (
495 view_state.viewport.top_byte,
496 view_state.viewport.width,
497 view_state.viewport.height,
498 );
499 let (changed, previous) = match self.previous_viewports.get(split_id) {
504 Some(previous) => (*previous != current, Some(*previous)),
505 None => (false, None), };
507 tracing::trace!(
508 "viewport_changed check: split={:?} current={:?} previous={:?} changed={}",
509 split_id,
510 current,
511 previous,
512 changed
513 );
514 if changed {
515 if let Some(buffer_id) = self.split_manager.get_buffer_id((*split_id).into()) {
516 let top_line = self.buffers.get(&buffer_id).and_then(|state| {
518 if state.buffer.line_count().is_some() {
519 Some(state.buffer.get_line_number(view_state.viewport.top_byte))
520 } else {
521 None
522 }
523 });
524 tracing::debug!(
525 "Firing viewport_changed hook: split={:?} buffer={:?} top_byte={} top_line={:?}",
526 split_id,
527 buffer_id,
528 view_state.viewport.top_byte,
529 top_line
530 );
531 self.plugin_manager.run_hook(
532 "viewport_changed",
533 crate::services::plugins::hooks::HookArgs::ViewportChanged {
534 split_id: (*split_id).into(),
535 buffer_id,
536 top_byte: view_state.viewport.top_byte,
537 top_line,
538 width: view_state.viewport.width,
539 height: view_state.viewport.height,
540 },
541 );
542 }
543 }
544 }
545 }
546
547 self.previous_viewports.clear();
549 for (split_id, view_state) in &self.split_view_states {
550 self.previous_viewports.insert(
551 *split_id,
552 (
553 view_state.viewport.top_byte,
554 view_state.viewport.width,
555 view_state.viewport.height,
556 ),
557 );
558 }
559
560 self.render_terminal_splits(frame, &split_areas);
562
563 self.cached_layout.split_areas = split_areas;
564 self.cached_layout.horizontal_scrollbar_areas = horizontal_scrollbar_areas;
565 self.cached_layout.tab_layouts = tab_layouts;
566 self.cached_layout.close_split_areas = close_split_areas;
567 self.cached_layout.maximize_split_areas = maximize_split_areas;
568 self.cached_layout.view_line_mappings = view_line_mappings;
569 let mut separator_areas = self
570 .split_manager
571 .get_separators_with_ids(editor_content_area);
572 separator_areas.extend(grouped_separator_areas);
577 self.cached_layout.separator_areas = separator_areas;
578 self.cached_layout.editor_content_area = Some(editor_content_area);
579
580 self.render_hover_highlights(frame);
582
583 self.cached_layout.suggestions_area = None;
585 self.file_browser_layout = None;
586
587 let display_name = self
589 .buffer_metadata
590 .get(&self.active_buffer())
591 .map(|m| m.display_name.clone())
592 .unwrap_or_else(|| "[No Name]".to_string());
593
594 self.update_terminal_title(&display_name);
598
599 let status_message = self.status_message.clone();
600 let plugin_status_message = self.plugin_status_message.clone();
601 let prompt = self.prompt.clone();
602 let current_language = self
625 .buffers
626 .get(&self.active_buffer())
627 .map(|s| s.language.clone())
628 .unwrap_or_default();
629 let buffer_lsp_disabled_reason = self
630 .buffer_metadata
631 .get(&self.active_buffer())
632 .filter(|m| !m.lsp_enabled)
633 .and_then(|m| m.lsp_disabled_reason.as_deref());
634 let (lsp_status, lsp_indicator_state) = compose_lsp_status(
635 ¤t_language,
636 buffer_lsp_disabled_reason,
637 &self.lsp_progress,
638 &self.lsp_server_statuses,
639 &self.config.lsp,
640 &self.user_dismissed_lsp_languages,
641 );
642 let theme = self.theme.clone();
643 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());
648
649 if self.status_bar_visible && !has_suggestions && !has_file_browser {
651 let (warning_level, general_warning_count) =
654 if self.config.warnings.show_status_indicator {
655 let lsp_level = {
656 use crate::services::async_bridge::LspServerStatus;
657 let mut level = WarningLevel::None;
658 for ((lang, _), status) in &self.lsp_server_statuses {
659 if lang == ¤t_language {
660 match status {
661 LspServerStatus::Error => {
662 level = WarningLevel::Error;
663 break;
664 }
665 LspServerStatus::Starting | LspServerStatus::Initializing => {
666 if level != WarningLevel::Error {
667 level = WarningLevel::Warning;
668 }
669 }
670 _ => {}
671 }
672 }
673 }
674 level
675 };
676 (lsp_level, self.get_general_warning_count())
677 } else {
678 (WarningLevel::None, 0)
679 };
680
681 use crate::view::ui::status_bar::StatusBarHover;
683 let status_bar_hover = match &self.mouse_state.hover_target {
684 Some(HoverTarget::StatusBarLspIndicator) => StatusBarHover::LspIndicator,
685 Some(HoverTarget::StatusBarWarningBadge) => StatusBarHover::WarningBadge,
686 Some(HoverTarget::StatusBarLineEndingIndicator) => {
687 StatusBarHover::LineEndingIndicator
688 }
689 Some(HoverTarget::StatusBarEncodingIndicator) => StatusBarHover::EncodingIndicator,
690 Some(HoverTarget::StatusBarLanguageIndicator) => StatusBarHover::LanguageIndicator,
691 Some(HoverTarget::StatusBarRemoteIndicator) => StatusBarHover::RemoteIndicator,
692 _ => StatusBarHover::None,
693 };
694
695 let remote_connection = self.connection_display_string();
696
697 let session_name = self.session_name().map(|s| s.to_string());
699
700 let active_split = self.effective_active_split();
701 let active_buf = self.active_buffer();
702 let default_cursors = crate::model::cursor::Cursors::new();
703 let status_cursors = self
704 .split_view_states
705 .get(&active_split)
706 .map(|vs| &vs.cursors)
707 .unwrap_or(&default_cursors);
708 let is_read_only = self
709 .buffer_metadata
710 .get(&active_buf)
711 .map(|m| m.read_only)
712 .unwrap_or(false);
713 let mut status_ctx = crate::view::ui::status_bar::StatusBarContext {
714 state: self.buffers.get_mut(&active_buf).unwrap(),
715 cursors: status_cursors,
716 status_message: &status_message,
717 plugin_status_message: &plugin_status_message,
718 lsp_status: &lsp_status,
719 lsp_indicator_state,
720 theme: &theme,
721 display_name: &display_name,
722 keybindings: &keybindings_cloned,
723 chord_state: &chord_state_cloned,
724 update_available: update_available.as_deref(),
725 warning_level,
726 general_warning_count,
727 hover: status_bar_hover,
728 remote_connection: remote_connection.as_deref(),
729 session_name: session_name.as_deref(),
730 read_only: is_read_only,
731 remote_state_override: self.remote_indicator_override.as_ref(),
732 };
733 let status_bar_layout = StatusBarRenderer::render_status_bar(
734 frame,
735 main_chunks[status_bar_idx],
736 &mut status_ctx,
737 &self.config.editor.status_bar,
738 );
739
740 let status_bar_area = main_chunks[status_bar_idx];
742 self.cached_layout.status_bar_area =
743 Some((status_bar_area.y, status_bar_area.x, status_bar_area.width));
744 self.cached_layout.status_bar_lsp_area = status_bar_layout.lsp_indicator;
745 self.cached_layout.status_bar_warning_area = status_bar_layout.warning_badge;
746 self.cached_layout.status_bar_line_ending_area =
747 status_bar_layout.line_ending_indicator;
748 self.cached_layout.status_bar_encoding_area = status_bar_layout.encoding_indicator;
749 self.cached_layout.status_bar_language_area = status_bar_layout.language_indicator;
750 self.cached_layout.status_bar_message_area = status_bar_layout.message_area;
751 self.cached_layout.status_bar_remote_area = status_bar_layout.remote_indicator;
752 }
753
754 if show_search_options {
756 let confirm_each = self.prompt.as_ref().and_then(|p| {
758 if matches!(
759 p.prompt_type,
760 PromptType::ReplaceSearch
761 | PromptType::Replace { .. }
762 | PromptType::QueryReplaceSearch
763 | PromptType::QueryReplace { .. }
764 ) {
765 Some(self.search_confirm_each)
766 } else {
767 None
768 }
769 });
770
771 use crate::view::ui::status_bar::SearchOptionsHover;
773 let search_options_hover = match &self.mouse_state.hover_target {
774 Some(HoverTarget::SearchOptionCaseSensitive) => SearchOptionsHover::CaseSensitive,
775 Some(HoverTarget::SearchOptionWholeWord) => SearchOptionsHover::WholeWord,
776 Some(HoverTarget::SearchOptionRegex) => SearchOptionsHover::Regex,
777 Some(HoverTarget::SearchOptionConfirmEach) => SearchOptionsHover::ConfirmEach,
778 _ => SearchOptionsHover::None,
779 };
780
781 let search_options_layout = StatusBarRenderer::render_search_options(
782 frame,
783 main_chunks[search_options_idx],
784 self.search_case_sensitive,
785 self.search_whole_word,
786 self.search_use_regex,
787 confirm_each,
788 &theme,
789 &keybindings_cloned,
790 search_options_hover,
791 );
792 self.cached_layout.search_options_layout = Some(search_options_layout);
793 } else {
794 self.cached_layout.search_options_layout = None;
795 }
796
797 if let Some(prompt) = &prompt {
799 if matches!(
801 prompt.prompt_type,
802 crate::view::prompt::PromptType::OpenFile
803 | crate::view::prompt::PromptType::SwitchProject
804 ) {
805 if let Some(file_open_state) = &self.file_open_state {
806 StatusBarRenderer::render_file_open_prompt(
807 frame,
808 main_chunks[prompt_line_idx],
809 prompt,
810 file_open_state,
811 &theme,
812 );
813 } else {
814 StatusBarRenderer::render_prompt(
815 frame,
816 main_chunks[prompt_line_idx],
817 prompt,
818 &theme,
819 );
820 }
821 } else {
822 StatusBarRenderer::render_prompt(
823 frame,
824 main_chunks[prompt_line_idx],
825 prompt,
826 &theme,
827 );
828 }
829 }
830
831 self.render_prompt_popups(frame, main_chunks[prompt_line_idx], size.width);
834
835 let theme_clone = self.theme.clone();
838 let hover_target = self.mouse_state.hover_target.clone();
839
840 self.cached_layout.popup_areas.clear();
842
843 let popup_info: Vec<_> = {
845 let active_split = self.split_manager.active_split();
847 let viewport = self
848 .split_view_states
849 .get(&active_split)
850 .map(|vs| vs.viewport.clone());
851
852 let content_rect = self
857 .cached_layout
858 .split_areas
859 .iter()
860 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
861 .map(|(_, _, rect, _, _, _)| *rect);
862
863 let primary_cursor = self
864 .split_view_states
865 .get(&active_split)
866 .map(|vs| *vs.cursors.primary());
867 let state = self.active_state_mut();
868 if state.popups.is_visible() {
869 let primary_cursor =
871 primary_cursor.unwrap_or_else(|| crate::model::cursor::Cursor::new(0));
872
873 let gutter_width = viewport
875 .as_ref()
876 .map(|vp| vp.gutter_width(&state.buffer) as u16)
877 .unwrap_or(0);
878
879 let cursor_screen_pos = viewport
880 .as_ref()
881 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &primary_cursor))
882 .unwrap_or((0, 0));
883
884 let word_start_screen_pos = {
888 use crate::primitives::word_navigation::find_completion_word_start;
889 let word_start =
890 find_completion_word_start(&state.buffer, primary_cursor.position);
891 let word_start_cursor = crate::model::cursor::Cursor::new(word_start);
892 viewport
893 .as_ref()
894 .map(|vp| vp.cursor_screen_position(&mut state.buffer, &word_start_cursor))
895 .unwrap_or((0, 0))
896 };
897
898 let (base_x, base_y) = content_rect
903 .map(|r| (r.x + gutter_width, r.y))
904 .unwrap_or((gutter_width, 1));
905
906 let cursor_screen_pos =
907 (cursor_screen_pos.0 + base_x, cursor_screen_pos.1 + base_y);
908 let word_start_screen_pos = (
909 word_start_screen_pos.0 + base_x,
910 word_start_screen_pos.1 + base_y,
911 );
912
913 state
915 .popups
916 .all()
917 .iter()
918 .enumerate()
919 .map(|(popup_idx, popup)| {
920 let popup_pos = if popup.kind == crate::view::popup::PopupKind::Completion {
922 (word_start_screen_pos.0, cursor_screen_pos.1)
923 } else {
924 cursor_screen_pos
925 };
926 let popup_area = popup.calculate_area(size, Some(popup_pos));
927
928 let desc_height = popup.description_height();
931 let inner_area = if popup.bordered {
932 ratatui::layout::Rect {
933 x: popup_area.x + 1,
934 y: popup_area.y + 1 + desc_height,
935 width: popup_area.width.saturating_sub(2),
936 height: popup_area.height.saturating_sub(2 + desc_height),
937 }
938 } else {
939 ratatui::layout::Rect {
940 x: popup_area.x,
941 y: popup_area.y + desc_height,
942 width: popup_area.width,
943 height: popup_area.height.saturating_sub(desc_height),
944 }
945 };
946
947 let num_items = match &popup.content {
948 crate::view::popup::PopupContent::List { items, .. } => items.len(),
949 _ => 0,
950 };
951
952 let total_lines = popup.item_count();
954 let visible_lines = inner_area.height as usize;
955 let scrollbar_rect = if total_lines > visible_lines && inner_area.width > 2
956 {
957 Some(ratatui::layout::Rect {
958 x: inner_area.x + inner_area.width - 1,
959 y: inner_area.y,
960 width: 1,
961 height: inner_area.height,
962 })
963 } else {
964 None
965 };
966
967 (
968 popup_idx,
969 popup_area,
970 inner_area,
971 popup.scroll_offset,
972 num_items,
973 scrollbar_rect,
974 total_lines,
975 )
976 })
977 .collect()
978 } else {
979 Vec::new()
980 }
981 };
982
983 self.cached_layout.popup_areas = popup_info.clone();
985
986 let state = self.active_state_mut();
988 if state.popups.is_visible() {
989 for (popup_idx, popup) in state.popups.all().iter().enumerate() {
990 if let Some((_, popup_area, _, _, _, _, _)) = popup_info.get(popup_idx) {
991 popup.render_with_hover(
992 frame,
993 *popup_area,
994 &theme_clone,
995 hover_target.as_ref(),
996 );
997 }
998 }
999 }
1000
1001 self.cached_layout.global_popup_areas.clear();
1012 if let Some(popup) = self.global_popups.top() {
1013 let top_idx = self.global_popups.all().len() - 1;
1014 let popup_area = popup.calculate_area(size, None);
1015 let desc_height = popup.description_height();
1016 let inner_area = if popup.bordered {
1017 ratatui::layout::Rect {
1018 x: popup_area.x + 1,
1019 y: popup_area.y + 1 + desc_height,
1020 width: popup_area.width.saturating_sub(2),
1021 height: popup_area.height.saturating_sub(2 + desc_height),
1022 }
1023 } else {
1024 ratatui::layout::Rect {
1025 x: popup_area.x,
1026 y: popup_area.y + desc_height,
1027 width: popup_area.width,
1028 height: popup_area.height.saturating_sub(desc_height),
1029 }
1030 };
1031 let num_items = match &popup.content {
1032 crate::view::popup::PopupContent::List { items, .. } => items.len(),
1033 _ => 0,
1034 };
1035 self.cached_layout.global_popup_areas.push((
1036 top_idx,
1037 popup_area,
1038 inner_area,
1039 popup.scroll_offset,
1040 num_items,
1041 ));
1042 popup.render_with_hover(frame, popup_area, &theme_clone, hover_target.as_ref());
1043 }
1044
1045 self.update_menu_context();
1048
1049 let settings_visible = self
1052 .settings_state
1053 .as_ref()
1054 .map(|s| s.visible)
1055 .unwrap_or(false);
1056 if settings_visible {
1057 crate::view::dimming::apply_dimming(frame, size);
1059 }
1060 if let Some(ref mut settings_state) = self.settings_state {
1061 if settings_state.visible {
1062 settings_state.update_focus_states();
1063 let settings_layout = crate::view::settings::render_settings(
1064 frame,
1065 size,
1066 settings_state,
1067 &self.theme,
1068 );
1069 self.cached_layout.settings_layout = Some(settings_layout);
1070 }
1071 }
1072
1073 if let Some(ref wizard) = self.calibration_wizard {
1075 crate::view::dimming::apply_dimming(frame, size);
1077 crate::view::calibration_wizard::render_calibration_wizard(
1078 frame,
1079 size,
1080 wizard,
1081 &self.theme,
1082 );
1083 }
1084
1085 if let Some(ref mut kb_editor) = self.keybinding_editor {
1087 crate::view::dimming::apply_dimming(frame, size);
1088 crate::view::keybinding_editor::render_keybinding_editor(
1089 frame,
1090 size,
1091 kb_editor,
1092 &self.theme,
1093 );
1094 }
1095
1096 if let Some(ref debug) = self.event_debug {
1098 crate::view::dimming::apply_dimming(frame, size);
1100 crate::view::event_debug::render_event_debug(frame, size, debug, &self.theme);
1101 }
1102
1103 if self.menu_bar_visible {
1104 let keybindings = self.keybindings.read().unwrap();
1105 self.cached_layout.menu_layout = Some(crate::view::ui::MenuRenderer::render(
1106 frame,
1107 menu_bar_area,
1108 &self.menus,
1109 &self.menu_state,
1110 &keybindings,
1111 &self.theme,
1112 self.mouse_state.hover_target.as_ref(),
1113 self.config.editor.menu_bar_mnemonics,
1114 ));
1115 } else {
1116 self.cached_layout.menu_layout = None;
1117 }
1118
1119 if let Some(ref menu) = self.tab_context_menu {
1121 self.render_tab_context_menu(frame, menu);
1122 }
1123
1124 if let Some(ref menu) = self.file_explorer_context_menu {
1125 self.render_file_explorer_context_menu(frame, menu);
1126 }
1127
1128 self.record_non_editor_theme_regions();
1130
1131 self.render_theme_info_popup(frame);
1133
1134 if let Some(ref drag_state) = self.mouse_state.dragging_tab {
1136 if drag_state.is_dragging() {
1137 self.render_tab_drop_zone(frame, drag_state);
1138 }
1139 }
1140
1141 if self.gpm_active {
1147 if let Some((col, row)) = self.mouse_cursor_position {
1148 use ratatui::style::Modifier;
1149
1150 if col < size.width && row < size.height {
1152 let buf = frame.buffer_mut();
1154 if let Some(cell) = buf.cell_mut((col, row)) {
1155 cell.set_style(cell.style().add_modifier(Modifier::REVERSED));
1156 }
1157 }
1158 }
1159 }
1160
1161 if self.keyboard_capture && self.terminal_mode {
1164 let active_split = self.split_manager.active_split();
1166 let active_split_area = self
1167 .cached_layout
1168 .split_areas
1169 .iter()
1170 .find(|(split_id, _, _, _, _, _)| *split_id == active_split)
1171 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1172
1173 if let Some(terminal_area) = active_split_area {
1174 self.apply_keyboard_capture_dimming(frame, terminal_area);
1175 }
1176 }
1177
1178 if let Some((cx, cy)) = pending_hardware_cursor {
1189 if self.prompt.is_none() && !self.cursor_obscured_by_overlay(cx, cy) {
1190 frame.set_cursor_position((cx, cy));
1191 }
1192 }
1193
1194 crate::view::color_support::convert_buffer_colors(
1196 frame.buffer_mut(),
1197 self.color_capability,
1198 );
1199 }
1200
1201 fn cursor_obscured_by_overlay(&self, x: u16, y: u16) -> bool {
1205 let inside = |rect: ratatui::layout::Rect| -> bool {
1206 x >= rect.x
1207 && x < rect.x.saturating_add(rect.width)
1208 && y >= rect.y
1209 && y < rect.y.saturating_add(rect.height)
1210 };
1211
1212 if self
1213 .cached_layout
1214 .popup_areas
1215 .iter()
1216 .any(|entry| inside(entry.1))
1217 {
1218 return true;
1219 }
1220 if self
1221 .cached_layout
1222 .global_popup_areas
1223 .iter()
1224 .any(|entry| inside(entry.1))
1225 {
1226 return true;
1227 }
1228 if let Some((rect, _, _, _)) = self.cached_layout.suggestions_area {
1229 if inside(rect) {
1230 return true;
1231 }
1232 }
1233 if let Some(ref fb) = self.file_browser_layout {
1234 if inside(fb.popup_area) {
1235 return true;
1236 }
1237 }
1238 false
1239 }
1240
1241 fn render_quick_open_hints(
1243 frame: &mut Frame,
1244 area: ratatui::layout::Rect,
1245 theme: &crate::view::theme::Theme,
1246 ) {
1247 use ratatui::style::{Modifier, Style};
1248 use ratatui::text::{Line, Span};
1249 use ratatui::widgets::Paragraph;
1250 use rust_i18n::t;
1251
1252 let hints_style = Style::default()
1253 .fg(theme.line_number_fg)
1254 .bg(theme.suggestion_selected_bg)
1255 .add_modifier(Modifier::DIM);
1256 let hints_text = t!("quick_open.mode_hints");
1257 let left_margin = 2;
1259 let hints_width = crate::primitives::display_width::str_width(&hints_text);
1260 let mut spans = Vec::new();
1261 spans.push(Span::styled(" ".repeat(left_margin), hints_style));
1262 spans.push(Span::styled(hints_text.to_string(), hints_style));
1263 let remaining = (area.width as usize).saturating_sub(left_margin + hints_width);
1264 spans.push(Span::styled(" ".repeat(remaining), hints_style));
1265
1266 let paragraph = Paragraph::new(Line::from(spans));
1267 frame.render_widget(paragraph, area);
1268 }
1269
1270 fn apply_keyboard_capture_dimming(
1273 &self,
1274 frame: &mut Frame,
1275 terminal_area: ratatui::layout::Rect,
1276 ) {
1277 let size = frame.area();
1278 crate::view::dimming::apply_dimming_excluding(frame, size, Some(terminal_area));
1279 }
1280
1281 fn render_prompt_popups(
1284 &mut self,
1285 frame: &mut Frame,
1286 prompt_area: ratatui::layout::Rect,
1287 width: u16,
1288 ) {
1289 let Some(prompt) = &self.prompt else { return };
1290
1291 if matches!(
1292 prompt.prompt_type,
1293 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
1294 ) {
1295 let Some(file_open_state) = &self.file_open_state else {
1296 return;
1297 };
1298 let max_height = prompt_area.y.saturating_sub(1).min(20);
1299 let popup_area = ratatui::layout::Rect {
1300 x: 0,
1301 y: prompt_area.y.saturating_sub(max_height),
1302 width,
1303 height: max_height,
1304 };
1305 let keybindings = self.keybindings.read().unwrap();
1306 self.file_browser_layout = crate::view::ui::FileBrowserRenderer::render(
1307 frame,
1308 popup_area,
1309 file_open_state,
1310 &self.theme,
1311 &self.mouse_state.hover_target,
1312 Some(&*keybindings),
1313 );
1314 return;
1315 }
1316
1317 if prompt.suggestions.is_empty() {
1318 return;
1319 }
1320
1321 let suggestion_count = prompt.suggestions.len().min(10);
1322 let is_quick_open = prompt.prompt_type == crate::view::prompt::PromptType::QuickOpen;
1323 let hints_height: u16 = if is_quick_open { 1 } else { 0 };
1324 let height = suggestion_count as u16 + 2 + hints_height;
1325
1326 let suggestions_area = ratatui::layout::Rect {
1327 x: 0,
1328 y: prompt_area.y.saturating_sub(height),
1329 width,
1330 height: height - hints_height,
1331 };
1332
1333 frame.render_widget(ratatui::widgets::Clear, suggestions_area);
1334
1335 self.cached_layout.suggestions_area = SuggestionsRenderer::render_with_hover(
1336 frame,
1337 suggestions_area,
1338 prompt,
1339 &self.theme,
1340 self.mouse_state.hover_target.as_ref(),
1341 );
1342
1343 if is_quick_open {
1344 let hints_area = ratatui::layout::Rect {
1345 x: 0,
1346 y: prompt_area.y.saturating_sub(hints_height),
1347 width,
1348 height: hints_height,
1349 };
1350 frame.render_widget(ratatui::widgets::Clear, hints_area);
1351 Self::render_quick_open_hints(frame, hints_area, &self.theme);
1352 }
1353 }
1354
1355 pub(super) fn render_hover_highlights(&self, frame: &mut Frame) {
1357 use ratatui::style::Style;
1358 use ratatui::text::Span;
1359 use ratatui::widgets::Paragraph;
1360
1361 match &self.mouse_state.hover_target {
1362 Some(HoverTarget::SplitSeparator(split_id, direction)) => {
1363 for (sid, dir, x, y, length) in &self.cached_layout.separator_areas {
1365 if sid == split_id && dir == direction {
1366 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1367 match dir {
1368 SplitDirection::Horizontal => {
1369 let line_text = "─".repeat(*length as usize);
1370 let paragraph =
1371 Paragraph::new(Span::styled(line_text, hover_style));
1372 frame.render_widget(
1373 paragraph,
1374 ratatui::layout::Rect::new(*x, *y, *length, 1),
1375 );
1376 }
1377 SplitDirection::Vertical => {
1378 for offset in 0..*length {
1379 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1380 frame.render_widget(
1381 paragraph,
1382 ratatui::layout::Rect::new(*x, y + offset, 1, 1),
1383 );
1384 }
1385 }
1386 }
1387 }
1388 }
1389 }
1390 Some(HoverTarget::ScrollbarThumb(split_id)) => {
1391 for (sid, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1393 &self.cached_layout.split_areas
1394 {
1395 if sid == split_id {
1396 let hover_style = Style::default().bg(self.theme.scrollbar_thumb_hover_fg);
1397 for row_offset in *thumb_start..*thumb_end {
1398 let paragraph = Paragraph::new(Span::styled(" ", hover_style));
1399 frame.render_widget(
1400 paragraph,
1401 ratatui::layout::Rect::new(
1402 scrollbar_rect.x,
1403 scrollbar_rect.y + row_offset as u16,
1404 1,
1405 1,
1406 ),
1407 );
1408 }
1409 }
1410 }
1411 }
1412 Some(HoverTarget::ScrollbarTrack(split_id, hovered_row)) => {
1413 for (sid, _buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
1415 &self.cached_layout.split_areas
1416 {
1417 if sid == split_id {
1418 let track_hover_style =
1419 Style::default().bg(self.theme.scrollbar_track_hover_fg);
1420 let paragraph = Paragraph::new(Span::styled(" ", track_hover_style));
1421 frame.render_widget(
1422 paragraph,
1423 ratatui::layout::Rect::new(
1424 scrollbar_rect.x,
1425 scrollbar_rect.y + hovered_row,
1426 1,
1427 1,
1428 ),
1429 );
1430 }
1431 }
1432 }
1433 Some(HoverTarget::FileExplorerBorder) => {
1434 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1436 let hover_style = Style::default().fg(self.theme.split_separator_hover_fg);
1437 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1438 for row_offset in 0..explorer_area.height {
1439 let paragraph = Paragraph::new(Span::styled("│", hover_style));
1440 frame.render_widget(
1441 paragraph,
1442 ratatui::layout::Rect::new(
1443 border_x,
1444 explorer_area.y + row_offset,
1445 1,
1446 1,
1447 ),
1448 );
1449 }
1450 }
1451 }
1452 _ => {}
1454 }
1455 }
1456
1457 fn render_tab_context_menu(&self, frame: &mut Frame, menu: &TabContextMenu) {
1459 use ratatui::style::Style;
1460 use ratatui::text::{Line, Span};
1461 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1462
1463 let items = super::types::TabContextMenuItem::all();
1464 let menu_width = 22u16; let menu_height = items.len() as u16 + 2; let screen_width = frame.area().width;
1469 let screen_height = frame.area().height;
1470
1471 let menu_x = if menu.position.0 + menu_width > screen_width {
1472 screen_width.saturating_sub(menu_width)
1473 } else {
1474 menu.position.0
1475 };
1476
1477 let menu_y = if menu.position.1 + menu_height > screen_height {
1478 screen_height.saturating_sub(menu_height)
1479 } else {
1480 menu.position.1
1481 };
1482
1483 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1484
1485 frame.render_widget(Clear, area);
1487
1488 let mut lines = Vec::new();
1490 for (idx, item) in items.iter().enumerate() {
1491 let is_highlighted = idx == menu.highlighted;
1492
1493 let style = if is_highlighted {
1494 Style::default()
1495 .fg(self.theme.menu_highlight_fg)
1496 .bg(self.theme.menu_highlight_bg)
1497 } else {
1498 Style::default()
1499 .fg(self.theme.menu_dropdown_fg)
1500 .bg(self.theme.menu_dropdown_bg)
1501 };
1502
1503 let label = item.label();
1505 let content_width = (menu_width as usize).saturating_sub(2); let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1507
1508 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1509 }
1510
1511 let block = Block::default()
1512 .borders(Borders::ALL)
1513 .border_style(Style::default().fg(self.theme.menu_border_fg))
1514 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1515
1516 let paragraph = Paragraph::new(lines).block(block);
1517 frame.render_widget(paragraph, area);
1518 }
1519
1520 fn render_file_explorer_context_menu(
1522 &self,
1523 frame: &mut Frame,
1524 menu: &super::types::FileExplorerContextMenu,
1525 ) {
1526 use ratatui::style::Style;
1527 use ratatui::text::{Line, Span};
1528 use ratatui::widgets::{Block, Borders, Clear, Paragraph};
1529
1530 let items = menu.items();
1531 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
1532 let menu_height = menu.height();
1533 let (menu_x, menu_y) = menu.clamped_position(frame.area().width, frame.area().height);
1534
1535 let area = ratatui::layout::Rect::new(menu_x, menu_y, menu_width, menu_height);
1536
1537 frame.render_widget(Clear, area);
1538
1539 let mut lines = Vec::new();
1540 for (idx, item) in items.iter().enumerate() {
1541 let is_highlighted = idx == menu.highlighted;
1542
1543 let style = if is_highlighted {
1544 Style::default()
1545 .fg(self.theme.menu_highlight_fg)
1546 .bg(self.theme.menu_highlight_bg)
1547 } else {
1548 Style::default()
1549 .fg(self.theme.menu_dropdown_fg)
1550 .bg(self.theme.menu_dropdown_bg)
1551 };
1552
1553 let label = item.label();
1554 let content_width = (menu_width as usize).saturating_sub(2);
1555 let padded_label = format!(" {:<width$}", label, width = content_width - 1);
1556
1557 lines.push(Line::from(vec![Span::styled(padded_label, style)]));
1558 }
1559
1560 let block = Block::default()
1561 .borders(Borders::ALL)
1562 .border_style(Style::default().fg(self.theme.menu_border_fg))
1563 .style(Style::default().bg(self.theme.menu_dropdown_bg));
1564
1565 let paragraph = Paragraph::new(lines).block(block);
1566 frame.render_widget(paragraph, area);
1567 }
1568
1569 fn render_tab_drop_zone(&self, frame: &mut Frame, drag_state: &super::types::TabDragState) {
1571 use ratatui::style::Modifier;
1572
1573 let Some(ref drop_zone) = drag_state.drop_zone else {
1574 return;
1575 };
1576
1577 let split_id = drop_zone.split_id();
1578
1579 let split_area = self
1581 .cached_layout
1582 .split_areas
1583 .iter()
1584 .find(|(sid, _, _, _, _, _)| *sid == split_id)
1585 .map(|(_, _, content_rect, _, _, _)| *content_rect);
1586
1587 let Some(content_rect) = split_area else {
1588 return;
1589 };
1590
1591 use super::types::TabDropZone;
1593
1594 let highlight_area = match drop_zone {
1595 TabDropZone::TabBar(_, _) | TabDropZone::SplitCenter(_) => {
1596 content_rect
1599 }
1600 TabDropZone::SplitLeft(_) => {
1601 let width = (content_rect.width / 2).max(3);
1603 ratatui::layout::Rect::new(
1604 content_rect.x,
1605 content_rect.y,
1606 width,
1607 content_rect.height,
1608 )
1609 }
1610 TabDropZone::SplitRight(_) => {
1611 let width = (content_rect.width / 2).max(3);
1613 let x = content_rect.x + content_rect.width - width;
1614 ratatui::layout::Rect::new(x, content_rect.y, width, content_rect.height)
1615 }
1616 TabDropZone::SplitTop(_) => {
1617 let height = (content_rect.height / 2).max(2);
1619 ratatui::layout::Rect::new(
1620 content_rect.x,
1621 content_rect.y,
1622 content_rect.width,
1623 height,
1624 )
1625 }
1626 TabDropZone::SplitBottom(_) => {
1627 let height = (content_rect.height / 2).max(2);
1629 let y = content_rect.y + content_rect.height - height;
1630 ratatui::layout::Rect::new(content_rect.x, y, content_rect.width, height)
1631 }
1632 };
1633
1634 let buf = frame.buffer_mut();
1637 let drop_zone_bg = self.theme.tab_drop_zone_bg;
1638 let drop_zone_border = self.theme.tab_drop_zone_border;
1639
1640 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1642 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1643 if let Some(cell) = buf.cell_mut((x, y)) {
1644 cell.set_bg(drop_zone_bg);
1647
1648 let is_border = x == highlight_area.x
1650 || x == highlight_area.x + highlight_area.width - 1
1651 || y == highlight_area.y
1652 || y == highlight_area.y + highlight_area.height - 1;
1653
1654 if is_border {
1655 cell.set_fg(drop_zone_border);
1656 cell.set_style(cell.style().add_modifier(Modifier::BOLD));
1657 }
1658 }
1659 }
1660 }
1661
1662 match drop_zone {
1664 TabDropZone::SplitLeft(_) => {
1665 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1667 if let Some(cell) = buf.cell_mut((highlight_area.x, y)) {
1668 cell.set_symbol("▌");
1669 cell.set_fg(drop_zone_border);
1670 }
1671 }
1672 }
1673 TabDropZone::SplitRight(_) => {
1674 let x = highlight_area.x + highlight_area.width - 1;
1676 for y in highlight_area.y..highlight_area.y + highlight_area.height {
1677 if let Some(cell) = buf.cell_mut((x, y)) {
1678 cell.set_symbol("▐");
1679 cell.set_fg(drop_zone_border);
1680 }
1681 }
1682 }
1683 TabDropZone::SplitTop(_) => {
1684 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1686 if let Some(cell) = buf.cell_mut((x, highlight_area.y)) {
1687 cell.set_symbol("▀");
1688 cell.set_fg(drop_zone_border);
1689 }
1690 }
1691 }
1692 TabDropZone::SplitBottom(_) => {
1693 let y = highlight_area.y + highlight_area.height - 1;
1695 for x in highlight_area.x..highlight_area.x + highlight_area.width {
1696 if let Some(cell) = buf.cell_mut((x, y)) {
1697 cell.set_symbol("▄");
1698 cell.set_fg(drop_zone_border);
1699 }
1700 }
1701 }
1702 TabDropZone::SplitCenter(_) | TabDropZone::TabBar(_, _) => {
1703 }
1705 }
1706 }
1707
1708 pub fn recompute_layout(&mut self, width: u16, height: u16) {
1713 let size = ratatui::layout::Rect::new(0, 0, width, height);
1714
1715 let active_split = self.split_manager.active_split();
1717 self.pre_sync_ensure_visible(active_split);
1718 self.sync_scroll_groups();
1719
1720 let constraints = vec![
1723 Constraint::Length(if self.menu_bar_visible { 1 } else { 0 }),
1724 Constraint::Min(0),
1725 Constraint::Length(if self.status_bar_visible { 1 } else { 0 }), Constraint::Length(0), Constraint::Length(if self.prompt_line_visible { 1 } else { 0 }), ];
1729 let main_chunks = Layout::default()
1730 .direction(Direction::Vertical)
1731 .constraints(constraints)
1732 .split(size);
1733 let main_content_area = main_chunks[1];
1734
1735 let file_explorer_should_show = self.file_explorer_visible
1737 && (self.file_explorer.is_some() || self.file_explorer_sync_in_progress);
1738 let editor_content_area = if file_explorer_should_show {
1739 let explorer_cols = self.file_explorer_width.to_cols(main_content_area.width);
1740 let horizontal_chunks = Layout::default()
1741 .direction(Direction::Horizontal)
1742 .constraints([Constraint::Length(explorer_cols), Constraint::Min(0)])
1743 .split(main_content_area);
1744 horizontal_chunks[1]
1745 } else {
1746 main_content_area
1747 };
1748
1749 let view_line_mappings = SplitRenderer::compute_content_layout(
1751 editor_content_area,
1752 &self.split_manager,
1753 &mut self.buffers,
1754 &mut self.split_view_states,
1755 &self.theme,
1756 false, self.config.editor.estimated_line_length,
1758 self.config.editor.highlight_context_bytes,
1759 self.config.editor.relative_line_numbers,
1760 self.config.editor.use_terminal_bg,
1761 self.session_mode || !self.software_cursor_only,
1762 self.software_cursor_only,
1763 self.tab_bar_visible,
1764 self.config.editor.show_vertical_scrollbar,
1765 self.config.editor.show_horizontal_scrollbar,
1766 self.config.editor.diagnostics_inline_text,
1767 self.config.editor.show_tilde,
1768 );
1769
1770 self.cached_layout.view_line_mappings = view_line_mappings;
1771 }
1772
1773 pub fn clear_search_history(&mut self) {
1776 if let Some(history) = self.prompt_histories.get_mut("search") {
1777 history.clear();
1778 }
1779 }
1780
1781 fn update_terminal_title(&mut self, display_name: &str) {
1788 if !self.config.editor.set_window_title {
1789 return;
1790 }
1791 let new_title = format!("{} \u{2014} Fresh", display_name);
1792 if self.last_window_title.as_deref() == Some(new_title.as_str()) {
1793 return;
1794 }
1795 crate::services::terminal_title::write_terminal_title(&new_title);
1796 self.last_window_title = Some(new_title);
1797 }
1798
1799 pub fn save_histories(&self) {
1802 if let Err(e) = self
1804 .authority
1805 .filesystem
1806 .create_dir_all(&self.dir_context.data_dir)
1807 {
1808 tracing::warn!("Failed to create data directory: {}", e);
1809 return;
1810 }
1811
1812 for (key, history) in &self.prompt_histories {
1814 let path = self.dir_context.prompt_history_path(key);
1815 if let Err(e) = history.save_to_file(&path) {
1816 tracing::warn!("Failed to save {} history: {}", key, e);
1817 } else {
1818 tracing::debug!("Saved {} history to {:?}", key, path);
1819 }
1820 }
1821 }
1822}