fret_runtime/
window_text_boundary_mode.rs1use 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}