tenseleyflow/fackr / c5090ec

Browse files

feat: add centered rename modal dialog

Replace the status bar text input for LSP rename with a sleek
centered modal dialog that shows:
- "Rename Symbol" title
- "From:" with the original symbol name
- "To:" with an editable input field

The modal provides better visibility and UX for the rename operation.
Authored by espadonne
SHA
c5090ecf7775d7306802e3c2a4db717bcd1f0534
Parents
15f5908
Tree
f91fb4c

2 changed files

StatusFile+-
M src/editor/state.rs 64 23
M src/render/screen.rs 109 0
src/editor/state.rsmodified
@@ -26,6 +26,14 @@ enum PromptState {
2626
     RestoreBackup,
2727
     /// Text input prompt (label, current input buffer)
2828
     TextInput { label: String, buffer: String, action: TextInputAction },
29
+    /// LSP rename modal with original name shown
30
+    RenameModal {
31
+        original_name: String,
32
+        new_name: String,
33
+        path: String,
34
+        line: u32,
35
+        col: u32,
36
+    },
2937
 }
3038
 
3139
 /// Action to perform when text input is complete
@@ -35,8 +43,6 @@ enum TextInputAction {
3543
     GitCommit,
3644
     /// Create a git tag
3745
     GitTag,
38
-    /// LSP rename symbol
39
-    LspRename { path: String, line: u32, col: u32 },
4046
 }
4147
 
4248
 /// LSP UI state
@@ -781,12 +787,18 @@ impl Editor {
781787
                 String::new()
782788
             };
783789
 
784
-            self.prompt = PromptState::TextInput {
785
-                label: "Rename to: ".to_string(),
786
-                buffer: current_word.clone(),
787
-                action: TextInputAction::LspRename { path: path_str, line, col },
790
+            if current_word.is_empty() {
791
+                self.message = Some("No symbol under cursor".to_string());
792
+                return;
793
+            }
794
+
795
+            self.prompt = PromptState::RenameModal {
796
+                original_name: current_word.clone(),
797
+                new_name: current_word,
798
+                path: path_str,
799
+                line,
800
+                col,
788801
             };
789
-            self.message = Some(format!("Rename '{}' to: {}", current_word, current_word));
790802
         } else {
791803
             self.message = Some("No file open".to_string());
792804
         }
@@ -1156,6 +1168,11 @@ impl Editor {
11561168
                 self.screen.render_server_manager_panel(&self.server_manager)?;
11571169
             }
11581170
 
1171
+            // Render rename modal if active
1172
+            if let PromptState::RenameModal { ref original_name, ref new_name, .. } = self.prompt {
1173
+                self.screen.render_rename_modal(original_name, new_name)?;
1174
+            }
1175
+
11591176
             // After all overlays are rendered, reposition cursor to the correct location
11601177
             // (overlays may have moved the terminal cursor position)
11611178
             let cursor = cursors.primary();
@@ -3403,6 +3420,46 @@ impl Editor {
34033420
                     }
34043421
                 }
34053422
             }
3423
+            PromptState::RenameModal { ref original_name, ref mut new_name, ref path, line, col } => {
3424
+                match key {
3425
+                    Key::Enter => {
3426
+                        // Clone values before modifying self.prompt
3427
+                        let original = original_name.clone();
3428
+                        let new = new_name.clone();
3429
+                        let path = path.clone();
3430
+
3431
+                        // Execute rename
3432
+                        if new.is_empty() {
3433
+                            self.prompt = PromptState::None;
3434
+                            self.message = Some("Rename cancelled: empty name".to_string());
3435
+                        } else if new == original {
3436
+                            self.prompt = PromptState::None;
3437
+                            self.message = Some("Rename cancelled: name unchanged".to_string());
3438
+                        } else {
3439
+                            self.prompt = PromptState::None;
3440
+                            match self.workspace.lsp.request_rename(&path, line, col, &new) {
3441
+                                Ok(_id) => {
3442
+                                    self.message = Some(format!("Renaming '{}' to '{}'...", original, new));
3443
+                                }
3444
+                                Err(e) => {
3445
+                                    self.message = Some(format!("Rename failed: {}", e));
3446
+                                }
3447
+                            }
3448
+                        }
3449
+                    }
3450
+                    Key::Escape => {
3451
+                        self.prompt = PromptState::None;
3452
+                        self.message = Some("Rename cancelled".to_string());
3453
+                    }
3454
+                    Key::Backspace => {
3455
+                        new_name.pop();
3456
+                    }
3457
+                    Key::Char(c) => {
3458
+                        new_name.push(c);
3459
+                    }
3460
+                    _ => {}
3461
+                }
3462
+            }
34063463
             PromptState::None => {}
34073464
         }
34083465
         Ok(())
@@ -3418,22 +3475,6 @@ impl Editor {
34183475
                 let (_, msg) = self.workspace.fuss.git_tag(buffer);
34193476
                 self.message = Some(msg);
34203477
             }
3421
-            TextInputAction::LspRename { path, line, col } => {
3422
-                if buffer.is_empty() {
3423
-                    self.message = Some("Rename cancelled: empty name".to_string());
3424
-                    return;
3425
-                }
3426
-                match self.workspace.lsp.request_rename(&path, line, col, buffer) {
3427
-                    Ok(_id) => {
3428
-                        self.message = Some(format!("Renaming to '{}'...", buffer));
3429
-                        // Note: The actual rename edits will be applied when we receive
3430
-                        // the response and implement WorkspaceEdit handling
3431
-                    }
3432
-                    Err(e) => {
3433
-                        self.message = Some(format!("Rename failed: {}", e));
3434
-                    }
3435
-                }
3436
-            }
34373478
         }
