Skip to main content

fresh/app/
types.rs

1use crate::app::file_open::SortMode;
2use crate::input::keybindings::Action;
3use crate::model::event::{BufferId, SplitDirection, SplitId};
4use crate::services::async_bridge::LspMessageType;
5use ratatui::layout::Rect;
6use rust_i18n::t;
7use std::collections::{HashMap, HashSet};
8use std::ops::Range;
9use std::path::{Path, PathBuf};
10
11pub const DEFAULT_BACKGROUND_FILE: &str = "scripts/landscape-wide.txt";
12
13/// Pre-calculated line information for an event
14/// Calculated BEFORE buffer modification so line numbers are accurate
15#[derive(Debug, Clone, Default)]
16pub(super) struct EventLineInfo {
17    /// Start line (0-indexed) where the change begins
18    pub start_line: usize,
19    /// End line (0-indexed) where the change ends (in original buffer for deletes)
20    pub end_line: usize,
21    /// Number of lines added (for inserts) or removed (for deletes)
22    pub line_delta: i32,
23}
24
25/// Search state for find/replace functionality
26#[derive(Debug, Clone)]
27pub(super) struct SearchState {
28    /// The search query
29    pub query: String,
30    /// All match positions in the buffer (byte offsets)
31    pub matches: Vec<usize>,
32    /// Index of the currently selected match
33    pub current_match_index: Option<usize>,
34    /// Whether search wraps around at document boundaries
35    pub wrap_search: bool,
36    /// Optional search range (for search in selection)
37    pub search_range: Option<Range<usize>>,
38}
39
40/// A bookmark in the editor (position in a specific buffer)
41#[derive(Debug, Clone)]
42pub(super) struct Bookmark {
43    /// Buffer ID where the bookmark is set
44    pub buffer_id: BufferId,
45    /// Byte offset position in the buffer
46    pub position: usize,
47}
48
49/// State for interactive replace (query-replace)
50#[derive(Debug, Clone)]
51pub(super) struct InteractiveReplaceState {
52    /// The search pattern
53    pub search: String,
54    /// The replacement text
55    pub replacement: String,
56    /// Current match position (byte offset of the match we're at)
57    pub current_match_pos: usize,
58    /// Starting position (to detect when we've wrapped around full circle)
59    pub start_pos: usize,
60    /// Whether we've wrapped around to the beginning
61    pub has_wrapped: bool,
62    /// Number of replacements made so far
63    pub replacements_made: usize,
64}
65
66/// The kind of buffer (file-backed or virtual)
67#[derive(Debug, Clone, PartialEq)]
68pub enum BufferKind {
69    /// A buffer backed by a file on disk
70    File {
71        /// Path to the file
72        path: PathBuf,
73        /// LSP URI for the file
74        uri: Option<lsp_types::Uri>,
75    },
76    /// A virtual buffer (not backed by a file)
77    /// Used for special buffers like *Diagnostics*, *Grep*, etc.
78    Virtual {
79        /// The buffer's mode (e.g., "diagnostics-list", "grep-results")
80        mode: String,
81    },
82}
83
84/// Metadata associated with a buffer
85#[derive(Debug, Clone)]
86pub struct BufferMetadata {
87    /// The kind of buffer (file or virtual)
88    pub kind: BufferKind,
89
90    /// Display name for the buffer (project-relative path or filename or *BufferName*)
91    pub display_name: String,
92
93    /// Whether LSP is enabled for this buffer (always false for virtual buffers)
94    pub lsp_enabled: bool,
95
96    /// Reason LSP is disabled (if applicable)
97    pub lsp_disabled_reason: Option<String>,
98
99    /// Whether the buffer is read-only (typically true for virtual buffers)
100    pub read_only: bool,
101
102    /// Whether the buffer contains binary content
103    /// Binary buffers are automatically read-only and render unprintable chars as code points
104    pub binary: bool,
105
106    /// LSP server instance IDs that have received didOpen for this buffer.
107    /// Used to ensure didOpen is sent before any requests to a new/restarted server.
108    /// When a server restarts, it gets a new ID, so didOpen is automatically resent.
109    /// Old IDs are harmless - they just remain in the set but don't match any active server.
110    pub lsp_opened_with: HashSet<u64>,
111
112    /// Whether this buffer should be hidden from tabs (used for composite source buffers)
113    pub hidden_from_tabs: bool,
114
115    /// Stable recovery ID for unnamed buffers.
116    /// For file-backed buffers, recovery ID is computed from the path hash.
117    /// For unnamed buffers, this is generated once and reused across auto-saves.
118    pub recovery_id: Option<String>,
119}
120
121impl BufferMetadata {
122    /// Get the file path if this is a file-backed buffer
123    pub fn file_path(&self) -> Option<&PathBuf> {
124        match &self.kind {
125            BufferKind::File { path, .. } => Some(path),
126            BufferKind::Virtual { .. } => None,
127        }
128    }
129
130    /// Get the file URI if this is a file-backed buffer
131    pub fn file_uri(&self) -> Option<&lsp_types::Uri> {
132        match &self.kind {
133            BufferKind::File { uri, .. } => uri.as_ref(),
134            BufferKind::Virtual { .. } => None,
135        }
136    }
137
138    /// Check if this is a virtual buffer
139    pub fn is_virtual(&self) -> bool {
140        matches!(self.kind, BufferKind::Virtual { .. })
141    }
142
143    /// Get the mode name for virtual buffers
144    pub fn virtual_mode(&self) -> Option<&str> {
145        match &self.kind {
146            BufferKind::Virtual { mode } => Some(mode),
147            BufferKind::File { .. } => None,
148        }
149    }
150}
151
152impl Default for BufferMetadata {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl BufferMetadata {
159    /// Create new metadata for a buffer (unnamed, file-backed)
160    pub fn new() -> Self {
161        Self {
162            kind: BufferKind::File {
163                path: PathBuf::new(),
164                uri: None,
165            },
166            display_name: t!("buffer.no_name").to_string(),
167            lsp_enabled: true,
168            lsp_disabled_reason: None,
169            read_only: false,
170            binary: false,
171            lsp_opened_with: HashSet::new(),
172            hidden_from_tabs: false,
173            recovery_id: None,
174        }
175    }
176
177    /// Create new metadata for an unnamed buffer with a custom display name
178    /// Used for buffers created from stdin or other non-file sources
179    pub fn new_unnamed(display_name: String) -> Self {
180        Self {
181            kind: BufferKind::File {
182                path: PathBuf::new(),
183                uri: None,
184            },
185            display_name,
186            lsp_enabled: false, // No file path, so no LSP
187            lsp_disabled_reason: Some(t!("lsp.disabled.unnamed").to_string()),
188            read_only: false,
189            binary: false,
190            lsp_opened_with: HashSet::new(),
191            hidden_from_tabs: false,
192            recovery_id: None,
193        }
194    }
195
196    /// Create metadata for a file-backed buffer
197    ///
198    /// # Arguments
199    /// * `path` - The canonical absolute path to the file
200    /// * `working_dir` - The canonical working directory for computing relative display name
201    pub fn with_file(path: PathBuf, working_dir: &Path) -> Self {
202        // Compute URI from the absolute path
203        let file_uri = url::Url::from_file_path(&path)
204            .ok()
205            .and_then(|u| u.as_str().parse::<lsp_types::Uri>().ok());
206
207        // Compute display name (project-relative when under working_dir, else absolute path).
208        // Use canonicalized forms first to handle macOS /var -> /private/var differences.
209        let display_name = Self::display_name_for_path(&path, working_dir);
210
211        // Check if this is a library file (outside project or in vendor directories)
212        let (lsp_enabled, lsp_disabled_reason) = if Self::is_library_path(&path, working_dir) {
213            (false, Some(t!("lsp.disabled.library_file").to_string()))
214        } else {
215            (true, None)
216        };
217
218        Self {
219            kind: BufferKind::File {
220                path,
221                uri: file_uri,
222            },
223            display_name,
224            lsp_enabled,
225            lsp_disabled_reason,
226            read_only: false,
227            binary: false,
228            lsp_opened_with: HashSet::new(),
229            hidden_from_tabs: false,
230            recovery_id: None,
231        }
232    }
233
234    /// Check if a path is a library file (outside project root or in vendor directories)
235    ///
236    /// Library files include:
237    /// - Files outside the working directory
238    /// - Files in common vendor/dependency directories (.cargo, node_modules, etc.)
239    pub fn is_library_path(path: &Path, working_dir: &Path) -> bool {
240        // Check if outside working directory
241        if !path.starts_with(working_dir) {
242            return true;
243        }
244
245        // Check for common library paths within the project
246        let path_str = path.to_string_lossy();
247
248        // Rust: .cargo directory (can be within project for vendor'd crates)
249        if path_str.contains("/.cargo/") || path_str.contains("\\.cargo\\") {
250            return true;
251        }
252
253        // Node.js: node_modules
254        if path_str.contains("/node_modules/") || path_str.contains("\\node_modules\\") {
255            return true;
256        }
257
258        // Python: site-packages, dist-packages
259        if path_str.contains("/site-packages/")
260            || path_str.contains("\\site-packages\\")
261            || path_str.contains("/dist-packages/")
262            || path_str.contains("\\dist-packages\\")
263        {
264            return true;
265        }
266
267        // Go: pkg/mod
268        if path_str.contains("/pkg/mod/") || path_str.contains("\\pkg\\mod\\") {
269            return true;
270        }
271
272        // Ruby: gems
273        if path_str.contains("/gems/") || path_str.contains("\\gems\\") {
274            return true;
275        }
276
277        // Java/Gradle: .gradle
278        if path_str.contains("/.gradle/") || path_str.contains("\\.gradle\\") {
279            return true;
280        }
281
282        // Maven: .m2
283        if path_str.contains("/.m2/") || path_str.contains("\\.m2\\") {
284            return true;
285        }
286
287        false
288    }
289
290    /// Compute display name relative to working_dir when possible, otherwise absolute
291    pub fn display_name_for_path(path: &Path, working_dir: &Path) -> String {
292        // Canonicalize working_dir to normalize platform-specific prefixes
293        let canonical_working_dir = working_dir
294            .canonicalize()
295            .unwrap_or_else(|_| working_dir.to_path_buf());
296
297        // Try to canonicalize the file path; if it fails (e.g., new file), fall back to absolute
298        let absolute_path = if path.is_absolute() {
299            path.to_path_buf()
300        } else {
301            // If we were given a relative path, anchor it to working_dir
302            canonical_working_dir.join(path)
303        };
304        let canonical_path = absolute_path
305            .canonicalize()
306            .unwrap_or_else(|_| absolute_path.clone());
307
308        // Prefer canonical comparison first, then raw prefix as a fallback
309        let relative = canonical_path
310            .strip_prefix(&canonical_working_dir)
311            .or_else(|_| path.strip_prefix(working_dir))
312            .ok()
313            .and_then(|rel| rel.to_str().map(|s| s.to_string()));
314
315        relative
316            .or_else(|| canonical_path.to_str().map(|s| s.to_string()))
317            .unwrap_or_else(|| t!("buffer.unknown").to_string())
318    }
319
320    /// Create metadata for a virtual buffer (not backed by a file)
321    ///
322    /// # Arguments
323    /// * `name` - Display name (e.g., "*Diagnostics*")
324    /// * `mode` - Buffer mode for keybindings (e.g., "diagnostics-list")
325    /// * `read_only` - Whether the buffer should be read-only
326    pub fn virtual_buffer(name: String, mode: String, read_only: bool) -> Self {
327        Self {
328            kind: BufferKind::Virtual { mode },
329            display_name: name,
330            lsp_enabled: false, // Virtual buffers don't use LSP
331            lsp_disabled_reason: Some(t!("lsp.disabled.virtual").to_string()),
332            read_only,
333            binary: false,
334            lsp_opened_with: HashSet::new(),
335            hidden_from_tabs: false,
336            recovery_id: None,
337        }
338    }
339
340    /// Create metadata for a hidden virtual buffer (for composite source buffers)
341    /// These buffers are not shown in tabs and are managed by their parent composite buffer.
342    /// Hidden buffers are always read-only to prevent accidental edits.
343    pub fn hidden_virtual_buffer(name: String, mode: String) -> Self {
344        Self {
345            kind: BufferKind::Virtual { mode },
346            display_name: name,
347            lsp_enabled: false,
348            lsp_disabled_reason: Some(t!("lsp.disabled.virtual").to_string()),
349            read_only: true, // Hidden buffers are always read-only
350            binary: false,
351            lsp_opened_with: HashSet::new(),
352            hidden_from_tabs: true,
353            recovery_id: None,
354        }
355    }
356
357    /// Disable LSP for this buffer with a reason
358    pub fn disable_lsp(&mut self, reason: String) {
359        self.lsp_enabled = false;
360        self.lsp_disabled_reason = Some(reason);
361    }
362}
363
364/// State for macro recording
365#[derive(Debug, Clone)]
366pub(super) struct MacroRecordingState {
367    /// The register key for this macro
368    pub key: char,
369    /// Actions recorded so far
370    pub actions: Vec<Action>,
371}
372
373/// LSP progress information
374#[derive(Debug, Clone)]
375pub(super) struct LspProgressInfo {
376    pub language: String,
377    pub title: String,
378    pub message: Option<String>,
379    pub percentage: Option<u32>,
380}
381
382/// LSP message entry (for window messages and logs)
383#[derive(Debug, Clone)]
384#[allow(dead_code)]
385pub(super) struct LspMessageEntry {
386    pub language: String,
387    pub message_type: LspMessageType,
388    pub message: String,
389    pub timestamp: std::time::Instant,
390}
391
392/// Types of UI elements that can be hovered over
393#[derive(Debug, Clone, PartialEq)]
394pub enum HoverTarget {
395    /// Hovering over a split separator (split_id, direction)
396    SplitSeparator(SplitId, SplitDirection),
397    /// Hovering over a scrollbar thumb (split_id)
398    ScrollbarThumb(SplitId),
399    /// Hovering over a scrollbar track (split_id)
400    ScrollbarTrack(SplitId),
401    /// Hovering over a menu bar item (menu_index)
402    MenuBarItem(usize),
403    /// Hovering over a menu dropdown item (menu_index, item_index)
404    MenuDropdownItem(usize, usize),
405    /// Hovering over a submenu item (depth, item_index) - depth 1+ for nested submenus
406    SubmenuItem(usize, usize),
407    /// Hovering over a popup list item (popup_index in stack, item_index)
408    PopupListItem(usize, usize),
409    /// Hovering over a suggestion item (item_index)
410    SuggestionItem(usize),
411    /// Hovering over the file explorer border (for resize)
412    FileExplorerBorder,
413    /// Hovering over a file browser navigation shortcut
414    FileBrowserNavShortcut(usize),
415    /// Hovering over a file browser file/directory entry
416    FileBrowserEntry(usize),
417    /// Hovering over a file browser column header
418    FileBrowserHeader(SortMode),
419    /// Hovering over the file browser scrollbar
420    FileBrowserScrollbar,
421    /// Hovering over the file browser "Show Hidden" checkbox
422    FileBrowserShowHiddenCheckbox,
423    /// Hovering over a tab name (buffer_id, split_id) - for non-active tabs
424    TabName(BufferId, SplitId),
425    /// Hovering over a tab close button (buffer_id, split_id)
426    TabCloseButton(BufferId, SplitId),
427    /// Hovering over a close split button (split_id)
428    CloseSplitButton(SplitId),
429    /// Hovering over a maximize/unmaximize split button (split_id)
430    MaximizeSplitButton(SplitId),
431    /// Hovering over the file explorer close button
432    FileExplorerCloseButton,
433    /// Hovering over a file explorer item's status indicator (path)
434    FileExplorerStatusIndicator(std::path::PathBuf),
435    /// Hovering over the status bar LSP indicator
436    StatusBarLspIndicator,
437    /// Hovering over the status bar warning badge
438    StatusBarWarningBadge,
439    /// Hovering over the status bar line ending indicator
440    StatusBarLineEndingIndicator,
441    /// Hovering over the status bar language indicator
442    StatusBarLanguageIndicator,
443    /// Hovering over the search options "Case Sensitive" checkbox
444    SearchOptionCaseSensitive,
445    /// Hovering over the search options "Whole Word" checkbox
446    SearchOptionWholeWord,
447    /// Hovering over the search options "Regex" checkbox
448    SearchOptionRegex,
449    /// Hovering over the search options "Confirm Each" checkbox
450    SearchOptionConfirmEach,
451    /// Hovering over a tab context menu item (item_index)
452    TabContextMenuItem(usize),
453}
454
455/// Tab context menu items
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457pub enum TabContextMenuItem {
458    /// Close this tab
459    Close,
460    /// Close all other tabs
461    CloseOthers,
462    /// Close tabs to the right
463    CloseToRight,
464    /// Close tabs to the left
465    CloseToLeft,
466    /// Close all tabs
467    CloseAll,
468}
469
470impl TabContextMenuItem {
471    /// Get all menu items in order
472    pub fn all() -> &'static [Self] {
473        &[
474            Self::Close,
475            Self::CloseOthers,
476            Self::CloseToRight,
477            Self::CloseToLeft,
478            Self::CloseAll,
479        ]
480    }
481
482    /// Get the display label for this menu item
483    pub fn label(&self) -> String {
484        match self {
485            Self::Close => t!("tab.close").to_string(),
486            Self::CloseOthers => t!("tab.close_others").to_string(),
487            Self::CloseToRight => t!("tab.close_to_right").to_string(),
488            Self::CloseToLeft => t!("tab.close_to_left").to_string(),
489            Self::CloseAll => t!("tab.close_all").to_string(),
490        }
491    }
492}
493
494/// State for tab context menu (right-click popup on tabs)
495#[derive(Debug, Clone)]
496pub struct TabContextMenu {
497    /// The buffer ID this context menu is for
498    pub buffer_id: BufferId,
499    /// The split ID where the tab is located
500    pub split_id: SplitId,
501    /// Screen position where the menu should appear (x, y)
502    pub position: (u16, u16),
503    /// Currently highlighted menu item index
504    pub highlighted: usize,
505}
506
507impl TabContextMenu {
508    /// Create a new tab context menu
509    pub fn new(buffer_id: BufferId, split_id: SplitId, x: u16, y: u16) -> Self {
510        Self {
511            buffer_id,
512            split_id,
513            position: (x, y),
514            highlighted: 0,
515        }
516    }
517
518    /// Get the currently highlighted item
519    pub fn highlighted_item(&self) -> TabContextMenuItem {
520        TabContextMenuItem::all()[self.highlighted]
521    }
522
523    /// Move highlight down
524    pub fn next_item(&mut self) {
525        let items = TabContextMenuItem::all();
526        self.highlighted = (self.highlighted + 1) % items.len();
527    }
528
529    /// Move highlight up
530    pub fn prev_item(&mut self) {
531        let items = TabContextMenuItem::all();
532        self.highlighted = if self.highlighted == 0 {
533            items.len() - 1
534        } else {
535            self.highlighted - 1
536        };
537    }
538}
539
540/// Drop zone for tab drag-and-drop
541/// Indicates where a dragged tab will be placed when released
542#[derive(Debug, Clone, Copy, PartialEq, Eq)]
543pub enum TabDropZone {
544    /// Drop into an existing split's tab bar (before tab at index, or at end if None)
545    /// (target_split_id, insert_index)
546    TabBar(SplitId, Option<usize>),
547    /// Create a new split on the left edge of the target split
548    SplitLeft(SplitId),
549    /// Create a new split on the right edge of the target split
550    SplitRight(SplitId),
551    /// Create a new split on the top edge of the target split
552    SplitTop(SplitId),
553    /// Create a new split on the bottom edge of the target split
554    SplitBottom(SplitId),
555    /// Drop into the center of a split (switch to that split's tab bar)
556    SplitCenter(SplitId),
557}
558
559impl TabDropZone {
560    /// Get the split ID this drop zone is associated with
561    pub fn split_id(&self) -> SplitId {
562        match self {
563            Self::TabBar(id, _)
564            | Self::SplitLeft(id)
565            | Self::SplitRight(id)
566            | Self::SplitTop(id)
567            | Self::SplitBottom(id)
568            | Self::SplitCenter(id) => *id,
569        }
570    }
571}
572
573/// State for a tab being dragged
574#[derive(Debug, Clone)]
575pub struct TabDragState {
576    /// The buffer being dragged
577    pub buffer_id: BufferId,
578    /// The split the tab was dragged from
579    pub source_split_id: SplitId,
580    /// Starting mouse position when drag began
581    pub start_position: (u16, u16),
582    /// Current mouse position
583    pub current_position: (u16, u16),
584    /// Currently detected drop zone (if any)
585    pub drop_zone: Option<TabDropZone>,
586}
587
588impl TabDragState {
589    /// Create a new tab drag state
590    pub fn new(buffer_id: BufferId, source_split_id: SplitId, start_position: (u16, u16)) -> Self {
591        Self {
592            buffer_id,
593            source_split_id,
594            start_position,
595            current_position: start_position,
596            drop_zone: None,
597        }
598    }
599
600    /// Check if the drag has moved enough to be considered a real drag (not just a click)
601    pub fn is_dragging(&self) -> bool {
602        let dx = (self.current_position.0 as i32 - self.start_position.0 as i32).abs();
603        let dy = (self.current_position.1 as i32 - self.start_position.1 as i32).abs();
604        dx > 3 || dy > 3 // Threshold of 3 pixels before drag activates
605    }
606}
607
608/// Mouse state tracking
609#[derive(Debug, Clone, Default)]
610pub(super) struct MouseState {
611    /// Whether we're currently dragging a scrollbar
612    pub dragging_scrollbar: Option<SplitId>,
613    /// Last mouse position
614    pub last_position: Option<(u16, u16)>,
615    /// Mouse hover for LSP: byte position being hovered, timer start, and screen position
616    /// Format: (byte_position, hover_start_instant, screen_x, screen_y)
617    pub lsp_hover_state: Option<(usize, std::time::Instant, u16, u16)>,
618    /// Whether we've already sent a hover request for the current position
619    pub lsp_hover_request_sent: bool,
620    /// Initial mouse row when starting to drag the scrollbar thumb
621    /// Used to calculate relative movement rather than jumping
622    pub drag_start_row: Option<u16>,
623    /// Initial viewport top_byte when starting to drag the scrollbar thumb
624    pub drag_start_top_byte: Option<usize>,
625    /// Whether we're currently dragging a split separator
626    /// Stores (split_id, direction) for the separator being dragged
627    pub dragging_separator: Option<(SplitId, SplitDirection)>,
628    /// Initial mouse position when starting to drag a separator
629    pub drag_start_position: Option<(u16, u16)>,
630    /// Initial split ratio when starting to drag a separator
631    pub drag_start_ratio: Option<f32>,
632    /// Whether we're currently dragging the file explorer border
633    pub dragging_file_explorer: bool,
634    /// Initial file explorer width percentage when starting to drag
635    pub drag_start_explorer_width: Option<f32>,
636    /// Current hover target (if any)
637    pub hover_target: Option<HoverTarget>,
638    /// Whether we're currently doing a text selection drag
639    pub dragging_text_selection: bool,
640    /// The split where text selection started
641    pub drag_selection_split: Option<SplitId>,
642    /// The buffer byte position where the selection anchor is
643    pub drag_selection_anchor: Option<usize>,
644    /// Tab drag state (for drag-to-split functionality)
645    pub dragging_tab: Option<TabDragState>,
646    /// Whether we're currently dragging a popup scrollbar (popup index)
647    pub dragging_popup_scrollbar: Option<usize>,
648    /// Initial scroll offset when starting to drag popup scrollbar
649    pub drag_start_popup_scroll: Option<usize>,
650    /// Whether we're currently selecting text in a popup (popup index)
651    pub selecting_in_popup: Option<usize>,
652}
653
654/// Mapping from visual row to buffer positions for mouse click handling
655/// Each entry represents one visual row with byte position info for click handling
656#[derive(Debug, Clone, Default)]
657pub struct ViewLineMapping {
658    /// Source byte offset for each character (None for injected/virtual content)
659    pub char_source_bytes: Vec<Option<usize>>,
660    /// Character index at each visual column (for O(1) mouse clicks)
661    pub visual_to_char: Vec<usize>,
662    /// Last valid byte position in this visual row (newline for real lines, last char for wrapped)
663    /// Clicks past end of visible text position cursor here
664    pub line_end_byte: usize,
665}
666
667impl ViewLineMapping {
668    /// Get source byte at a given visual column (O(1) for mouse clicks)
669    #[inline]
670    pub fn source_byte_at_visual_col(&self, visual_col: usize) -> Option<usize> {
671        let char_idx = self.visual_to_char.get(visual_col).copied()?;
672        self.char_source_bytes.get(char_idx).copied().flatten()
673    }
674}
675
676/// Type alias for popup area layout information used in mouse hit testing.
677/// Fields: (popup_index, rect, inner_rect, scroll_offset, num_items, scrollbar_rect, total_lines)
678pub(crate) type PopupAreaLayout = (usize, Rect, Rect, usize, usize, Option<Rect>, usize);
679
680/// Cached layout information for mouse hit testing
681#[derive(Debug, Clone, Default)]
682pub(crate) struct CachedLayout {
683    /// File explorer area (if visible)
684    pub file_explorer_area: Option<Rect>,
685    /// Editor content area (excluding file explorer)
686    pub editor_content_area: Option<Rect>,
687    /// Individual split areas with their scrollbar areas and thumb positions
688    /// (split_id, buffer_id, content_rect, scrollbar_rect, thumb_start, thumb_end)
689    pub split_areas: Vec<(SplitId, BufferId, Rect, Rect, usize, usize)>,
690    /// Split separator positions for drag resize
691    /// (split_id, direction, x, y, length)
692    pub separator_areas: Vec<(SplitId, SplitDirection, u16, u16, u16)>,
693    /// Popup areas for mouse hit testing
694    /// scrollbar_rect is Some if popup has a scrollbar
695    pub popup_areas: Vec<PopupAreaLayout>,
696    /// Suggestions area for mouse hit testing
697    /// (inner_rect, scroll_start_idx, visible_count, total_count)
698    pub suggestions_area: Option<(Rect, usize, usize, usize)>,
699    /// Tab layouts per split for mouse interaction
700    pub tab_layouts: HashMap<SplitId, crate::view::ui::tabs::TabLayout>,
701    /// Close split button hit areas
702    /// (split_id, row, start_col, end_col)
703    pub close_split_areas: Vec<(SplitId, u16, u16, u16)>,
704    /// Maximize split button hit areas
705    /// (split_id, row, start_col, end_col)
706    pub maximize_split_areas: Vec<(SplitId, u16, u16, u16)>,
707    /// View line mappings for accurate mouse click positioning per split
708    /// Maps visual row index to character position mappings
709    /// Used to translate screen coordinates to buffer byte positions
710    pub view_line_mappings: HashMap<SplitId, Vec<ViewLineMapping>>,
711    /// Settings modal layout for hit testing
712    pub settings_layout: Option<crate::view::settings::SettingsLayout>,
713    /// Status bar area (row, x, width)
714    pub status_bar_area: Option<(u16, u16, u16)>,
715    /// Status bar LSP indicator area (row, start_col, end_col)
716    pub status_bar_lsp_area: Option<(u16, u16, u16)>,
717    /// Status bar warning badge area (row, start_col, end_col)
718    pub status_bar_warning_area: Option<(u16, u16, u16)>,
719    /// Status bar line ending indicator area (row, start_col, end_col)
720    pub status_bar_line_ending_area: Option<(u16, u16, u16)>,
721    /// Status bar language indicator area (row, start_col, end_col)
722    pub status_bar_language_area: Option<(u16, u16, u16)>,
723    /// Status bar message area (row, start_col, end_col) - clickable to show status log
724    pub status_bar_message_area: Option<(u16, u16, u16)>,
725    /// Search options layout for checkbox hit testing
726    pub search_options_layout: Option<crate::view::ui::status_bar::SearchOptionsLayout>,
727    /// Menu bar layout for hit testing
728    pub menu_layout: Option<crate::view::ui::menu::MenuLayout>,
729}