Skip to main content

fret_runtime/
window_text_boundary_mode.rs

1use std::collections::HashMap;
2
3use fret_core::AppWindowId;
4
5use crate::TextBoundaryMode;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8struct WindowTextBoundaryModeToken(u64);
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct WindowTextBoundaryModeHandle {
12    window: AppWindowId,
13    token: WindowTextBoundaryModeToken,
14}
15
16#[derive(Debug, Default)]
17struct WindowTextBoundaryModeWindowState {
18    base: Option<TextBoundaryMode>,
19    stack: Vec<(WindowTextBoundaryModeToken, TextBoundaryMode)>,
20}
21
22#[derive(Debug, Default)]
23pub struct WindowTextBoundaryModeService {
24    by_window: HashMap<AppWindowId, WindowTextBoundaryModeWindowState>,
25    next_token: u64,
26}
27
28impl WindowTextBoundaryModeService {
29    pub fn mode(&self, window: AppWindowId) -> Option<TextBoundaryMode> {
30        self.by_window
31            .get(&window)
32            .and_then(|state| state.stack.last().map(|(_, mode)| *mode).or(state.base))
33    }
34
35    pub fn set_base_mode(&mut self, window: AppWindowId, mode: TextBoundaryMode) {
36        let state = self.by_window.entry(window).or_default();
37        state.base = Some(mode);
38        self.gc_window(window);
39    }
40
41    pub fn clear_base_mode(&mut self, window: AppWindowId) {
42        if let Some(state) = self.by_window.get_mut(&window) {
43            state.base = None;
44        }
45        self.gc_window(window);
46    }
47
48    pub fn push_mode(
49        &mut self,
50        window: AppWindowId,
51        mode: TextBoundaryMode,
52    ) -> WindowTextBoundaryModeHandle {
53        let token = WindowTextBoundaryModeToken(self.next_token.max(1));
54        self.next_token = token.0.saturating_add(1);
55        let state = self.by_window.entry(window).or_default();
56        state.stack.push((token, mode));
57        WindowTextBoundaryModeHandle { window, token }
58    }
59
60    pub fn update_pushed_mode(
61        &mut self,
62        handle: WindowTextBoundaryModeHandle,
63        mode: TextBoundaryMode,
64    ) -> bool {
65        let Some(state) = self.by_window.get_mut(&handle.window) else {
66            return false;
67        };
68        for (token, entry) in &mut state.stack {
69            if *token == handle.token {
70                *entry = mode;
71                return true;
72            }
73        }
74        false
75    }
76
77    pub fn pop_mode(&mut self, handle: WindowTextBoundaryModeHandle) -> Option<TextBoundaryMode> {
78        let state = self.by_window.get_mut(&handle.window)?;
79
80        let idx = state
81            .stack
82            .iter()
83            .position(|(token, _)| *token == handle.token)?;
84        let (_, removed) = state.stack.remove(idx);
85        self.gc_window(handle.window);
86        Some(removed)
87    }
88
89    fn gc_window(&mut self, window: AppWindowId) {
90        if self
91            .by_window
92            .get(&window)
93            .is_some_and(|state| state.base.is_none() && state.stack.is_empty())
94        {
95            self.by_window.remove(&window);
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn stack_wins_over_base() {
106        let window = AppWindowId::default();
107        let mut svc = WindowTextBoundaryModeService::default();
108
109        svc.set_base_mode(window, TextBoundaryMode::UnicodeWord);
110        assert_eq!(svc.mode(window), Some(TextBoundaryMode::UnicodeWord));
111
112        let h1 = svc.push_mode(window, TextBoundaryMode::Identifier);
113        assert_eq!(svc.mode(window), Some(TextBoundaryMode::Identifier));
114
115        let h2 = svc.push_mode(window, TextBoundaryMode::UnicodeWord);
116        assert_eq!(svc.mode(window), Some(TextBoundaryMode::UnicodeWord));
117
118        svc.pop_mode(h2);
119        assert_eq!(svc.mode(window), Some(TextBoundaryMode::Identifier));
120
121        svc.pop_mode(h1);
122        assert_eq!(svc.mode(window), Some(TextBoundaryMode::UnicodeWord));
123    }
124
125    #[test]
126    fn clears_empty_window_state() {
127        let window = AppWindowId::default();
128        let mut svc = WindowTextBoundaryModeService::default();
129
130        svc.set_base_mode(window, TextBoundaryMode::UnicodeWord);
131        svc.clear_base_mode(window);
132        assert!(svc.by_window.is_empty());
133
134        let h = svc.push_mode(window, TextBoundaryMode::Identifier);
135        svc.pop_mode(h);
136        assert!(svc.by_window.is_empty());
137    }
138}