sql_cli/ui/state/
shadow_state.rs

1//! Shadow State Manager - Observes state transitions without controlling them
2//!
3//! This module runs in parallel to the existing state system, observing and
4//! logging state changes to help us understand patterns before migrating to
5//! centralized state management.
6
7use crate::buffer::{AppMode, Buffer, BufferAPI};
8use std::collections::VecDeque;
9use std::time::Instant;
10use tracing::{debug, info, warn};
11
12/// Simplified application state for observation
13#[derive(Debug, Clone, PartialEq)]
14pub enum AppState {
15    /// Command/query input mode
16    Command,
17    /// Results navigation mode
18    Results,
19    /// Any search mode active
20    Search { search_type: SearchType },
21    /// Help mode
22    Help,
23    /// Debug view
24    Debug,
25    /// History search mode
26    History,
27    /// Jump to row mode
28    JumpToRow,
29    /// Column statistics view
30    ColumnStats,
31}
32
33/// Types of search that can be active
34#[derive(Debug, Clone, PartialEq)]
35pub enum SearchType {
36    Vim,    // / search
37    Column, // Column name search
38    Data,   // Data content search
39    Fuzzy,  // Fuzzy filter
40}
41
42/// Shadow state manager that observes but doesn't control
43pub struct ShadowStateManager {
44    /// Current observed state
45    state: AppState,
46
47    /// Previous state for transition tracking
48    previous_state: Option<AppState>,
49
50    /// History of state transitions
51    history: VecDeque<StateTransition>,
52
53    /// Count of transitions observed
54    transition_count: usize,
55
56    /// Track if we're in sync with actual state
57    discrepancies: Vec<String>,
58}
59
60#[derive(Debug, Clone)]
61struct StateTransition {
62    timestamp: Instant,
63    from: AppState,
64    to: AppState,
65    trigger: String,
66}
67
68impl ShadowStateManager {
69    pub fn new() -> Self {
70        info!(target: "shadow_state", "Shadow state manager initialized");
71
72        Self {
73            state: AppState::Command,
74            previous_state: None,
75            history: VecDeque::with_capacity(100),
76            transition_count: 0,
77            discrepancies: Vec::new(),
78        }
79    }
80
81    /// Observe a mode change from the existing system
82    pub fn observe_mode_change(&mut self, mode: AppMode, trigger: &str) {
83        let new_state = self.mode_to_state(mode.clone());
84
85        // Only log if state actually changed
86        if new_state == self.state {
87            debug!(target: "shadow_state", 
88                "Redundant mode change to {:?} ignored", mode);
89        } else {
90            self.transition_count += 1;
91
92            info!(target: "shadow_state",
93                "[#{}] {} -> {} (trigger: {})",
94                self.transition_count,
95                self.state_display(&self.state),
96                self.state_display(&new_state),
97                trigger
98            );
99
100            // Record transition
101            let transition = StateTransition {
102                timestamp: Instant::now(),
103                from: self.state.clone(),
104                to: new_state.clone(),
105                trigger: trigger.to_string(),
106            };
107
108            self.history.push_back(transition);
109            if self.history.len() > 100 {
110                self.history.pop_front();
111            }
112
113            // Update state
114            self.previous_state = Some(self.state.clone());
115            self.state = new_state;
116
117            // Log what side effects should happen
118            self.log_expected_side_effects();
119        }
120    }
121
122    // ============= Write-Through Methods (Temporary) =============
123    // These methods update both shadow state and buffer during migration
124    // Eventually the buffer parameter will be removed
125
126    /// Set mode - authoritative method that updates both shadow state and buffer
127    pub fn set_mode(&mut self, mode: AppMode, buffer: &mut Buffer, trigger: &str) {
128        let new_state = self.mode_to_state(mode.clone());
129
130        // Only proceed if state actually changed
131        if new_state == self.state {
132            debug!(target: "shadow_state", 
133                "Redundant mode change to {:?} ignored", mode);
134        } else {
135            self.transition_count += 1;
136
137            info!(target: "shadow_state",
138                "[#{}] {} -> {} (trigger: {})",
139                self.transition_count,
140                self.state_display(&self.state),
141                self.state_display(&new_state),
142                trigger
143            );
144
145            // Record transition
146            let transition = StateTransition {
147                timestamp: Instant::now(),
148                from: self.state.clone(),
149                to: new_state.clone(),
150                trigger: trigger.to_string(),
151            };
152
153            self.history.push_back(transition);
154            if self.history.len() > 100 {
155                self.history.pop_front();
156            }
157
158            // Update shadow state
159            self.previous_state = Some(self.state.clone());
160            self.state = new_state;
161
162            // Update buffer (temporary - will be removed)
163            buffer.set_mode(mode);
164
165            // Log what side effects should happen
166            self.log_expected_side_effects();
167        }
168    }
169
170    /// Switch to Results mode
171    pub fn switch_to_results(&mut self, buffer: &mut Buffer) {
172        self.set_mode(AppMode::Results, buffer, "switch_to_results");
173    }
174
175    /// Switch to Command mode
176    pub fn switch_to_command(&mut self, buffer: &mut Buffer) {
177        self.set_mode(AppMode::Command, buffer, "switch_to_command");
178    }
179
180    /// Start search with specific type
181    pub fn start_search(&mut self, search_type: SearchType, buffer: &mut Buffer, trigger: &str) {
182        let mode = match search_type {
183            SearchType::Column => AppMode::ColumnSearch,
184            SearchType::Data | SearchType::Vim => AppMode::Search,
185            SearchType::Fuzzy => AppMode::FuzzyFilter,
186        };
187
188        // Update state
189        self.state = AppState::Search {
190            search_type: search_type.clone(),
191        };
192        self.transition_count += 1;
193
194        info!(target: "shadow_state",
195            "[#{}] Starting {:?} search (trigger: {})",
196            self.transition_count, search_type, trigger
197        );
198
199        // Update buffer
200        buffer.set_mode(mode);
201    }
202
203    /// Exit current mode to Results
204    pub fn exit_to_results(&mut self, buffer: &mut Buffer) {
205        self.set_mode(AppMode::Results, buffer, "exit_to_results");
206    }
207
208    // ============= Original Observer Methods =============
209
210    /// Observe search starting
211    pub fn observe_search_start(&mut self, search_type: SearchType, trigger: &str) {
212        let new_state = AppState::Search {
213            search_type: search_type.clone(),
214        };
215
216        if !matches!(self.state, AppState::Search { .. }) {
217            self.transition_count += 1;
218
219            info!(target: "shadow_state",
220                "[#{}] {} -> {:?} search (trigger: {})",
221                self.transition_count,
222                self.state_display(&self.state),
223                search_type,
224                trigger
225            );
226
227            self.previous_state = Some(self.state.clone());
228            self.state = new_state;
229
230            // Note: When we see search start, other searches should be cleared
231            warn!(target: "shadow_state",
232                "⚠️  Search started - verify other search states were cleared!");
233        }
234    }
235
236    /// Observe search ending
237    pub fn observe_search_end(&mut self, trigger: &str) {
238        if matches!(self.state, AppState::Search { .. }) {
239            // Return to Results mode (assuming we were in results before search)
240            let new_state = AppState::Results;
241
242            info!(target: "shadow_state",
243                "[#{}] Exiting search -> {} (trigger: {})",
244                self.transition_count,
245                self.state_display(&new_state),
246                trigger
247            );
248
249            self.previous_state = Some(self.state.clone());
250            self.state = new_state;
251
252            // Log expected cleanup
253            info!(target: "shadow_state", 
254                "✓ Expected side effects: Clear search UI, restore navigation keys");
255        }
256    }
257
258    /// Check if we're in search mode
259    #[must_use]
260    pub fn is_search_active(&self) -> bool {
261        matches!(self.state, AppState::Search { .. })
262    }
263
264    /// Get current search type if active
265    #[must_use]
266    pub fn get_search_type(&self) -> Option<SearchType> {
267        if let AppState::Search { ref search_type } = self.state {
268            Some(search_type.clone())
269        } else {
270            None
271        }
272    }
273
274    /// Get display string for status line
275    #[must_use]
276    pub fn status_display(&self) -> String {
277        format!("[Shadow: {}]", self.state_display(&self.state))
278    }
279
280    /// Get debug info about recent transitions
281    #[must_use]
282    pub fn debug_info(&self) -> String {
283        let mut info = format!(
284            "Shadow State Debug (transitions: {})\n",
285            self.transition_count
286        );
287        info.push_str(&format!("Current: {:?}\n", self.state));
288
289        if !self.history.is_empty() {
290            info.push_str("\nRecent transitions:\n");
291            for transition in self.history.iter().rev().take(5) {
292                info.push_str(&format!(
293                    "  {:?} ago: {} -> {} ({})\n",
294                    transition.timestamp.elapsed(),
295                    self.state_display(&transition.from),
296                    self.state_display(&transition.to),
297                    transition.trigger
298                ));
299            }
300        }
301
302        if !self.discrepancies.is_empty() {
303            info.push_str("\n⚠️  Discrepancies detected:\n");
304            for disc in self.discrepancies.iter().rev().take(3) {
305                info.push_str(&format!("  - {disc}\n"));
306            }
307        }
308
309        info
310    }
311
312    /// Report a discrepancy between shadow and actual state
313    pub fn report_discrepancy(&mut self, expected: &str, actual: &str) {
314        let msg = format!("Expected: {expected}, Actual: {actual}");
315        warn!(target: "shadow_state", "Discrepancy: {}", msg);
316        self.discrepancies.push(msg);
317    }
318
319    // ============= Comprehensive Read Methods =============
320    // These methods make shadow state easy to query and will eventually
321    // replace all buffer().get_mode() calls
322
323    /// Get the current state
324    #[must_use]
325    pub fn get_state(&self) -> &AppState {
326        &self.state
327    }
328
329    /// Get the current mode (converts state to `AppMode` for compatibility)
330    #[must_use]
331    pub fn get_mode(&self) -> AppMode {
332        match &self.state {
333            AppState::Command => AppMode::Command,
334            AppState::Results => AppMode::Results,
335            AppState::Search { search_type } => match search_type {
336                SearchType::Column => AppMode::ColumnSearch,
337                SearchType::Data => AppMode::Search,
338                SearchType::Fuzzy => AppMode::FuzzyFilter,
339                SearchType::Vim => AppMode::Search, // Vim search uses Search mode
340            },
341            AppState::Help => AppMode::Help,
342            AppState::Debug => AppMode::Debug,
343            AppState::History => AppMode::History,
344            AppState::JumpToRow => AppMode::JumpToRow,
345            AppState::ColumnStats => AppMode::ColumnStats,
346        }
347    }
348
349    /// Check if currently in Results mode
350    #[must_use]
351    pub fn is_in_results_mode(&self) -> bool {
352        matches!(self.state, AppState::Results)
353    }
354
355    /// Check if currently in Command mode
356    #[must_use]
357    pub fn is_in_command_mode(&self) -> bool {
358        matches!(self.state, AppState::Command)
359    }
360
361    /// Check if currently in any Search mode
362    #[must_use]
363    pub fn is_in_search_mode(&self) -> bool {
364        matches!(self.state, AppState::Search { .. })
365    }
366
367    /// Check if currently in Help mode
368    #[must_use]
369    pub fn is_in_help_mode(&self) -> bool {
370        matches!(self.state, AppState::Help)
371    }
372
373    /// Check if currently in Debug mode
374    #[must_use]
375    pub fn is_in_debug_mode(&self) -> bool {
376        matches!(self.state, AppState::Debug)
377    }
378
379    /// Check if currently in History mode
380    #[must_use]
381    pub fn is_in_history_mode(&self) -> bool {
382        matches!(self.state, AppState::History)
383    }
384
385    /// Check if currently in `JumpToRow` mode
386    #[must_use]
387    pub fn is_in_jump_mode(&self) -> bool {
388        matches!(self.state, AppState::JumpToRow)
389    }
390
391    /// Check if currently in `ColumnStats` mode
392    #[must_use]
393    pub fn is_in_column_stats_mode(&self) -> bool {
394        matches!(self.state, AppState::ColumnStats)
395    }
396
397    /// Check if in column search specifically
398    #[must_use]
399    pub fn is_in_column_search(&self) -> bool {
400        matches!(
401            self.state,
402            AppState::Search {
403                search_type: SearchType::Column
404            }
405        )
406    }
407
408    /// Check if in data search specifically
409    #[must_use]
410    pub fn is_in_data_search(&self) -> bool {
411        matches!(
412            self.state,
413            AppState::Search {
414                search_type: SearchType::Data
415            }
416        )
417    }
418
419    /// Check if in fuzzy filter mode specifically
420    #[must_use]
421    pub fn is_in_fuzzy_filter(&self) -> bool {
422        matches!(
423            self.state,
424            AppState::Search {
425                search_type: SearchType::Fuzzy
426            }
427        )
428    }
429
430    /// Check if in vim search mode specifically
431    #[must_use]
432    pub fn is_in_vim_search(&self) -> bool {
433        matches!(
434            self.state,
435            AppState::Search {
436                search_type: SearchType::Vim
437            }
438        )
439    }
440
441    /// Get the previous state if any
442    #[must_use]
443    pub fn get_previous_state(&self) -> Option<&AppState> {
444        self.previous_state.as_ref()
445    }
446
447    /// Check if we can navigate (in Results mode)
448    #[must_use]
449    pub fn can_navigate(&self) -> bool {
450        self.is_in_results_mode()
451    }
452
453    /// Check if we can edit (in Command mode or search modes)
454    #[must_use]
455    pub fn can_edit(&self) -> bool {
456        self.is_in_command_mode() || self.is_in_search_mode()
457    }
458
459    /// Get transition count (useful for debugging)
460    #[must_use]
461    pub fn get_transition_count(&self) -> usize {
462        self.transition_count
463    }
464
465    /// Get the last transition if any
466    #[must_use]
467    pub fn get_last_transition(&self) -> Option<&StateTransition> {
468        self.history.back()
469    }
470
471    // Helper methods
472
473    fn mode_to_state(&self, mode: AppMode) -> AppState {
474        match mode {
475            AppMode::Command => AppState::Command,
476            AppMode::Results => AppState::Results,
477            AppMode::Search | AppMode::ColumnSearch => {
478                // Try to preserve search type if we're already in search
479                if let AppState::Search { ref search_type } = self.state {
480                    AppState::Search {
481                        search_type: search_type.clone(),
482                    }
483                } else {
484                    // Guess based on mode
485                    let search_type = match mode {
486                        AppMode::ColumnSearch => SearchType::Column,
487                        _ => SearchType::Data,
488                    };
489                    AppState::Search { search_type }
490                }
491            }
492            AppMode::Help => AppState::Help,
493            AppMode::Debug | AppMode::PrettyQuery => AppState::Debug,
494            AppMode::History => AppState::History,
495            AppMode::JumpToRow => AppState::JumpToRow,
496            AppMode::ColumnStats => AppState::ColumnStats,
497            _ => self.state.clone(), // Preserve current for unknown modes
498        }
499    }
500
501    fn state_display(&self, state: &AppState) -> String {
502        match state {
503            AppState::Command => "COMMAND".to_string(),
504            AppState::Results => "RESULTS".to_string(),
505            AppState::Search { search_type } => format!("SEARCH({search_type:?})"),
506            AppState::Help => "HELP".to_string(),
507            AppState::Debug => "DEBUG".to_string(),
508            AppState::History => "HISTORY".to_string(),
509            AppState::JumpToRow => "JUMP_TO_ROW".to_string(),
510            AppState::ColumnStats => "COLUMN_STATS".to_string(),
511        }
512    }
513
514    fn log_expected_side_effects(&self) {
515        match (&self.previous_state, &self.state) {
516            (Some(AppState::Command), AppState::Results) => {
517                debug!(target: "shadow_state", 
518                    "Expected side effects: Clear searches, reset viewport, enable nav keys");
519            }
520            (Some(AppState::Results), AppState::Search { .. }) => {
521                debug!(target: "shadow_state",
522                    "Expected side effects: Clear other searches, setup search UI");
523            }
524            (Some(AppState::Search { .. }), AppState::Results) => {
525                debug!(target: "shadow_state",
526                    "Expected side effects: Clear search UI, restore nav keys");
527            }
528            _ => {}
529        }
530    }
531}
532
533impl Default for ShadowStateManager {
534    fn default() -> Self {
535        Self::new()
536    }
537}