tenseleyflow/fackr / 0c38b2b

Browse files

feat: Shift+F12 references panel with filtering

- Show navigable panel when multiple references found
- Arrow keys to navigate, Enter to jump, Esc to close
- Type to filter references by filename
- Panel shows relative paths with line numbers
Authored by espadonne
SHA
0c38b2b4bea10570f3daab80e9e251f223684644
Parents
f17f608
Tree
c41b528

2 changed files

StatusFile+-
M src/editor/state.rs 83 3
M src/render/screen.rs 176 1
src/editor/state.rsmodified
@@ -34,6 +34,13 @@ enum PromptState {
3434
         line: u32,
3535
         col: u32,
3636
     },
37
+    /// LSP references panel
38
+    ReferencesPanel {
39
+        locations: Vec<Location>,
40
+        selected_index: usize,
41
+        /// Search query being typed (for filtering)
42
+        query: String,
43
+    },
3744
 }
3845
 
3946
 /// Action to perform when text input is complete
@@ -394,11 +401,16 @@ impl Editor {
394401
                         if locations.is_empty() {
395402
                             self.message = Some("No references found".to_string());
396403
                         } else if locations.len() == 1 {
404
+                            // Single reference - just go there
397405
                             self.goto_location(&locations[0]);
398406
                         } else {
399
-                            // Multiple references - show count and go to first
400
-                            self.message = Some(format!("Found {} references", locations.len()));
401
-                            self.goto_location(&locations[0]);
407
+                            // Multiple references - show the references panel
408
+                            self.prompt = PromptState::ReferencesPanel {
409
+                                locations,
410
+                                selected_index: 0,
411
+                                query: String::new(),
412
+                            };
413
+                            self.message = None;
402414
                         }
403415
                     }
404416
                 }
@@ -1173,6 +1185,11 @@ impl Editor {
11731185
                 self.screen.render_rename_modal(original_name, new_name)?;
11741186
             }
11751187
 
1188
+            // Render references panel if active
1189
+            if let PromptState::ReferencesPanel { ref locations, selected_index, ref query } = self.prompt {
1190
+                self.screen.render_references_panel(locations, selected_index, query, &self.workspace.root)?;
1191
+            }
1192
+
11761193
             // After all overlays are rendered, reposition cursor to the correct location
11771194
             // (overlays may have moved the terminal cursor position)
11781195
             let cursor = cursors.primary();
@@ -3460,6 +3477,69 @@ impl Editor {
34603477
                     _ => {}
34613478
                 }
34623479
             }
3480
+            PromptState::ReferencesPanel { ref locations, ref mut selected_index, ref mut query } => {
3481
+                // Filter locations based on query
3482
+                let filtered: Vec<(usize, &Location)> = if query.is_empty() {
3483
+                    locations.iter().enumerate().collect()
3484
+                } else {
3485
+                    let q = query.to_lowercase();
3486
+                    locations.iter().enumerate()
3487
+                        .filter(|(_, loc)| {
3488
+                            loc.uri.to_lowercase().contains(&q)
3489
+                        })
3490
+                        .collect()
3491
+                };
3492
+
3493
+                match key {
3494
+                    Key::Enter => {
3495
+                        // Jump to selected reference
3496
+                        if let Some((orig_idx, _)) = filtered.get(*selected_index) {
3497
+                            let loc = locations[*orig_idx].clone();
3498
+                            self.prompt = PromptState::None;
3499
+                            self.goto_location(&loc);
3500
+                        }
3501
+                    }
3502
+                    Key::Escape => {
3503
+                        self.prompt = PromptState::None;
3504
+                        self.message = None;
3505
+                    }
3506
+                    Key::Up => {
3507
+                        if *selected_index > 0 {
3508
+                            *selected_index -= 1;
3509
+                        }
3510
+                    }
3511
+                    Key::Down => {
3512
+                        if *selected_index + 1 < filtered.len() {
3513
+                            *selected_index += 1;
3514
+                        }
3515
+                    }
3516
+                    Key::PageUp => {
3517
+                        *selected_index = selected_index.saturating_sub(10);
3518
+                    }
3519
+                    Key::PageDown => {
3520
+                        *selected_index = (*selected_index + 10).min(filtered.len().saturating_sub(1));
3521
+                    }
3522
+                    Key::Home => {
3523
+                        *selected_index = 0;
3524
+                    }
3525
+                    Key::End => {
3526
+                        if !filtered.is_empty() {
3527
+                            *selected_index = filtered.len() - 1;
3528
+                        }
3529
+                    }
3530
+                    Key::Backspace => {
3531
+                        query.pop();
3532
+                        // Reset selection when filter changes
3533
+                        *selected_index = 0;
3534
+                    }
3535
+                    Key::Char(c) => {
3536
+                        query.push(c);
3537
+                        // Reset selection when filter changes
3538
+                        *selected_index = 0;
3539
+                    }
3540
+                    _ => {}
3541
+                }
3542
+            }
34633543
             PromptState::None => {}