34383479
     }
34393480
 
src/render/screen.rsmodified
@@ -1838,6 +1838,115 @@ impl Screen {
18381838
         Ok(())
18391839
     }
18401840
 
1841
+    /// Render a centered rename modal dialog
1842
+    pub fn render_rename_modal(&mut self, original_name: &str, new_name: &str) -> Result<()> {
1843
+        let (width, height) = (self.cols, self.rows);
1844
+
1845
+        // Calculate modal dimensions
1846
+        let title = "Rename Symbol";
1847
+        let from_label = "From: ";
1848
+        let to_label = "To:   ";
1849
+        let content_width = original_name.len().max(new_name.len()).max(20).max(title.len());
1850
+        let modal_width = content_width + 8; // padding + border
1851
+        let modal_height = 6; // title + from + to + bottom border + padding
1852
+
1853
+        // Center the modal
1854
+        let start_col = ((width as usize).saturating_sub(modal_width)) / 2;
1855
+        let start_row = ((height as usize).saturating_sub(modal_height)) / 2;
1856
+
1857
+        let bg = Color::AnsiValue(236);
1858
+        let border_color = Color::AnsiValue(244);
1859
+        let label_color = Color::AnsiValue(248);
1860
+        let value_color = Color::White;
1861
+        let input_bg = Color::AnsiValue(238);
1862
+
1863
+        // Draw top border
1864
+        execute!(
1865
+            self.stdout,
1866
+            MoveTo(start_col as u16, start_row as u16),
1867
+            SetBackgroundColor(bg),
1868
+            SetForegroundColor(border_color),
1869
+            Print(format!("┌{:─<width$}┐", "", width = modal_width - 2)),
1870
+        )?;
1871
+
1872
+        // Draw title row
1873
+        let title_padding = (modal_width - 2 - title.len()) / 2;
1874
+        execute!(
1875
+            self.stdout,
1876
+            MoveTo(start_col as u16, start_row as u16 + 1),
1877
+            SetBackgroundColor(bg),
1878
+            SetForegroundColor(border_color),
1879
+            Print("│"),
1880
+            SetForegroundColor(Color::Cyan),
1881
+            Print(format!("{:>pad$}{}{:<rpad$}", "", title, "", pad = title_padding, rpad = modal_width - 2 - title_padding - title.len())),
1882
+            SetForegroundColor(border_color),
1883
+            Print("│"),
1884
+        )?;
1885
+
1886
+        // Draw separator
1887
+        execute!(
1888
+            self.stdout,
1889
+            MoveTo(start_col as u16, start_row as u16 + 2),
1890
+            SetBackgroundColor(bg),
1891
+            SetForegroundColor(border_color),
1892
+            Print(format!("├{:─<width$}┤", "", width = modal_width - 2)),
1893
+        )?;
1894
+
1895
+        // Draw "From:" row
1896
+        execute!(
1897
+            self.stdout,
1898
+            MoveTo(start_col as u16, start_row as u16 + 3),
1899
+            SetBackgroundColor(bg),
1900
+            SetForegroundColor(border_color),
1901
+            Print("│ "),
1902
+            SetForegroundColor(label_color),
1903
+            Print(from_label),
1904
+            SetForegroundColor(value_color),
1905
+            Print(format!("{:<width$}", original_name, width = modal_width - 4 - from_label.len())),
1906
+            SetForegroundColor(border_color),
1907
+            Print(" │"),
1908
+        )?;
1909
+
1910
+        // Draw "To:" row with input field
1911
+        let input_width = modal_width - 4 - to_label.len();
1912
+        execute!(
1913
+            self.stdout,
1914
+            MoveTo(start_col as u16, start_row as u16 + 4),
1915
+            SetBackgroundColor(bg),
1916
+            SetForegroundColor(border_color),
1917
+            Print("│ "),
1918
+            SetForegroundColor(label_color),
1919
+            Print(to_label),
1920
+            SetBackgroundColor(input_bg),
1921
+            SetForegroundColor(Color::White),
1922
+            Print(format!("{:<width$}", new_name, width = input_width)),
1923
+            SetBackgroundColor(bg),
1924
+            SetForegroundColor(border_color),
1925
+            Print(" │"),
1926
+        )?;
1927
+
1928
+        // Draw bottom border
1929
+        execute!(
1930
+            self.stdout,
1931
+            MoveTo(start_col as u16, start_row as u16 + 5),
1932
+            SetBackgroundColor(bg),
1933
+            SetForegroundColor(border_color),
1934
+            Print(format!("└{:─<width$}┘", "", width = modal_width - 2)),
1935
+            ResetColor,
1936
+        )?;
1937
+
1938
+        // Position cursor in the input field
1939
+        let cursor_col = start_col + 2 + to_label.len() + new_name.len();
1940
+        execute!(
1941
+            self.stdout,
1942
+            MoveTo(cursor_col as u16, start_row as u16 + 4),
1943
+            SetBackgroundColor(input_bg),
1944
+            crossterm::cursor::Show,
1945
+        )?;
1946
+
1947
+        Ok(())
1948
+    }
1949
+
18411950
     /// Render the LSP server manager panel
18421951
     pub fn render_server_manager_panel(&mut self, panel: &ServerManagerPanel) -> Result<()> {
18431952
         if !panel.visible {