Skip to main content

mq_edit/
app.rs

1use std::path::{Path, PathBuf};
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use lsp_types::CompletionItem;
5use markdown_lsp::{DiagnosticsManager, LspEvent, LspManager};
6use miette::Result;
7
8use crate::config::Config;
9use crate::document::{CursorMovement, DocumentBuffer, FileType};
10use crate::navigation::{FileLocation, NavigationHistory};
11use crate::renderer::{CodeRenderer, ImageManager};
12use crate::ui::{FileTree, SearchField, SearchMode};
13
14/// Main application state
15pub struct App {
16    /// Document buffer
17    buffer: DocumentBuffer,
18    /// Whether the app should quit
19    should_quit: bool,
20    /// Scroll offset for viewport
21    scroll_offset: usize,
22    /// Status message (for errors, info, etc.)
23    status_message: Option<String>,
24    /// File browser tree
25    file_tree: Option<FileTree>,
26    /// Whether file browser is visible
27    show_file_browser: bool,
28    /// Current working directory
29    current_dir: PathBuf,
30    /// Application configuration
31    config: Config,
32    /// Image manager for rendering images
33    image_manager: ImageManager,
34    /// Code renderer for syntax highlighting
35    code_renderer: CodeRenderer,
36    /// LSP manager for language server protocol support
37    lsp_manager: Option<LspManager>,
38    /// Diagnostics manager for LSP diagnostics
39    diagnostics_manager: DiagnosticsManager,
40    /// Document version for LSP synchronization
41    document_version: i32,
42    /// Navigation history for jump operations
43    navigation_history: NavigationHistory,
44    /// Pending definition request location (for adding to history)
45    pending_definition_request: Option<(PathBuf, usize, usize)>,
46    /// Completion items from LSP (original unfiltered list)
47    completion_items: Vec<CompletionItem>,
48    /// Filtered completion items based on user input
49    filtered_completion_items: Vec<CompletionItem>,
50    /// Selected completion item index
51    completion_selected: usize,
52    /// Whether completion popup is visible
53    show_completion: bool,
54    /// Column position where completion started
55    completion_start_column: usize,
56    /// Whether quit confirmation is pending (user pressed quit with unsaved changes)
57    quit_confirm_pending: bool,
58    /// Whether quit confirmation dialog is visible
59    show_quit_dialog: bool,
60    /// Whether line numbers are visible
61    show_line_numbers: bool,
62    /// Whether current line highlight is visible
63    show_current_line_highlight: bool,
64    /// Whether search dialog is visible
65    show_search_dialog: bool,
66    /// Search query
67    search_query: String,
68    /// Replace query
69    replace_query: String,
70    /// Search mode (Find or Replace)
71    search_mode: SearchMode,
72    /// Current search results (line, column)
73    search_results: Vec<(usize, usize)>,
74    /// Current search result index
75    search_index: Option<usize>,
76    /// Active search field
77    search_active_field: SearchField,
78    /// Whether save-as dialog is visible
79    show_save_as_dialog: bool,
80    /// Filename input for save-as dialog
81    save_as_filename: String,
82    /// Whether goto line dialog is visible
83    show_goto_line_dialog: bool,
84    /// Line number input for goto line dialog
85    goto_line_input: String,
86    /// Whether mq query dialog is visible
87    show_mq_query_dialog: bool,
88    /// mq query input string
89    mq_query_input: String,
90    /// mq query result (output or error message)
91    mq_query_result: Option<String>,
92    /// Whether the app is running in pipe mode (stdin/stdout piped)
93    pipe_mode: bool,
94}
95
96impl App {
97    /// Create a new app with an empty buffer
98    pub fn new() -> Self {
99        let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
100        let config = Config::load_or_default();
101        let mut image_manager = ImageManager::new();
102        image_manager.set_base_path(current_dir.clone());
103
104        // Create LSP manager
105        let lsp_manager = Some(LspManager::new(
106            config.lsp_server_configs(),
107            current_dir.clone(),
108        ));
109
110        let show_line_numbers = config.editor.show_line_numbers;
111        let show_current_line_highlight = config.editor.show_current_line_highlight;
112        let mut code_renderer = CodeRenderer::with_theme(&config.editor.theme);
113        code_renderer.set_use_semantic_tokens(config.editor.use_semantic_tokens);
114
115        Self {
116            buffer: DocumentBuffer::new(),
117            should_quit: false,
118            scroll_offset: 0,
119            status_message: None,
120            file_tree: None,
121            show_file_browser: false,
122            current_dir,
123            config,
124            image_manager,
125            code_renderer,
126            lsp_manager,
127            diagnostics_manager: DiagnosticsManager::new(),
128            document_version: 0,
129            navigation_history: NavigationHistory::new(),
130            pending_definition_request: None,
131            completion_items: Vec::new(),
132            filtered_completion_items: Vec::new(),
133            completion_selected: 0,
134            show_completion: false,
135            completion_start_column: 0,
136            quit_confirm_pending: false,
137            show_quit_dialog: false,
138            show_line_numbers,
139            show_current_line_highlight,
140            show_search_dialog: false,
141            search_query: String::new(),
142            replace_query: String::new(),
143            search_mode: SearchMode::Find,
144            search_results: Vec::new(),
145            search_index: None,
146            search_active_field: SearchField::Search,
147            show_save_as_dialog: false,
148            save_as_filename: String::new(),
149            show_goto_line_dialog: false,
150            goto_line_input: String::new(),
151            show_mq_query_dialog: false,
152            mq_query_input: String::new(),
153            mq_query_result: None,
154            pipe_mode: false,
155        }
156    }
157
158    /// Create app from a file
159    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
160        let path = path.as_ref();
161        let buffer = DocumentBuffer::from_file(path)?;
162        let current_dir = path
163            .parent()
164            .map(|p| p.to_path_buf())
165            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
166        let config = Config::load_or_default();
167        let mut image_manager = ImageManager::new();
168        image_manager.set_base_path(path.to_path_buf());
169
170        // Create LSP manager
171        let mut lsp_manager = Some(LspManager::new(
172            config.lsp_server_configs(),
173            current_dir.clone(),
174        ));
175
176        // Notify LSP that a document was opened
177        let diagnostics_manager = DiagnosticsManager::new();
178        let language_id = file_type_to_language_id(buffer.file_type());
179        if let Some(ref mut lsp) = lsp_manager
180            && let Some(lang_id) = &language_id
181            && lsp.is_enabled(lang_id)
182        {
183            let content = buffer.content();
184            if let Err(e) = lsp.did_open(lang_id, path, &content) {
185                eprintln!("LSP did_open error: {}", e);
186            }
187        }
188
189        let show_line_numbers = config.editor.show_line_numbers;
190        let show_current_line_highlight = config.editor.show_current_line_highlight;
191        let mut code_renderer = CodeRenderer::with_theme(&config.editor.theme);
192        code_renderer.set_use_semantic_tokens(config.editor.use_semantic_tokens);
193
194        Ok(Self {
195            buffer,
196            should_quit: false,
197            scroll_offset: 0,
198            status_message: None,
199            file_tree: None,
200            show_file_browser: false,
201            current_dir,
202            config,
203            image_manager,
204            code_renderer,
205            lsp_manager,
206            diagnostics_manager,
207            document_version: 1,
208            navigation_history: NavigationHistory::new(),
209            pending_definition_request: None,
210            completion_items: Vec::new(),
211            filtered_completion_items: Vec::new(),
212            completion_selected: 0,
213            show_completion: false,
214            completion_start_column: 0,
215            quit_confirm_pending: false,
216            show_quit_dialog: false,
217            show_line_numbers,
218            show_current_line_highlight,
219            show_search_dialog: false,
220            search_query: String::new(),
221            replace_query: String::new(),
222            search_mode: SearchMode::Find,
223            search_results: Vec::new(),
224            search_index: None,
225            search_active_field: SearchField::Search,
226            show_save_as_dialog: false,
227            save_as_filename: String::new(),
228            show_goto_line_dialog: false,
229            goto_line_input: String::new(),
230            show_mq_query_dialog: false,
231            mq_query_input: String::new(),
232            mq_query_result: None,
233            pipe_mode: false,
234        })
235    }
236
237    /// Create app from a string content (for pipe mode)
238    pub fn from_string(content: &str) -> Result<Self> {
239        let buffer = DocumentBuffer::from_string(content)?;
240        let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
241        let config = Config::load_or_default();
242        let mut image_manager = ImageManager::new();
243        image_manager.set_base_path(current_dir.clone());
244
245        let lsp_manager = Some(LspManager::new(
246            config.lsp_server_configs(),
247            current_dir.clone(),
248        ));
249
250        let show_line_numbers = config.editor.show_line_numbers;
251        let show_current_line_highlight = config.editor.show_current_line_highlight;
252        let mut code_renderer = CodeRenderer::with_theme(&config.editor.theme);
253        code_renderer.set_use_semantic_tokens(config.editor.use_semantic_tokens);
254
255        Ok(Self {
256            buffer,
257            should_quit: false,
258            scroll_offset: 0,
259            status_message: None,
260            file_tree: None,
261            show_file_browser: false,
262            current_dir,
263            config,
264            image_manager,
265            code_renderer,
266            lsp_manager,
267            diagnostics_manager: DiagnosticsManager::new(),
268            document_version: 0,
269            navigation_history: NavigationHistory::new(),
270            pending_definition_request: None,
271            completion_items: Vec::new(),
272            filtered_completion_items: Vec::new(),
273            completion_selected: 0,
274            show_completion: false,
275            completion_start_column: 0,
276            quit_confirm_pending: false,
277            show_quit_dialog: false,
278            show_line_numbers,
279            show_current_line_highlight,
280            show_search_dialog: false,
281            search_query: String::new(),
282            replace_query: String::new(),
283            search_mode: SearchMode::Find,
284            search_results: Vec::new(),
285            search_index: None,
286            search_active_field: SearchField::Search,
287            show_save_as_dialog: false,
288            save_as_filename: String::new(),
289            show_goto_line_dialog: false,
290            goto_line_input: String::new(),
291            show_mq_query_dialog: false,
292            mq_query_input: String::new(),
293            mq_query_result: None,
294            pipe_mode: false,
295        })
296    }
297
298    /// Get the document buffer
299    pub fn buffer(&self) -> &DocumentBuffer {
300        &self.buffer
301    }
302
303    /// Get mutable document buffer
304    pub fn buffer_mut(&mut self) -> &mut DocumentBuffer {
305        &mut self.buffer
306    }
307
308    /// Set pipe mode (stdin/stdout piped)
309    pub fn set_pipe_mode(&mut self, pipe_mode: bool) {
310        self.pipe_mode = pipe_mode;
311    }
312
313    /// Check if the app should quit
314    pub fn should_quit(&self) -> bool {
315        self.should_quit
316    }
317
318    /// Get scroll offset
319    pub fn scroll_offset(&self) -> usize {
320        self.scroll_offset
321    }
322
323    /// Get image manager
324    pub fn image_manager(&self) -> &ImageManager {
325        &self.image_manager
326    }
327
328    /// Get mutable image manager
329    pub fn image_manager_mut(&mut self) -> &mut ImageManager {
330        &mut self.image_manager
331    }
332
333    /// Get diagnostics manager
334    pub fn diagnostics_manager(&self) -> &DiagnosticsManager {
335        &self.diagnostics_manager
336    }
337
338    /// Get code renderer
339    pub fn code_renderer(&self) -> &CodeRenderer {
340        &self.code_renderer
341    }
342
343    /// Get mutable code renderer
344    pub fn code_renderer_mut(&mut self) -> &mut CodeRenderer {
345        &mut self.code_renderer
346    }
347
348    /// Get status message
349    pub fn status_message(&self) -> Option<&str> {
350        self.status_message.as_deref()
351    }
352
353    /// Set status message
354    pub fn set_status_message(&mut self, message: String) {
355        self.status_message = Some(message);
356    }
357
358    /// Clear status message
359    pub fn clear_status_message(&mut self) {
360        self.status_message = None;
361    }
362
363    /// Get completion items (filtered by current input)
364    pub fn filtered_completion_items(&self) -> &[CompletionItem] {
365        &self.filtered_completion_items
366    }
367
368    /// Get selected completion index
369    pub fn completion_selected(&self) -> usize {
370        self.completion_selected
371    }
372
373    /// Check if completion popup is visible
374    pub fn show_completion(&self) -> bool {
375        self.show_completion
376    }
377
378    /// Get the current completion prefix (text typed since completion started)
379    fn get_completion_prefix(&self) -> String {
380        let cursor = self.buffer.cursor();
381        let line = self.buffer.line(cursor.line).unwrap_or_default();
382        if cursor.column > self.completion_start_column {
383            line.chars()
384                .skip(self.completion_start_column)
385                .take(cursor.column - self.completion_start_column)
386                .collect()
387        } else {
388            String::new()
389        }
390    }
391
392    /// Filter completion items based on user input
393    fn filter_completion_items(&mut self) {
394        let prefix = self.get_completion_prefix().to_lowercase();
395        self.filtered_completion_items = self
396            .completion_items
397            .iter()
398            .filter(|item| {
399                if prefix.is_empty() {
400                    true
401                } else {
402                    item.label.to_lowercase().contains(&prefix)
403                }
404            })
405            .cloned()
406            .collect();
407
408        // Reset selection if it's out of bounds
409        if self.filtered_completion_items.is_empty()
410            || self.completion_selected >= self.filtered_completion_items.len()
411        {
412            self.completion_selected = 0;
413        }
414
415        // Hide completion if no items match
416        if self.filtered_completion_items.is_empty() && !prefix.is_empty() {
417            self.show_completion = false;
418        }
419    }
420
421    /// Get file tree
422    pub fn file_tree(&self) -> Option<&FileTree> {
423        self.file_tree.as_ref()
424    }
425
426    /// Get mutable file tree
427    pub fn file_tree_mut(&mut self) -> Option<&mut FileTree> {
428        self.file_tree.as_mut()
429    }
430
431    /// Check if file browser is visible
432    pub fn is_file_browser_visible(&self) -> bool {
433        self.show_file_browser
434    }
435
436    /// Toggle file browser visibility
437    pub fn toggle_file_browser(&mut self) {
438        self.show_file_browser = !self.show_file_browser;
439
440        // Initialize file tree if it doesn't exist
441        if self.show_file_browser && self.file_tree.is_none() {
442            self.file_tree = Some(FileTree::new(&self.current_dir));
443        }
444    }
445
446    /// Check if line numbers are visible
447    pub fn show_line_numbers(&self) -> bool {
448        self.show_line_numbers
449    }
450
451    /// Toggle line numbers visibility
452    pub fn toggle_line_numbers(&mut self) {
453        self.show_line_numbers = !self.show_line_numbers;
454    }
455
456    /// Check if current line highlight is visible
457    pub fn show_current_line_highlight(&self) -> bool {
458        self.show_current_line_highlight
459    }
460
461    /// Toggle current line highlight visibility
462    pub fn toggle_current_line_highlight(&mut self) {
463        self.show_current_line_highlight = !self.show_current_line_highlight;
464    }
465
466    /// Calculate the width of line number gutter (including separator)
467    pub fn line_number_gutter_width(&self) -> u16 {
468        if self.show_line_numbers {
469            let total_lines = self.buffer.line_count();
470            let digits = if total_lines == 0 {
471                1
472            } else {
473                ((total_lines as f64).log10().floor() as usize) + 1
474            }
475            .max(3);
476            // digits + " │ " (3 characters: space + vertical bar + space)
477            (digits + 3) as u16
478        } else {
479            0
480        }
481    }
482
483    /// Open file from path
484    pub fn open_file(&mut self, path: impl AsRef<Path>) -> Result<()> {
485        let path = path.as_ref();
486        self.buffer = DocumentBuffer::from_file(path)?;
487
488        if let Some(parent) = path.parent() {
489            self.current_dir = parent.to_path_buf();
490        }
491
492        self.scroll_offset = 0;
493        self.document_version = 1;
494
495        // Notify LSP that a document was opened
496        let language_id = file_type_to_language_id(self.buffer.file_type());
497        if let Some(ref mut lsp) = self.lsp_manager
498            && let Some(lang_id) = &language_id
499            && lsp.is_enabled(lang_id)
500        {
501            let content = self.buffer.content();
502            if let Err(e) = lsp.did_open(lang_id, path, &content) {
503                eprintln!("LSP did_open error: {}", e);
504            }
505        }
506
507        Ok(())
508    }
509
510    /// Poll LSP events and update diagnostics
511    pub fn poll_lsp_events(&mut self) {
512        // Collect events first (separate the borrow from processing)
513        let events = if let Some(ref mut lsp) = self.lsp_manager {
514            lsp.poll_events()
515        } else {
516            Vec::new()
517        };
518
519        // Process events without holding the lsp_manager borrow
520        for (_language_id, event) in events {
521            match event {
522                LspEvent::Diagnostics(params) => {
523                    // Convert LSP diagnostics to our format and update the manager
524                    self.diagnostics_manager.update(params.diagnostics);
525                }
526                LspEvent::SemanticTokens(_uri, tokens) => {
527                    // Semantic tokens received - decode and store in CodeRenderer
528                    let content = self.buffer.content();
529                    let decoded_tokens =
530                        crate::renderer::code::decode_semantic_tokens(&tokens, &content);
531                    self.code_renderer.set_semantic_tokens(decoded_tokens);
532                }
533                LspEvent::Completion(completion) => {
534                    // Completion items received - display popup
535                    use lsp_types::CompletionResponse;
536
537                    let items = match completion {
538                        CompletionResponse::Array(items) => items,
539                        CompletionResponse::List(list) => list.items,
540                    };
541
542                    if !items.is_empty() {
543                        self.completion_items = items;
544                        // Note: completion_start_column is set in request_completion()
545                        self.completion_selected = 0;
546                        self.show_completion = true;
547                        self.filter_completion_items();
548                    }
549                }
550                LspEvent::Definition(response) => {
551                    // Definition response received
552                    // Save current location to history if we had a pending request
553                    if let Some((file_path, line, column)) = self.pending_definition_request.take()
554                    {
555                        self.navigation_history
556                            .push(FileLocation::new(file_path, line, column));
557                    }
558
559                    // Jump to the definition location
560                    if let Err(e) = self.jump_to_definition(response) {
561                        self.set_status_message(format!("Failed to jump to definition: {}", e));
562                    }
563                }
564                LspEvent::References(_locations) => {
565                    // References response received
566                    // TODO: Display references list
567                }
568                LspEvent::Initialized(trigger_chars) => {
569                    // Server initialized - save trigger characters and request semantic tokens
570                    if let Some(ref mut lsp) = self.lsp_manager {
571                        // Save trigger characters for this language
572                        if let FileType::Code(lang_id) = self.buffer.file_type() {
573                            lsp.set_trigger_characters(lang_id, trigger_chars);
574                        } else if let FileType::Markdown = self.buffer.file_type() {
575                            lsp.set_trigger_characters("markdown", trigger_chars);
576                        }
577
578                        let language_id = file_type_to_language_id(self.buffer.file_type());
579                        if let Some(file_path) = self.buffer.file_path()
580                            && let Some(lang_id) = &language_id
581                            && lsp.is_enabled(lang_id)
582                        {
583                            let path_buf = file_path.to_path_buf();
584                            if let Err(e) = lsp.request_semantic_tokens(lang_id, &path_buf) {
585                                eprintln!("Failed to request semantic tokens: {}", e);
586                            }
587                        }
588                    }
589                }
590                LspEvent::Error(err) => {
591                    eprintln!("LSP error: {}", err);
592                }
593            }
594        }
595    }
596
597    /// Notify LSP that the document has changed
598    fn notify_lsp_document_change(&mut self) {
599        let language_id = file_type_to_language_id(self.buffer.file_type());
600        if let Some(ref mut lsp) = self.lsp_manager
601            && let Some(file_path) = self.buffer.file_path()
602            && let Some(lang_id) = &language_id
603            && lsp.is_enabled(lang_id)
604        {
605            self.document_version += 1;
606            let content = self.buffer.content();
607            let path_buf = file_path.to_path_buf();
608            if let Err(e) = lsp.did_change(lang_id, &path_buf, self.document_version, &content) {
609                eprintln!("LSP did_change error: {}", e);
610            }
611
612            // Request new semantic tokens after document change
613            if let Err(e) = lsp.request_semantic_tokens(lang_id, &path_buf) {
614                eprintln!("Failed to request semantic tokens: {}", e);
615            }
616        }
617    }
618
619    /// Request go to definition at current cursor position
620    pub fn request_go_to_definition(&mut self) -> Result<()> {
621        let language_id = file_type_to_language_id(self.buffer.file_type());
622        if let Some(ref mut lsp) = self.lsp_manager {
623            if let Some(file_path) = self.buffer.file_path() {
624                if let Some(lang_id) = &language_id
625                    && lsp.is_enabled(lang_id)
626                {
627                    let cursor = self.buffer.cursor();
628                    let path_buf = file_path.to_path_buf();
629
630                    // Store current location for history
631                    self.pending_definition_request =
632                        Some((path_buf.clone(), cursor.line, cursor.column));
633
634                    lsp.request_definition(
635                        lang_id,
636                        &path_buf,
637                        cursor.line as u32,
638                        cursor.column as u32,
639                    )
640                    .map_err(|e| miette::miette!("Failed to request definition: {}", e))?;
641
642                    self.set_status_message("Requesting definition...".to_string());
643                } else {
644                    self.set_status_message("LSP not available for this file type".to_string());
645                }
646            } else {
647                self.set_status_message("No file open".to_string());
648            }
649        } else {
650            self.set_status_message("LSP not initialized".to_string());
651        }
652        Ok(())
653    }
654
655    /// Jump to a definition response location
656    fn jump_to_definition(&mut self, response: lsp_types::GotoDefinitionResponse) -> Result<()> {
657        use lsp_types::GotoDefinitionResponse;
658
659        // Extract the first location from the response
660        let location = match response {
661            GotoDefinitionResponse::Scalar(loc) => Some(loc),
662            GotoDefinitionResponse::Array(locs) => locs.into_iter().next(),
663            GotoDefinitionResponse::Link(links) => {
664                // Convert LocationLink to Location
665                links.into_iter().next().map(|link| lsp_types::Location {
666                    uri: link.target_uri,
667                    range: link.target_selection_range,
668                })
669            }
670        };
671
672        if let Some(loc) = location {
673            // Convert URI to file path - strip file:// prefix if present
674            let path_str = loc.uri.path().as_str();
675            let path = PathBuf::from(path_str);
676
677            // Jump to the location
678            self.jump_to_location(
679                &path,
680                loc.range.start.line as usize,
681                loc.range.start.character as usize,
682            )?;
683            self.set_status_message(format!(
684                "Jumped to {}:{}:{}",
685                path.display(),
686                loc.range.start.line + 1,
687                loc.range.start.character + 1
688            ));
689        } else {
690            self.set_status_message("No definition found".to_string());
691        }
692
693        Ok(())
694    }
695
696    /// Jump to a specific location in a file
697    fn jump_to_location(&mut self, file_path: &Path, line: usize, column: usize) -> Result<()> {
698        // If it's a different file, open it
699        if self
700            .buffer
701            .file_path()
702            .map(|p| p != file_path)
703            .unwrap_or(true)
704        {
705            self.open_file(file_path)?;
706        }
707
708        // Move cursor to the position
709        let cursor = self.buffer.cursor_mut();
710        cursor.line = line;
711        cursor.column = column;
712        self.adjust_scroll();
713
714        Ok(())
715    }
716
717    /// Navigate back in history
718    pub fn navigate_back(&mut self) -> Result<()> {
719        // Try to go back
720        if let Some(prev) = self.navigation_history.back() {
721            let prev = prev.clone(); // Clone to avoid borrow issues
722            self.jump_to_location(&prev.path, prev.line, prev.column)?;
723            self.set_status_message(format!(
724                "Back to {}:{}:{}",
725                prev.path.display(),
726                prev.line + 1,
727                prev.column + 1
728            ));
729        } else {
730            self.set_status_message("No previous location in history".to_string());
731        }
732
733        Ok(())
734    }
735
736    /// Navigate forward in history
737    pub fn navigate_forward(&mut self) -> Result<()> {
738        // Try to go forward
739        if let Some(next) = self.navigation_history.forward() {
740            let next = next.clone(); // Clone to avoid borrow issues
741            self.jump_to_location(&next.path, next.line, next.column)?;
742            self.set_status_message(format!(
743                "Forward to {}:{}:{}",
744                next.path.display(),
745                next.line + 1,
746                next.column + 1
747            ));
748        } else {
749            self.set_status_message("No next location in history".to_string());
750        }
751
752        Ok(())
753    }
754
755    /// Request code completion at current cursor position
756    pub fn request_completion(&mut self, trigger_character: Option<String>) -> Result<()> {
757        let language_id = file_type_to_language_id(self.buffer.file_type());
758        if let Some(ref mut lsp) = self.lsp_manager {
759            if let Some(file_path) = self.buffer.file_path() {
760                if let Some(lang_id) = &language_id
761                    && lsp.is_enabled(lang_id)
762                {
763                    let cursor = self.buffer.cursor();
764                    let path_buf = file_path.to_path_buf();
765
766                    // Set completion_start_column to the beginning of the current word
767                    // This ensures we replace the entire word being typed when applying completion
768                    self.completion_start_column =
769                        self.buffer.word_start_column(cursor.line, cursor.column);
770
771                    lsp.request_completion(
772                        lang_id,
773                        &path_buf,
774                        cursor.line as u32,
775                        cursor.column as u32,
776                        trigger_character,
777                    )
778                    .map_err(|e| miette::miette!("Failed to request completion: {}", e))?;
779                } else {
780                    self.set_status_message("LSP not available for this file type".to_string());
781                }
782            } else {
783                self.set_status_message("No file open".to_string());
784            }
785        } else {
786            self.set_status_message("LSP not initialized".to_string());
787        }
788        Ok(())
789    }
790
791    /// Apply the selected completion item
792    pub fn apply_completion(&mut self) -> Result<()> {
793        if self.show_completion && !self.filtered_completion_items.is_empty() {
794            let item = self.filtered_completion_items[self.completion_selected].clone();
795
796            // Determine the text to insert (prefer insert_text over label)
797            let insert_text = item.insert_text.as_ref().unwrap_or(&item.label).clone();
798
799            // Delete the prefix that was typed since completion started (from completion_start_column to cursor)
800            self.buffer.delete_range(self.completion_start_column);
801
802            // Insert the completion text
803            self.buffer.insert_str(&insert_text);
804
805            // Close completion popup
806            self.show_completion = false;
807            self.completion_items.clear();
808            self.filtered_completion_items.clear();
809
810            // Notify LSP of document change
811            self.notify_lsp_document_change();
812        }
813        Ok(())
814    }
815
816    /// Move completion selection up
817    pub fn completion_up(&mut self) {
818        if self.show_completion && !self.filtered_completion_items.is_empty() {
819            if self.completion_selected > 0 {
820                self.completion_selected -= 1;
821            } else {
822                self.completion_selected = self.filtered_completion_items.len() - 1;
823            }
824        }
825    }
826
827    /// Move completion selection down
828    pub fn completion_down(&mut self) {
829        if self.show_completion && !self.filtered_completion_items.is_empty() {
830            if self.completion_selected < self.filtered_completion_items.len() - 1 {
831                self.completion_selected += 1;
832            } else {
833                self.completion_selected = 0;
834            }
835        }
836    }
837
838    /// Cancel completion popup
839    pub fn cancel_completion(&mut self) {
840        self.show_completion = false;
841        self.completion_items.clear();
842        self.filtered_completion_items.clear();
843        self.completion_selected = 0;
844    }
845
846    /// Check if quit dialog is visible
847    pub fn show_quit_dialog(&self) -> bool {
848        self.show_quit_dialog
849    }
850
851    /// Confirm quit (called when user confirms in dialog)
852    pub fn confirm_quit(&mut self) {
853        self.should_quit = true;
854        self.show_quit_dialog = false;
855    }
856
857    /// Cancel quit dialog
858    pub fn cancel_quit_dialog(&mut self) {
859        self.show_quit_dialog = false;
860        self.quit_confirm_pending = false;
861    }
862
863    /// Check if search dialog is visible
864    pub fn show_search_dialog(&self) -> bool {
865        self.show_search_dialog
866    }
867
868    /// Get search query
869    pub fn search_query(&self) -> &str {
870        &self.search_query
871    }
872
873    /// Get replace query
874    pub fn replace_query(&self) -> &str {
875        &self.replace_query
876    }
877
878    /// Get search mode
879    pub fn search_mode(&self) -> SearchMode {
880        self.search_mode
881    }
882
883    /// Get search results count
884    pub fn search_match_count(&self) -> usize {
885        self.search_results.len()
886    }
887
888    /// Get current search index
889    pub fn search_current_index(&self) -> Option<usize> {
890        self.search_index
891    }
892
893    /// Get active search field
894    pub fn search_active_field(&self) -> SearchField {
895        self.search_active_field
896    }
897
898    /// Check if save-as dialog is visible
899    pub fn show_save_as_dialog(&self) -> bool {
900        self.show_save_as_dialog
901    }
902
903    /// Get save-as filename
904    pub fn save_as_filename(&self) -> &str {
905        &self.save_as_filename
906    }
907
908    /// Check if goto line dialog is visible
909    pub fn show_goto_line_dialog(&self) -> bool {
910        self.show_goto_line_dialog
911    }
912
913    /// Get goto line input
914    pub fn goto_line_input(&self) -> &str {
915        &self.goto_line_input
916    }
917
918    /// Open goto line dialog
919    pub fn open_goto_line_dialog(&mut self) {
920        self.show_goto_line_dialog = true;
921        self.goto_line_input.clear();
922    }
923
924    /// Close goto line dialog
925    pub fn close_goto_line_dialog(&mut self) {
926        self.show_goto_line_dialog = false;
927        self.goto_line_input.clear();
928    }
929
930    /// Confirm goto line with the entered line number
931    pub fn confirm_goto_line(&mut self) -> Result<()> {
932        if self.goto_line_input.is_empty() {
933            return Err(miette::miette!("Line number cannot be empty"));
934        }
935
936        let line_number: usize = self
937            .goto_line_input
938            .trim()
939            .parse()
940            .map_err(|_| miette::miette!("Invalid line number"))?;
941
942        if line_number == 0 {
943            return Err(miette::miette!("Line number must be greater than 0"));
944        }
945
946        // Convert from 1-based to 0-based indexing
947        let target_line = line_number.saturating_sub(1);
948        let max_line = self.buffer.line_count().saturating_sub(1);
949
950        // Clamp to valid range
951        let target_line = target_line.min(max_line);
952
953        // Move cursor to the line
954        self.buffer.cursor_mut().line = target_line;
955        self.buffer.cursor_mut().column = 0;
956        self.adjust_scroll();
957
958        self.set_status_message(format!("Jumped to line {}", line_number));
959        self.close_goto_line_dialog();
960
961        Ok(())
962    }
963
964    /// Check if mq query dialog is visible
965    pub fn show_mq_query_dialog(&self) -> bool {
966        self.show_mq_query_dialog
967    }
968
969    /// Get mq query input
970    pub fn mq_query_input(&self) -> &str {
971        &self.mq_query_input
972    }
973
974    /// Get mq query result
975    pub fn mq_query_result(&self) -> Option<&str> {
976        self.mq_query_result.as_deref()
977    }
978
979    /// Open mq query dialog
980    pub fn open_mq_query_dialog(&mut self) {
981        self.show_mq_query_dialog = true;
982        self.mq_query_input.clear();
983        self.mq_query_result = None;
984    }
985
986    /// Close mq query dialog
987    pub fn close_mq_query_dialog(&mut self) {
988        self.show_mq_query_dialog = false;
989        self.mq_query_input.clear();
990        self.mq_query_result = None;
991    }
992
993    /// Execute mq query against the current buffer content
994    pub fn execute_mq_query(&mut self) {
995        if self.mq_query_input.is_empty() {
996            self.mq_query_result = Some("Error: Query is empty".to_string());
997            return;
998        }
999
1000        let content = self.buffer.content();
1001        let query = self.mq_query_input.clone();
1002
1003        let mut engine = mq_lang::DefaultEngine::default();
1004        engine.load_builtin_module();
1005
1006        let input = match mq_lang::parse_markdown_input(&content) {
1007            Ok(input) => input,
1008            Err(e) => {
1009                self.mq_query_result = Some(format!("Error: {}", e));
1010                return;
1011            }
1012        };
1013
1014        match engine.eval(&query, input.into_iter()) {
1015            Ok(results) => {
1016                let output = results
1017                    .into_iter()
1018                    .filter_map(|v| {
1019                        if v.is_empty() {
1020                            None
1021                        } else {
1022                            Some(v.to_string())
1023                        }
1024                    })
1025                    .collect::<Vec<_>>()
1026                    .join("\n");
1027                let start_line = self.buffer.cursor().line;
1028                let start_column = self.buffer.cursor().column;
1029                self.buffer.insert_str(&output);
1030                self.buffer.cursor_mut().line = start_line;
1031                self.buffer.cursor_mut().column = start_column;
1032                self.buffer.cursor_mut().update_desired_column();
1033                self.adjust_scroll();
1034                self.notify_lsp_document_change();
1035                self.set_status_message("mq query executed successfully.".to_string());
1036                self.close_mq_query_dialog();
1037            }
1038            Err(e) => {
1039                self.mq_query_result = Some(format!("Error: {}", e));
1040            }
1041        }
1042    }
1043
1044    /// Open save-as dialog
1045    pub fn open_save_as_dialog(&mut self) {
1046        self.show_save_as_dialog = true;
1047        // Pre-fill with suggested name if no file path
1048        if self.buffer.file_path().is_none() {
1049            self.save_as_filename = "untitled".to_string();
1050        } else if let Some(path) = self.buffer.file_path() {
1051            self.save_as_filename = path
1052                .file_name()
1053                .and_then(|n| n.to_str())
1054                .unwrap_or("untitled")
1055                .to_string();
1056        }
1057    }
1058
1059    /// Close save-as dialog
1060    pub fn close_save_as_dialog(&mut self) {
1061        self.show_save_as_dialog = false;
1062        self.save_as_filename.clear();
1063    }
1064
1065    /// Confirm save-as with the entered filename
1066    pub fn confirm_save_as(&mut self) -> Result<()> {
1067        if self.save_as_filename.is_empty() {
1068            return Err(miette::miette!("Filename cannot be empty"));
1069        }
1070
1071        // Construct the full path
1072        let path = if self.save_as_filename.contains('/') || self.save_as_filename.contains('\\') {
1073            // Absolute or relative path provided
1074            PathBuf::from(&self.save_as_filename)
1075        } else {
1076            // Just filename, save in current directory
1077            self.current_dir.join(&self.save_as_filename)
1078        };
1079
1080        // Save the file
1081        self.buffer.save_as(&path)?;
1082        self.set_status_message(format!("Saved as: {}", path.display()));
1083        self.close_save_as_dialog();
1084
1085        // Update file type based on new extension
1086        let file_type = FileType::from_path(&path);
1087        let language_id = file_type_to_language_id(&file_type);
1088        if let Some(lsp) = &mut self.lsp_manager
1089            && let Some(lang_id) = &language_id
1090            && lsp.is_enabled(lang_id)
1091        {
1092            // Notify LSP of the new file
1093            let content = self.buffer.content();
1094            if let Err(e) = lsp.did_open(lang_id, &path, &content) {
1095                eprintln!("LSP did_open error: {}", e);
1096            }
1097        }
1098
1099        Ok(())
1100    }
1101
1102    /// Open search dialog
1103    pub fn open_search(&mut self) {
1104        self.show_search_dialog = true;
1105        self.search_mode = SearchMode::Find;
1106        self.search_active_field = SearchField::Search;
1107    }
1108
1109    /// Open replace dialog
1110    pub fn open_replace(&mut self) {
1111        self.show_search_dialog = true;
1112        self.search_mode = SearchMode::Replace;
1113        self.search_active_field = SearchField::Search;
1114    }
1115
1116    /// Close search dialog
1117    pub fn close_search(&mut self) {
1118        self.show_search_dialog = false;
1119    }
1120
1121    /// Update search results
1122    fn update_search_results(&mut self) {
1123        self.search_results = self.buffer.find_all(&self.search_query);
1124        if self.search_results.is_empty() {
1125            self.search_index = None;
1126        } else if self.search_index.is_none()
1127            || self.search_index.unwrap() >= self.search_results.len()
1128        {
1129            self.search_index = Some(0);
1130        }
1131    }
1132
1133    /// Find next match
1134    pub fn search_next(&mut self) {
1135        if self.search_results.is_empty() {
1136            return;
1137        }
1138        if let Some(idx) = self.search_index {
1139            self.search_index = Some((idx + 1) % self.search_results.len());
1140        } else {
1141            self.search_index = Some(0);
1142        }
1143        self.jump_to_current_match();
1144    }
1145
1146    /// Find previous match
1147    pub fn search_prev(&mut self) {
1148        if self.search_results.is_empty() {
1149            return;
1150        }
1151        if let Some(idx) = self.search_index {
1152            if idx == 0 {
1153                self.search_index = Some(self.search_results.len() - 1);
1154            } else {
1155                self.search_index = Some(idx - 1);
1156            }
1157        } else {
1158            self.search_index = Some(self.search_results.len() - 1);
1159        }
1160        self.jump_to_current_match();
1161    }
1162
1163    /// Jump to current match
1164    fn jump_to_current_match(&mut self) {
1165        if let Some(idx) = self.search_index
1166            && let Some(&(line, column)) = self.search_results.get(idx)
1167        {
1168            self.buffer.cursor_mut().line = line;
1169            self.buffer.cursor_mut().column = column;
1170            self.adjust_scroll();
1171        }
1172    }
1173
1174    /// Replace current match
1175    pub fn replace_current(&mut self) {
1176        if let Some(idx) = self.search_index
1177            && let Some(&(line, column)) = self.search_results.get(idx)
1178            && self
1179                .buffer
1180                .replace_at(line, column, &self.search_query, &self.replace_query)
1181        {
1182            self.notify_lsp_document_change();
1183            self.update_search_results();
1184            // Move to next match or stay at current position
1185            if !self.search_results.is_empty() {
1186                if idx >= self.search_results.len() {
1187                    self.search_index = Some(0);
1188                }
1189                self.jump_to_current_match();
1190            }
1191        }
1192    }
1193
1194    /// Replace all matches
1195    pub fn replace_all(&mut self) {
1196        let count = self
1197            .buffer
1198            .replace_all(&self.search_query, &self.replace_query);
1199        if count > 0 {
1200            self.notify_lsp_document_change();
1201            self.set_status_message(format!("Replaced {} occurrences", count));
1202            self.update_search_results();
1203        }
1204    }
1205
1206    /// Toggle between search and replace fields
1207    pub fn toggle_search_field(&mut self) {
1208        if self.search_mode == SearchMode::Replace {
1209            self.search_active_field = match self.search_active_field {
1210                SearchField::Search => SearchField::Replace,
1211                SearchField::Replace => SearchField::Search,
1212            };
1213        }
1214    }
1215
1216    /// Handle keyboard input
1217    pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
1218        // Clear status message on any key press
1219        self.clear_status_message();
1220
1221        // Handle quit confirmation dialog if visible
1222        if self.show_quit_dialog {
1223            match key.code {
1224                KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
1225                    self.confirm_quit();
1226                    return Ok(());
1227                }
1228                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
1229                    self.cancel_quit_dialog();
1230                    return Ok(());
1231                }
1232                _ => {
1233                    // Ignore other keys when dialog is open
1234                    return Ok(());
1235                }
1236            }
1237        }
1238
1239        // Handle search dialog if visible
1240        if self.show_search_dialog {
1241            return self.handle_search_key(key);
1242        }
1243
1244        // Handle save-as dialog if visible
1245        if self.show_save_as_dialog {
1246            return self.handle_save_as_key(key);
1247        }
1248
1249        // Handle goto line dialog if visible
1250        if self.show_goto_line_dialog {
1251            return self.handle_goto_line_key(key);
1252        }
1253
1254        // Handle mq query dialog if visible
1255        if self.show_mq_query_dialog {
1256            return self.handle_mq_query_key(key);
1257        }
1258
1259        // Handle completion popup if visible
1260        if self.show_completion {
1261            match key.code {
1262                KeyCode::Up => {
1263                    self.completion_up();
1264                    return Ok(());
1265                }
1266                KeyCode::Down => {
1267                    self.completion_down();
1268                    return Ok(());
1269                }
1270                KeyCode::Enter => {
1271                    return self.apply_completion();
1272                }
1273                KeyCode::Esc => {
1274                    self.cancel_completion();
1275                    return Ok(());
1276                }
1277                KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
1278                    // Continue typing while completion is open - filter will be updated
1279                    self.buffer.insert_char(c);
1280                    self.notify_lsp_document_change();
1281                    self.filter_completion_items();
1282                    return Ok(());
1283                }
1284                KeyCode::Backspace => {
1285                    // Handle backspace while completion is open
1286                    let cursor = self.buffer.cursor();
1287                    if cursor.column <= self.completion_start_column {
1288                        // Backspace before completion start - close completion
1289                        self.cancel_completion();
1290                    }
1291                    self.buffer.delete_char();
1292                    self.notify_lsp_document_change();
1293                    if self.show_completion {
1294                        self.filter_completion_items();
1295                    }
1296                    return Ok(());
1297                }
1298                _ => {
1299                    // Any other key closes completion
1300                    self.cancel_completion();
1301                    // Continue to normal key handling below
1302                }
1303            }
1304        }
1305
1306        // Toggle file browser
1307        if self.config.keybindings.toggle_file_browser.matches(&key)
1308            || self
1309                .config
1310                .keybindings
1311                .toggle_file_browser_alt
1312                .matches(&key)
1313        {
1314            self.toggle_file_browser();
1315            return Ok(());
1316        }
1317
1318        // Handle file browser navigation when visible
1319        if self.show_file_browser {
1320            return self.handle_file_browser_key(key);
1321        }
1322
1323        // Check configured keybindings first
1324        if self.config.keybindings.quit.matches(&key)
1325            || self.config.keybindings.quit_alt.matches(&key)
1326        {
1327            if self.buffer.is_modified() && !self.pipe_mode {
1328                // Show quit confirmation dialog
1329                self.show_quit_dialog = true;
1330                self.set_status_message("Unsaved changes! Save before quitting?".to_string());
1331            } else {
1332                self.should_quit = true;
1333            }
1334            return Ok(());
1335        }
1336
1337        // Reset quit confirmation if any other key is pressed
1338        self.quit_confirm_pending = false;
1339
1340        if self.config.keybindings.save.matches(&key) {
1341            // Check if file has a path
1342            if self.buffer.file_path().is_none() {
1343                // No file path, open save-as dialog
1344                self.open_save_as_dialog();
1345            } else {
1346                // Has file path, save directly
1347                if let Err(e) = self.buffer.save() {
1348                    self.set_status_message(format!("Error saving file: {}", e));
1349                } else {
1350                    self.set_status_message("File saved successfully.".to_string());
1351                }
1352            }
1353            return Ok(());
1354        }
1355
1356        // LSP navigation - go to definition
1357        if self.config.keybindings.goto_definition.matches(&key) {
1358            return self.request_go_to_definition();
1359        }
1360
1361        // Navigation history - back and forward
1362        if self.config.keybindings.navigate_back.matches(&key) {
1363            return self.navigate_back();
1364        }
1365        if self.config.keybindings.navigate_forward.matches(&key) {
1366            return self.navigate_forward();
1367        }
1368
1369        // Toggle line numbers
1370        if self.config.keybindings.toggle_line_numbers.matches(&key) {
1371            self.toggle_line_numbers();
1372            return Ok(());
1373        }
1374
1375        // Toggle current line highlight
1376        if self
1377            .config
1378            .keybindings
1379            .toggle_current_line_highlight
1380            .matches(&key)
1381        {
1382            self.toggle_current_line_highlight();
1383            return Ok(());
1384        }
1385
1386        // Search
1387        if self.config.keybindings.search.matches(&key) {
1388            self.open_search();
1389            return Ok(());
1390        }
1391
1392        // Replace
1393        if self.config.keybindings.replace.matches(&key) {
1394            self.open_replace();
1395            return Ok(());
1396        }
1397
1398        // Go to line
1399        if self.config.keybindings.goto_line.matches(&key) {
1400            self.open_goto_line_dialog();
1401            return Ok(());
1402        }
1403
1404        // Execute mq query
1405        if self.config.keybindings.execute_mq_query.matches(&key) {
1406            self.open_mq_query_dialog();
1407            return Ok(());
1408        }
1409
1410        // Undo
1411        if self.config.keybindings.undo.matches(&key) {
1412            self.buffer.undo();
1413            self.notify_lsp_document_change();
1414            self.adjust_scroll();
1415            return Ok(());
1416        }
1417
1418        // Redo
1419        if self.config.keybindings.redo.matches(&key) {
1420            self.buffer.redo();
1421            self.notify_lsp_document_change();
1422            self.adjust_scroll();
1423            return Ok(());
1424        }
1425
1426        // Code completion - Ctrl+Space
1427        if key.code == KeyCode::Char(' ') && key.modifiers.contains(KeyModifiers::CONTROL) {
1428            return self.request_completion(None);
1429        }
1430
1431        // Navigation and editing keys (not configurable)
1432        match key.code {
1433            // Navigation
1434            KeyCode::Up => {
1435                self.buffer.move_cursor(CursorMovement::Up);
1436                self.adjust_scroll();
1437            }
1438            KeyCode::Down => {
1439                self.buffer.move_cursor(CursorMovement::Down);
1440                self.adjust_scroll();
1441            }
1442            KeyCode::Left => {
1443                self.buffer.move_cursor(CursorMovement::Left);
1444            }
1445            KeyCode::Right => {
1446                self.buffer.move_cursor(CursorMovement::Right);
1447            }
1448            KeyCode::Home => {
1449                self.buffer.move_cursor(CursorMovement::StartOfLine);
1450            }
1451            KeyCode::End => {
1452                self.buffer.move_cursor(CursorMovement::EndOfLine);
1453            }
1454            KeyCode::PageUp => {
1455                self.buffer.move_cursor(CursorMovement::PageUp);
1456                self.adjust_scroll();
1457            }
1458            KeyCode::PageDown => {
1459                self.buffer.move_cursor(CursorMovement::PageDown);
1460                self.adjust_scroll();
1461            }
1462
1463            // Editing
1464            KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
1465                self.buffer.insert_char(c);
1466                self.notify_lsp_document_change();
1467                // Check if the character is a trigger character for completion
1468                let language_id = file_type_to_language_id(self.buffer.file_type());
1469                if let Some(ref lsp) = self.lsp_manager
1470                    && let Some(lang_id) = &language_id
1471                    && lsp.is_trigger_character(lang_id, c)
1472                {
1473                    // Trigger completion with the character
1474                    let _ = self.request_completion(Some(c.to_string()));
1475                }
1476            }
1477            KeyCode::Enter => {
1478                self.buffer.insert_newline();
1479                self.adjust_scroll();
1480                self.notify_lsp_document_change();
1481            }
1482            KeyCode::Backspace => {
1483                self.buffer.delete_char();
1484                self.notify_lsp_document_change();
1485            }
1486            KeyCode::Tab => {
1487                self.buffer.insert_char('\t');
1488                self.notify_lsp_document_change();
1489            }
1490
1491            _ => {}
1492        }
1493
1494        Ok(())
1495    }
1496
1497    /// Handle paste event (used for IME input and clipboard paste)
1498    pub fn handle_paste(&mut self, text: String) -> Result<()> {
1499        // Don't handle paste when file browser is visible
1500        if self.show_file_browser {
1501            return Ok(());
1502        }
1503
1504        // Insert the pasted text at cursor position
1505        self.buffer.insert_str(&text);
1506        self.adjust_scroll();
1507        self.notify_lsp_document_change();
1508
1509        Ok(())
1510    }
1511
1512    /// Adjust scroll offset to keep cursor visible
1513    fn adjust_scroll(&mut self) {
1514        let cursor_line = self.buffer.cursor().line;
1515        let viewport_height = 20; // TODO: Get actual terminal height
1516
1517        // Scroll down if cursor is below viewport
1518        if cursor_line >= self.scroll_offset + viewport_height {
1519            self.scroll_offset = cursor_line - viewport_height + 1;
1520        }
1521
1522        // Scroll up if cursor is above viewport
1523        if cursor_line < self.scroll_offset {
1524            self.scroll_offset = cursor_line;
1525        }
1526    }
1527
1528    /// Handle keyboard input when file browser is visible
1529    fn handle_file_browser_key(&mut self, key: KeyEvent) -> Result<()> {
1530        match key.code {
1531            // Navigation in file browser
1532            KeyCode::Up => {
1533                if let Some(tree) = self.file_tree_mut() {
1534                    tree.move_up();
1535                }
1536            }
1537            KeyCode::Down => {
1538                if let Some(tree) = self.file_tree_mut() {
1539                    tree.move_down();
1540                }
1541            }
1542            KeyCode::Enter => {
1543                // Open selected file or toggle directory
1544                if let Some(tree) = self.file_tree_mut()
1545                    && let Some(item) = tree.selected_item()
1546                {
1547                    if item.is_dir {
1548                        tree.toggle_expand();
1549                    } else {
1550                        // Open the file
1551                        let path = item.path.clone();
1552                        let _ = tree; // Release borrow
1553                        if let Err(e) = self.open_file(&path) {
1554                            self.set_status_message(format!("Error opening file: {}", e));
1555                        } else {
1556                            self.show_file_browser = false;
1557                            self.set_status_message(format!("Opened: {}", path.display()));
1558                        }
1559                    }
1560                }
1561            }
1562            KeyCode::Left => {
1563                // Collapse directory
1564                if let Some(tree) = self.file_tree_mut()
1565                    && let Some(item) = tree.selected_item()
1566                    && item.is_dir
1567                    && item.expanded
1568                {
1569                    tree.toggle_expand();
1570                }
1571            }
1572            KeyCode::Right => {
1573                // Expand directory
1574                if let Some(tree) = self.file_tree_mut()
1575                    && let Some(item) = tree.selected_item()
1576                    && item.is_dir
1577                    && !item.expanded
1578                {
1579                    tree.toggle_expand();
1580                }
1581            }
1582            _ if self.config.keybindings.close_browser.matches(&key) => {
1583                // Close file browser
1584                self.show_file_browser = false;
1585            }
1586            _ => {}
1587        }
1588
1589        Ok(())
1590    }
1591
1592    /// Handle keyboard input when search dialog is visible
1593    fn handle_search_key(&mut self, key: KeyEvent) -> Result<()> {
1594        match key.code {
1595            KeyCode::Esc => {
1596                self.close_search();
1597            }
1598            KeyCode::Enter => {
1599                if key.modifiers.contains(KeyModifiers::SHIFT) {
1600                    self.search_prev();
1601                } else {
1602                    self.search_next();
1603                }
1604            }
1605            KeyCode::Tab => {
1606                self.toggle_search_field();
1607            }
1608            KeyCode::Backspace => match self.search_active_field {
1609                SearchField::Search => {
1610                    self.search_query.pop();
1611                    self.update_search_results();
1612                }
1613                SearchField::Replace => {
1614                    self.replace_query.pop();
1615                }
1616            },
1617            KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
1618                // Ctrl+R: Replace current
1619                if self.search_mode == SearchMode::Replace {
1620                    self.replace_current();
1621                }
1622            }
1623            KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
1624                // Ctrl+A: Replace all
1625                if self.search_mode == SearchMode::Replace {
1626                    self.replace_all();
1627                }
1628            }
1629            KeyCode::Char(c) => {
1630                match self.search_active_field {
1631                    SearchField::Search => {
1632                        self.search_query.push(c);
1633                        self.update_search_results();
1634                        // Auto-jump to first match
1635                        if !self.search_results.is_empty() {
1636                            self.search_index = Some(0);
1637                            self.jump_to_current_match();
1638                        }
1639                    }
1640                    SearchField::Replace => {
1641                        self.replace_query.push(c);
1642                    }
1643                }
1644            }
1645            _ => {}
1646        }
1647        Ok(())
1648    }
1649
1650    /// Handle keyboard input when save-as dialog is visible
1651    fn handle_save_as_key(&mut self, key: KeyEvent) -> Result<()> {
1652        match key.code {
1653            KeyCode::Esc => {
1654                self.close_save_as_dialog();
1655            }
1656            KeyCode::Enter => {
1657                if let Err(e) = self.confirm_save_as() {
1658                    self.set_status_message(format!("Error saving file: {}", e));
1659                    self.close_save_as_dialog();
1660                }
1661            }
1662            KeyCode::Backspace => {
1663                self.save_as_filename.pop();
1664            }
1665            KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
1666                self.save_as_filename.push(c);
1667            }
1668            _ => {}
1669        }
1670        Ok(())
1671    }
1672
1673    /// Handle keyboard input when mq query dialog is visible
1674    fn handle_mq_query_key(&mut self, key: KeyEvent) -> Result<()> {
1675        match key.code {
1676            KeyCode::Esc => {
1677                self.close_mq_query_dialog();
1678            }
1679            KeyCode::Enter => {
1680                self.execute_mq_query();
1681            }
1682            KeyCode::Backspace => {
1683                self.mq_query_input.pop();
1684                self.mq_query_result = None;
1685            }
1686            KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
1687                self.mq_query_input.push(c);
1688                self.mq_query_result = None;
1689            }
1690            _ => {}
1691        }
1692        Ok(())
1693    }
1694
1695    /// Handle keyboard input when goto line dialog is visible
1696    fn handle_goto_line_key(&mut self, key: KeyEvent) -> Result<()> {
1697        match key.code {
1698            KeyCode::Esc => {
1699                self.close_goto_line_dialog();
1700            }
1701            KeyCode::Enter => {
1702                if let Err(e) = self.confirm_goto_line() {
1703                    self.set_status_message(format!("{}", e));
1704                }
1705            }
1706            KeyCode::Backspace => {
1707                self.goto_line_input.pop();
1708            }
1709            KeyCode::Char(c) if c.is_ascii_digit() => {
1710                self.goto_line_input.push(c);
1711            }
1712            _ => {}
1713        }
1714        Ok(())
1715    }
1716}
1717
1718impl Default for App {
1719    fn default() -> Self {
1720        Self::new()
1721    }
1722}
1723
1724/// Convert FileType to language ID string for LSP
1725fn file_type_to_language_id(file_type: &FileType) -> Option<String> {
1726    match file_type {
1727        FileType::Code(lang_id) => Some(lang_id.clone()),
1728        FileType::Markdown => Some("markdown".to_string()),
1729        FileType::PlainText => None,
1730    }
1731}