Skip to content

Commit d97bd3d

Browse files
authored
Merge pull request #27 from yassinebridi/file-by-file-replace
File by file, and line by line replace
2 parents 502f145 + 9412e41 commit d97bd3d

19 files changed

Lines changed: 506 additions & 149 deletions

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "serpl"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
edition = "2021"
55
description = "A simple terminal UI for search and replace, ala VS Code"
66
repository = "https://github.com/yassinebridi/serpl"

README.md

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ https://github.com/yassinebridi/serpl/assets/18403595/348506704-73336074-bfaf-4a
2121
- [Replace Input](#replace-input)
2222
- [Search Results Pane](#search-results-pane)
2323
- [Preview Pane](#preview-pane)
24-
5. [Neovim Integration using toggleterm](#neovim-integration-using-toggleterm)
25-
6. [License](#license)
26-
7. [Contributing](#contributing)
27-
8. [Acknowledgements](#acknowledgements)
28-
9. [Similar Projects](#similar-projects)
24+
5. [Quick Hints](#quick-hints)
25+
6. [Neovim Integration using toggleterm](#neovim-integration-using-toggleterm)
26+
7. [License](#license)
27+
8. [Contributing](#contributing)
28+
9. [Acknowledgements](#acknowledgements)
29+
10. [Similar Projects](#similar-projects)
2930

3031
## Features
3132

@@ -115,24 +116,25 @@ Default key bindings can be customized through the `config.json` file.
115116

116117
#### Default Key Bindings
117118

118-
| Key Combination | Action |
119-
| ---------------------------- | --------------------------------- |
120-
| `Ctrl + c` | Quit |
121-
| `Ctrl + b` | Help |
122-
| `Tab` | Switch between tabs |
123-
| `Backtab` | Switch to previous tabs |
124-
| `Ctrl + o` | Process replace |
125-
| `Ctrl + n` | Toggle search and replace modes |
126-
| `Enter` | Execute search (for large folders)|
127-
| `g` / `Left` / `h` | Go to top of the list |
128-
| `G` / `Right` / `l` | Go to bottom of the list |
129-
| `j` / `Down` | Move to the next item |
130-
| `k` / `Up` | Move to the previous item |
131-
| `d` | Delete selected file or line |
132-
| `Esc` | Exit the current pane or dialog |
133-
| `Enter` (in dialogs) / `y` | Confirm action |
134-
| `Esc` (in dialogs) / `n` | Cancel action |
135-
| `h`, `l`, `Tab` (in dialogs) | Navigate dialog options |
119+
| Key Combination | Action |
120+
| ---------------------------- | ----------------------------------------- |
121+
| `Ctrl + c` | Quit |
122+
| `Ctrl + b` | Help |
123+
| `Tab` | Switch between tabs |
124+
| `Backtab` | Switch to previous tabs |
125+
| `Ctrl + o` | Process replace for all files |
126+
| `r` | Process replace for selected file or line |
127+
| `Ctrl + n` | Toggle search and replace modes |
128+
| `Enter` | Execute search (for large folders) |
129+
| `g` / `Left` / `h` | Go to top of the list |
130+
| `G` / `Right` / `l` | Go to bottom of the list |
131+
| `j` / `Down` | Move to the next item |
132+
| `k` / `Up` | Move to the previous item |
133+
| `d` | Delete selected file or line |
134+
| `Esc` | Exit the current pane or dialog |
135+
| `Enter` (in dialogs) / `y` | Confirm action |
136+
| `Esc` (in dialogs) / `n` | Cancel action |
137+
| `h`, `l`, `Tab` (in dialogs) | Navigate dialog options |
136138

137139
### Configuration
138140

@@ -214,7 +216,13 @@ You can customize the key bindings by modifying the configuration file in the fo
214216
### Search Input
215217

216218
- Input field for entering search keywords.
217-
- Toggle search modes (Simple, Match Case, Whole Word, Regex, AST Grep).
219+
- Toggle search modes (Simple, Match Case, Match Whole Word, Match Case Whole Word, Regex, AST Grep).
220+
- Simple: Search all occurrences of the keyword.
221+
- Match Case: Search occurrences with the same case as the keyword.
222+
- Match Whole Word: Search occurrences that match the keyword exactly.
223+
- Match Case Whole Word: Search occurrences that match the keyword exactly with the same case.
224+
- Regex: Search occurrences using a regular expression.
225+
- AST Grep: Search occurrences using AST Grep.
218226

219227
> [!TIP]
220228
> If current directory is considerebly large, you have to click `Enter` to start the search.
@@ -223,6 +231,9 @@ You can customize the key bindings by modifying the configuration file in the fo
223231

224232
- Input field for entering replacement text.
225233
- Toggle replace modes (Simple, Preserve Case, AST Grep).
234+
- Simple: Replace all occurrences of the keyword.
235+
- Preserve Case: Replace occurrences while preserving the case of the keyword.
236+
- AST Grep: Replace occurrences using AST Grep.
226237

227238
### Search Results Pane
228239

@@ -235,6 +246,14 @@ You can customize the key bindings by modifying the configuration file in the fo
235246
- Display of the selected file with highlighted search results, and context.
236247
- Navigation to view different matches within the file.
237248
- Option to delete individual lines containing matches.
249+
250+
## Quick Hints
251+
- Use the `Ctrl + b` key combination to display the help dialog.
252+
- Use the `Ctrl + o` key combination to process the replace for all files.
253+
- Use the `r` key to process the replace for the selected file or line.
254+
- Use the `Ctrl + n` key combination to toggle between search and replace modes.
255+
- Use the `g`, `G`, `j`, and `k` keys to navigate through the search results.
256+
- Use the `d` key to delete the selected file or line.
238257

239258
## Neovim Integration using toggleterm
240259

src/components/help_dialog.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl HelpDialog {
4747
// }
4848

4949
fn global_keybindings() -> String {
50-
"- q: Quit\n- Ctrl-c: Quit\n- Ctrl-b: Help dialog\n- Cltr-o: Process Replace\n- Ctrl-n: Loop through search and replace modes\n- Enter: Select/Deselect file\n- d: delete file/delete line from the replace process".to_string()
50+
"- q: Quit\n- Ctrl-c: Quit\n- Ctrl-b: Help dialog\n- Cltr-o: Process Replace For All Files\n- Ctrl-n: Loop through search and replace modes\n- Enter: Select/Deselect file\n- d: delete file/delete line from the replace process\n- r: Replace Selected File Or Line".to_string()
5151
}
5252

5353
fn navigation_keybindings() -> String {

src/components/preview.rs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::{
2121
action::Action,
2222
state::{FocusedScreen, ReplaceTextKind, SearchResultState, SearchTextKind, State, SubMatch},
2323
thunk::ThunkAction,
24+
utils::{apply_replace, get_search_regex},
2425
},
2526
tabs::Tab,
2627
};
@@ -99,12 +100,25 @@ impl Preview {
99100
}
100101
}
101102

103+
fn replace_selected_line(&mut self, selected_result_state: &SearchResultState) {
104+
if let Some(selected_index) = self.lines_state.selected() {
105+
let line_index = self.non_divider_lines.iter().position(|&index| index == selected_index).unwrap_or(0);
106+
let file_index = selected_result_state.index.unwrap_or(0);
107+
let replace_line_thunk = AppAction::Thunk(ThunkAction::ProcessLineReplace(file_index, line_index));
108+
self.command_tx.as_ref().unwrap().send(replace_line_thunk).unwrap();
109+
}
110+
}
111+
112+
// disable too_many_arguments clippy
113+
#[allow(clippy::too_many_arguments)]
102114
fn format_match_lines<'a>(
103115
&self,
104116
full_match: &'a str,
105117
submatches: &[SubMatch],
106-
replace_text: &'a String,
118+
replace_text: &'a str,
107119
replacement: &'a Option<String>,
120+
search_kind: &SearchTextKind,
121+
replace_kind: &ReplaceTextKind,
108122
is_ast_grep: bool,
109123
) -> Vec<Line<'a>> {
110124
let mut lines = Vec::new();
@@ -150,14 +164,31 @@ impl Preview {
150164

151165
spans.push(Span::raw(common_suffix));
152166
}
153-
} else if replace_text.is_empty() {
154-
spans.push(Span::styled(matched_text, Style::default().bg(Color::Blue)));
155167
} else {
156-
spans.push(Span::styled(
157-
matched_text,
158-
Style::default().fg(Color::LightRed).add_modifier(Modifier::CROSSED_OUT),
159-
));
160-
spans.push(Span::styled(replace_text, Style::default().fg(Color::White).bg(Color::Green)));
168+
let re = get_search_regex(matched_text, search_kind);
169+
170+
for cap in re.captures_iter(line) {
171+
let m = cap.get(0).unwrap();
172+
let start = m.start();
173+
let end = m.end();
174+
175+
if start > last_end {
176+
spans.push(Span::raw(&line[last_end..start]));
177+
}
178+
179+
if replace_text.is_empty() {
180+
spans.push(Span::styled(matched_text, Style::default().bg(Color::Blue)));
181+
} else {
182+
let replacement = apply_replace(matched_text, replace_text, replace_kind);
183+
spans.push(Span::styled(
184+
matched_text,
185+
Style::default().fg(Color::White).bg(Color::LightRed).add_modifier(Modifier::CROSSED_OUT),
186+
));
187+
spans.push(Span::styled(replacement, Style::default().fg(Color::White).bg(Color::Green)));
188+
}
189+
190+
last_end = end;
191+
}
161192
}
162193

163194
last_end = end;
@@ -235,6 +266,10 @@ impl Component for Preview {
235266
self.previous();
236267
Ok(None)
237268
},
269+
(KeyCode::Char('r'), _) => {
270+
self.replace_selected_line(&state.selected_result);
271+
Ok(None)
272+
},
238273
(KeyCode::Enter, _) | (KeyCode::Esc, _) => {
239274
let action = AppAction::Action(Action::SetActiveTab { tab: Tab::SearchResult });
240275
self.command_tx.as_ref().unwrap().send(action).unwrap();
@@ -292,6 +327,8 @@ impl Component for Preview {
292327
&result.submatches,
293328
&state.replace_text.text,
294329
&result.replacement,
330+
&state.search_text.kind,
331+
&state.replace_text.kind,
295332
is_ast_grep,
296333
);
297334
for (i, formatted_line) in formatted_lines.clone().into_iter().enumerate() {

src/components/replace.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ impl Replace {
5858
if replace_text_kind == ReplaceTextKind::AstGrep {
5959
let search_text_action = AppAction::Action(Action::SetSearchTextKind { kind: SearchTextKind::AstGrep });
6060
self.command_tx.as_ref().unwrap().send(search_text_action).unwrap();
61+
} else if replace_text_kind != ReplaceTextKind::AstGrep {
62+
let search_text_action = AppAction::Action(Action::SetSearchTextKind { kind: SearchTextKind::Simple });
63+
self.command_tx.as_ref().unwrap().send(search_text_action).unwrap();
6164
}
6265

6366
let process_search_thunk = AppAction::Thunk(ThunkAction::ProcessSearch);

src/components/search.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ impl Search {
8585
if search_text_kind == SearchTextKind::AstGrep {
8686
let replace_text_action = AppAction::Action(Action::SetReplaceTextKind { kind: ReplaceTextKind::AstGrep });
8787
self.command_tx.as_ref().unwrap().send(replace_text_action).unwrap();
88+
} else if state.replace_text.kind == ReplaceTextKind::AstGrep {
89+
let replace_text_action = AppAction::Action(Action::SetReplaceTextKind { kind: ReplaceTextKind::Simple });
90+
self.command_tx.as_ref().unwrap().send(replace_text_action).unwrap();
8891
}
8992

9093
let process_search_thunk = AppAction::Thunk(ThunkAction::ProcessSearch);

src/components/search_result.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ impl SearchResult {
180180
self.match_counts.push(total_matches_str);
181181
self.match_counts.last().unwrap()
182182
}
183+
184+
fn replace_single_file(&mut self, state: &State) {
185+
if let Some(selected_index) = self.state.selected() {
186+
if selected_index < state.search_result.list.len() {
187+
let process_single_file_replace_thunk = AppAction::Thunk(ThunkAction::ProcessSingleFileReplace(selected_index));
188+
self.command_tx.as_ref().unwrap().send(process_single_file_replace_thunk).unwrap();
189+
}
190+
}
191+
}
183192
}
184193

185194
impl Component for SearchResult {
@@ -216,6 +225,10 @@ impl Component for SearchResult {
216225
self.previous(state);
217226
Ok(None)
218227
},
228+
(KeyCode::Char('r'), _) => {
229+
self.replace_single_file(state);
230+
Ok(None)
231+
},
219232
(KeyCode::Enter, _) => {
220233
let action = AppAction::Action(Action::SetActiveTab { tab: Tab::Preview });
221234
self.command_tx.as_ref().unwrap().send(action).unwrap();

src/components/small_help.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ impl Component for SmallHelp {
5555
fn draw(&mut self, f: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
5656
let layout = get_layout(area);
5757
let content = match state.focused_screen {
58-
FocusedScreen::SearchInput => "Help: <Ctrl-b> | Search: <Enter> | Switch to Replace: <Tab> | Toggle search mode: <Ctrl-n>",
59-
FocusedScreen::ReplaceInput => "Help: <Ctrl-b> | Replace: <C-o> | Switch to Search List: <Tab> | Toggle replace mode: <Ctrl-n>",
60-
FocusedScreen::SearchResultList => "Help: <Ctrl-b> | Open File: <Enter> | Switch to Search: <Tab> | Next: <j> | Previous: <k> | Top: <g> | Bottom: <G> | Delete file: <d>",
61-
FocusedScreen::Preview => "Help: <Ctrl-b> | Back to list: <Enter> | Switch to Search: <Tab> | Next: <j> | Previous: <k> | Top: <g> | Bottom: <G> | Delete line: <d>",
58+
FocusedScreen::SearchInput => "Help: <Ctrl-b> | Search: <Enter> | Toggle search mode: <Ctrl-n>",
59+
FocusedScreen::ReplaceInput => "Help: <Ctrl-b> | Replace: <C-o> | Toggle replace mode: <Ctrl-n>",
60+
FocusedScreen::SearchResultList => "Help: <Ctrl-b> | Open File: <Enter> | Replace File: <r> | Next: <j> | Previous: <k> | Top: <g> | Bottom: <G> | Delete file: <d>",
61+
FocusedScreen::Preview => "Help: <Ctrl-b> | Back to list: <Enter> | Replace Line: <r> | Next: <j> | Previous: <k> | Top: <g> | Bottom: <G> | Delete line: <d>",
6262
FocusedScreen::ConfirmReplaceDialog => "Confirm Replace: <Enter> | Cancel Replace: <Esc>, Left: <h>, Right: <l>, Loop: <Tab>",
6363
FocusedScreen::ConfirmGitDirectoryDialog => "Confirm Replace: <Enter> | Cancel Replace: <Esc>, Left: <h>, Right: <l>, Loop: <Tab>",
6464
FocusedScreen::HelpDialog => "Close Help: <Esc> | Next Tab: <Right> | Previous Tab: <Left>",

src/redux.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod action;
22
pub mod reducer;
33
pub mod state;
44
pub mod thunk;
5+
pub mod utils;
56

67
#[derive(Debug)]
78
pub enum ActionOrThunk {

0 commit comments

Comments
 (0)