34643544
         }
34653545
         Ok(())
src/render/screen.rsmodified
@@ -15,7 +15,7 @@ use unicode_width::UnicodeWidthStr;
1515
 use crate::buffer::Buffer;
1616
 use crate::editor::{Cursors, Position};
1717
 use crate::fuss::VisibleItem;
18
-use crate::lsp::{CompletionItem, Diagnostic, DiagnosticSeverity, HoverInfo, ServerManagerPanel};
18
+use crate::lsp::{CompletionItem, Diagnostic, DiagnosticSeverity, HoverInfo, Location, ServerManagerPanel};
1919
 use crate::syntax::{Highlighter, HighlightState, Token};
2020
 
2121
 // Editor color scheme (256-color palette)
@@ -1947,6 +1947,181 @@ impl Screen {
19471947
         Ok(())
19481948
     }
19491949
 
1950
+    /// Render the LSP references panel (sidebar style)
1951
+    pub fn render_references_panel(
1952
+        &mut self,
1953
+        locations: &[Location],
1954
+        selected_index: usize,
1955
+        query: &str,
1956
+        workspace_root: &std::path::Path,
1957
+    ) -> Result<()> {
1958
+        let (width, height) = (self.cols as usize, self.rows as usize);
1959
+
1960
+        // Panel dimensions - sidebar style on the right
1961
+        let panel_width = 50.min(width / 2);
1962
+        let panel_height = height.saturating_sub(3); // Leave room for tab bar and status bar
1963
+        let start_col = width.saturating_sub(panel_width);
1964
+        let start_row = 1u16; // Below tab bar
1965
+
1966
+        // Filter locations based on query
1967
+        let filtered: Vec<(usize, &Location)> = if query.is_empty() {
1968
+            locations.iter().enumerate().collect()
1969
+        } else {
1970
+            let q = query.to_lowercase();
1971
+            locations.iter().enumerate()
1972
+                .filter(|(_, loc)| loc.uri.to_lowercase().contains(&q))
1973
+                .collect()
1974
+        };
1975
+
1976
+        // Colors
1977
+        let bg = Color::AnsiValue(235);
1978
+        let border_color = Color::AnsiValue(244);
1979
+        let header_color = Color::Cyan;
1980
+        let file_color = Color::AnsiValue(252);
1981
+        let line_num_color = Color::AnsiValue(243);
1982
+        let selected_bg = Color::AnsiValue(240);
1983
+        let input_bg = Color::AnsiValue(238);
1984
+
1985
+        // Draw top border with title
1986
+        let title = format!(" References ({}) ", filtered.len());
1987
+        execute!(
1988
+            self.stdout,
1989
+            MoveTo(start_col as u16, start_row),
1990
+            SetBackgroundColor(bg),
1991
+            SetForegroundColor(border_color),
1992
+            Print("┌"),
1993
+            SetForegroundColor(header_color),
1994
+            Print(&title),
1995
+            SetForegroundColor(border_color),
1996
+            Print(format!("{:─<width$}┐", "", width = panel_width.saturating_sub(title.len() + 2))),
1997
+        )?;
1998
+
1999
+        // Draw filter input row
2000
+        execute!(
2001
+            self.stdout,
2002
+            MoveTo(start_col as u16, start_row + 1),
2003
+            SetBackgroundColor(bg),
2004
+            SetForegroundColor(border_color),
2005
+            Print("│ "),
2006
+            SetForegroundColor(Color::AnsiValue(248)),
2007
+            Print("Filter: "),
2008
+            SetBackgroundColor(input_bg),
2009
+            SetForegroundColor(Color::White),
2010
+            Print(format!("{:<width$}", query, width = panel_width.saturating_sub(12))),
2011
+            SetBackgroundColor(bg),
2012
+            SetForegroundColor(border_color),
2013
+            Print("│"),
2014
+        )?;
2015
+
2016
+        // Draw separator
2017
+        execute!(
2018
+            self.stdout,
2019
+            MoveTo(start_col as u16, start_row + 2),
2020
+            SetBackgroundColor(bg),
2021
+            SetForegroundColor(border_color),
2022
+            Print(format!("├{:─<width$}┤", "", width = panel_width.saturating_sub(2))),
2023
+        )?;
2024
+
2025
+        // Calculate visible range with scrolling
2026
+        let visible_rows = panel_height.saturating_sub(5); // Account for borders, title, filter, help
2027
+        let scroll_offset = if selected_index >= visible_rows {
2028
+            selected_index - visible_rows + 1
2029
+        } else {
2030
+            0
2031
+        };
2032
+
2033
+        // Draw reference items
2034
+        for (display_idx, (_orig_idx, loc)) in filtered.iter().enumerate().skip(scroll_offset).take(visible_rows) {
2035
+            let row = start_row + 3 + (display_idx - scroll_offset) as u16;
2036
+            let is_selected = display_idx == selected_index;
2037
+
2038
+            // Extract relative path and line number
2039
+            let path_str = if loc.uri.starts_with("file://") {
2040
+                &loc.uri[7..]
2041
+            } else {
2042
+                &loc.uri
2043
+            };
2044
+
2045
+            // Make path relative to workspace if possible
2046
+            let display_path = if let Ok(rel_path) = std::path::Path::new(path_str).strip_prefix(workspace_root) {
2047
+                rel_path.to_string_lossy().to_string()
2048
+            } else {
2049
+                // Just show filename if we can't make it relative
2050
+                std::path::Path::new(path_str)
2051
+                    .file_name()
2052
+                    .map(|n| n.to_string_lossy().to_string())
2053
+                    .unwrap_or_else(|| path_str.to_string())
2054
+            };
2055
+
2056
+            let line_info = format!(":{}", loc.range.start.line + 1);
2057
+            let max_path_width = panel_width.saturating_sub(line_info.len() + 4);
2058
+            let truncated_path = if display_path.len() > max_path_width {
2059
+                format!("...{}", &display_path[display_path.len().saturating_sub(max_path_width - 3)..])
2060
+            } else {
2061
+                display_path
2062
+            };
2063
+
2064
+            let item_bg = if is_selected { selected_bg } else { bg };
2065
+
2066
+            execute!(
2067
+                self.stdout,
2068
+                MoveTo(start_col as u16, row),
2069
+                SetBackgroundColor(item_bg),
2070
+                SetForegroundColor(border_color),
2071
+                Print("│ "),
2072
+                SetForegroundColor(file_color),
2073
+                Print(format!("{:<width$}", truncated_path, width = max_path_width)),
2074
+                SetForegroundColor(line_num_color),
2075
+                Print(&line_info),
2076
+                SetForegroundColor(border_color),
2077
+                Print(format!("{:>width$}│", "", width = panel_width.saturating_sub(truncated_path.len() + line_info.len() + 3))),
2078
+            )?;
2079
+        }
2080
+
2081
+        // Fill remaining rows with empty space
2082
+        let items_drawn = filtered.len().saturating_sub(scroll_offset).min(visible_rows);
2083
+        for i in items_drawn..visible_rows {
2084
+            let row = start_row + 3 + i as u16;
2085
+            execute!(
2086
+                self.stdout,
2087
+                MoveTo(start_col as u16, row),
2088
+                SetBackgroundColor(bg),
2089
+                SetForegroundColor(border_color),
2090
+                Print(format!("│{:width$}│", "", width = panel_width.saturating_sub(2))),
2091
+            )?;
2092
+        }
2093
+
2094
+        // Draw help text row
2095
+        let help_row = start_row + 3 + visible_rows as u16;
2096
+        let help_text = "↑↓:nav  Enter:go  Esc:close";
2097
+        execute!(
2098
+            self.stdout,
2099
+            MoveTo(start_col as u16, help_row),
2100
+            SetBackgroundColor(bg),
2101
+            SetForegroundColor(border_color),
2102
+            Print("├"),
2103
+            SetForegroundColor(Color::AnsiValue(243)),
2104
+            Print(format!(" {:<width$}", help_text, width = panel_width.saturating_sub(3))),
2105
+            SetForegroundColor(border_color),
2106
+            Print("┤"),
2107
+        )?;
2108
+
2109
+        // Draw bottom border
2110
+        execute!(
2111
+            self.stdout,
2112
+            MoveTo(start_col as u16, help_row + 1),
2113
+            SetBackgroundColor(bg),
2114
+            SetForegroundColor(border_color),
2115
+            Print(format!("└{:─<width$}┘", "", width = panel_width.saturating_sub(2))),
2116
+            ResetColor,
2117
+        )?;
2118
+
2119
+        // Hide cursor when in references panel
2120
+        execute!(self.stdout, Hide)?;
2121
+
2122
+        Ok(())
2123
+    }
2124
+
19502125
     /// Render the LSP server manager panel
19512126
     pub fn render_server_manager_panel(&mut self, panel: &ServerManagerPanel) -> Result<()> {
19522127
         if !panel.visible {