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            self.transition_count += 1;
88
89            info!(target: "shadow_state",
90                "[#{}] {} -> {} (trigger: {})",
91                self.transition_count,
92                self.state_display(&self.state),
93                self.state_display(&new_state),
94                trigger
95            );
96
97            // Record transition
98            let transition = StateTransition {
99                timestamp: Instant::now(),
100                from: self.state.clone(),
101                to: new_state.clone(),
102                trigger: trigger.to_string(),
103            };
104
105            self.history.push_back(transition);
106            if self.history.len() > 100 {
107                self.history.pop_front();
108            }
109
110            // Update state
111            self.previous_state = Some(self.state.clone());
112            self.state = new_state;
113
114            // Log what side effects should happen
115            self.log_expected_side_effects();
116        } else {
117            debug!(target: "shadow_state", 
118                "Redundant mode change to {:?} ignored", mode);
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            self.transition_count += 1;
133
134            info!(target: "shadow_state",
135                "[#{}] {} -> {} (trigger: {})",
136                self.transition_count,
137                self.state_display(&self.state),
138                self.state_display(&new_state),
139                trigger
140            );
141
142            // Record transition
143            let transition = StateTransition {
144                timestamp: Instant::now(),
145                from: self.state.clone(),
146                to: new_state.clone(),
147                trigger: trigger.to_string(),
148            };
149
150            self.history.push_back(transition);
151            if self.history.len() > 100 {
152                self.history.pop_front();
153            }
154
155            // Update shadow state
156            self.previous_state = Some(self.state.clone());
157            self.state = new_state;
158
159            // Update buffer (temporary - will be removed)
160            buffer.set_mode(mode);
161
162            // Log what side effects should happen
163            self.log_expected_side_effects();
164        } else {
165            debug!(target: "shadow_state", 
166                "Redundant mode change to {:?} ignored", mode);
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    pub fn is_search_active(&self) -> bool {
260        matches!(self.state, AppState::Search { .. })
261    }
262
263    /// Get current search type if active
264    pub fn get_search_type(&self) -> Option<SearchType> {
265        if let AppState::Search { ref search_type } = self.state {
266            Some(search_type.clone())
267        } else {
268            None
269        }
270    }
271
272    /// Get display string for status line
273    pub fn status_display(&self) -> String {
274        format!("[Shadow: {}]", self.state_display(&self.state))
275    }
276
277    /// Get debug info about recent transitions
278    pub fn debug_info(&self) -> String {
279        let mut info = format!(
280            "Shadow State Debug (transitions: {})\n",
281            self.transition_count
282        );
283        info.push_str(&format!("Current: {:?}\n", self.state));
284
285        if !self.history.is_empty() {
286            info.push_str("\nRecent transitions:\n");
287            for transition in self.history.iter().rev().take(5) {
288                info.push_str(&format!(
289                    "  {:?} ago: {} -> {} ({})\n",
290                    transition.timestamp.elapsed(),
291                    self.state_display(&transition.from),
292                    self.state_display(&transition.to),
293                    transition.trigger
294                ));
295            }
296        }
297
298        if !self.discrepancies.is_empty() {
299            info.push_str("\n⚠️  Discrepancies detected:\n");
300            for disc in self.discrepancies.iter().rev().take(3) {
301                info.push_str(&format!("  - {}\n", disc));
302            }
303        }
304
305        info
306    }
307
308    /// Report a discrepancy between shadow and actual state
309    pub fn report_discrepancy(&mut self, expected: &str, actual: &str) {
310        let msg = format!("Expected: {}, Actual: {}", expected, actual);
311        warn!(target: "shadow_state", "Discrepancy: {}", msg);
312        self.discrepancies.push(msg);
313    }
314
315    // ============= Comprehensive Read Methods =============
316    // These methods make shadow state easy to query and will eventually
317    // replace all buffer().get_mode() calls
318
319    /// Get the current state
320    pub fn get_state(&self) -> &AppState {
321        &self.state
322    }
323
324    /// Get the current mode (converts state to AppMode for compatibility)
325    pub fn get_mode(&self) -> AppMode {
326        match &self.state {
327            AppState::Command => AppMode::Command,
328            AppState::Results => AppMode::Results,
329            AppState::Search { search_type } => match search_type {
330                SearchType::Column => AppMode::ColumnSearch,
331                SearchType::Data => AppMode::Search,
332                SearchType::Fuzzy => AppMode::FuzzyFilter,
333                SearchType::Vim => AppMode::Search, // Vim search uses Search mode
334            },
335            AppState::Help => AppMode::Help,
336            AppState::Debug => AppMode::Debug,
337            AppState::History => AppMode::History,
338            AppState::JumpToRow => AppMode::JumpToRow,
339            AppState::ColumnStats => AppMode::ColumnStats,
340        }
341    }
342
343    /// Check if currently in Results mode
344    pub fn is_in_results_mode(&self) -> bool {
345        matches!(self.state, AppState::Results)
346    }
347
348    /// Check if currently in Command mode
349    pub fn is_in_command_mode(&self) -> bool {
350        matches!(self.state, AppState::Command)
351    }
352
353    /// Check if currently in any Search mode
354    pub fn is_in_search_mode(&self) -> bool {
355        matches!(self.state, AppState::Search { .. })
356    }
357
358    /// Check if currently in Help mode
359    pub fn is_in_help_mode(&self) -> bool {
360        matches!(self.state, AppState::Help)
361    }
362
363    /// Check if currently in Debug mode
364    pub fn is_in_debug_mode(&self) -> bool {
365        matches!(self.state, AppState::Debug)
366    }
367
368    /// Check if currently in History mode
369    pub fn is_in_history_mode(&self) -> bool {
370        matches!(self.state, AppState::History)
371    }
372
373    /// Check if currently in JumpToRow mode
374    pub fn is_in_jump_mode(&self) -> bool {
375        matches!(self.state, AppState::JumpToRow)
376    }
377
378    /// Check if currently in ColumnStats mode
379    pub fn is_in_column_stats_mode(&self) -> bool {
380        matches!(self.state, AppState::ColumnStats)
381    }
382
383    /// Check if in column search specifically
384    pub fn is_in_column_search(&self) -> bool {
385        matches!(
386            self.state,
387            AppState::Search {
388                search_type: SearchType::Column
389            }
390        )
391    }
392
393    /// Check if in data search specifically
394    pub fn is_in_data_search(&self) -> bool {
395        matches!(
396            self.state,
397            AppState::Search {
398                search_type: SearchType::Data
399            }
400        )
401    }
402
403    /// Check if in fuzzy filter mode specifically
404    pub fn is_in_fuzzy_filter(&self) -> bool {
405        matches!(
406            self.state,
407            AppState::Search {
408                search_type: SearchType::Fuzzy
409            }
410        )
411    }
412
413    /// Check if in vim search mode specifically
414    pub fn is_in_vim_search(&self) -> bool {
415        matches!(
416            self.state,
417            AppState::Search {
418                search_type: SearchType::Vim
419            }
420        )
421    }
422
423    /// Get the previous state if any
424    pub fn get_previous_state(&self) -> Option<&AppState> {
425        self.previous_state.as_ref()
426    }
427
428    /// Check if we can navigate (in Results mode)
429    pub fn can_navigate(&self) -> bool {
430        self.is_in_results_mode()
431    }
432
433    /// Check if we can edit (in Command mode or search modes)
434    pub fn can_edit(&self) -> bool {
435        self.is_in_command_mode() || self.is_in_search_mode()
436    }
437
438    /// Get transition count (useful for debugging)
439    pub fn get_transition_count(&self) -> usize {
440        self.transition_count
441    }
442
443    /// Get the last transition if any
444    pub fn get_last_transition(&self) -> Option<&StateTransition> {
445        self.history.back()
446    }
447
448    // Helper methods
449
450    fn mode_to_state(&self, mode: AppMode) -> AppState {
451        match mode {
452            AppMode::Command => AppState::Command,
453            AppMode::Results => AppState::Results,
454            AppMode::Search | AppMode::ColumnSearch => {
455                // Try to preserve search type if we're already in search
456                if let AppState::Search { ref search_type } = self.state {
457                    AppState::Search {
458                        search_type: search_type.clone(),
459                    }
460                } else {
461                    // Guess based on mode
462                    let search_type = match mode {
463                        AppMode::ColumnSearch => SearchType::Column,
464                        _ => SearchType::Data,
465                    };
466                    AppState::Search { search_type }
467                }
468            }
469            AppMode::Help => AppState::Help,
470            AppMode::Debug | AppMode::PrettyQuery => AppState::Debug,
471            AppMode::History => AppState::History,
472            AppMode::JumpToRow => AppState::JumpToRow,
473            AppMode::ColumnStats => AppState::ColumnStats,
474            _ => self.state.clone(), // Preserve current for unknown modes
475        }
476    }
477
478    fn state_display(&self, state: &AppState) -> String {
479        match state {
480            AppState::Command => "COMMAND".to_string(),
481            AppState::Results => "RESULTS".to_string(),
482            AppState::Search { search_type } => format!("SEARCH({:?})", search_type),
483            AppState::Help => "HELP".to_string(),
484            AppState::Debug => "DEBUG".to_string(),
485            AppState::History => "HISTORY".to_string(),
486            AppState::JumpToRow => "JUMP_TO_ROW".to_string(),
487            AppState::ColumnStats => "COLUMN_STATS".to_string(),
488        }
489    }
490
491    fn log_expected_side_effects(&self) {
492        match (&self.previous_state, &self.state) {
493            (Some(AppState::Command), AppState::Results) => {
494                debug!(target: "shadow_state", 
495                    "Expected side effects: Clear searches, reset viewport, enable nav keys");
496            }
497            (Some(AppState::Results), AppState::Search { .. }) => {
498                debug!(target: "shadow_state",
499                    "Expected side effects: Clear other searches, setup search UI");
500            }
501            (Some(AppState::Search { .. }), AppState::Results) => {
502                debug!(target: "shadow_state",
503                    "Expected side effects: Clear search UI, restore nav keys");
504            }
505            _ => {}
506        }
507    }
508}
509
510impl Default for ShadowStateManager {
511    fn default() -> Self {
512        Self::new()
513    }
514}