Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ If no file is given, the editor opens an empty, unnamed buffer.
| Ctrl-Q | Quit (press again to discard unsaved changes) |
| Ctrl-F | Find: all matches are highlighted with a count in the prompt; Ctrl-F again jumps to the next match (wraps around), Enter confirms, Esc cancels |
| Ctrl-G | Jump to a line number (Enter jumps, Esc cancels) |
| Ctrl-R | Search & replace: type the search text, Enter, then the replacement text, Enter to replace every match at once (Esc cancels either step) |
| Ctrl-Z / Ctrl-Y | Undo / redo |
| Ctrl-C / Ctrl-X | Copy / cut the selection (also pushed to the system clipboard) |
| Ctrl-V | Paste the internal clipboard |
Expand Down
37 changes: 37 additions & 0 deletions src/editor.c
Original file line number Diff line number Diff line change
Expand Up @@ -836,3 +836,40 @@ void editor_paste_text(editor_t *ed, const char *text, size_t len)
}
insert_text_replacing_selection(ed, text, len);
}

size_t editor_replace_all(editor_t *ed, const char *query, const char *replacement)
{
size_t count;
search_match_t *matches = search_find_all(&ed->buf, query, &count);
if (count == 0) {
free(matches);
return 0;
}

size_t query_len = strlen(query);
size_t replacement_len = strlen(replacement);

size_t last_row = ed->buf.num_lines - 1;
size_t last_col = ed->buf.lines[last_row].len;

edit_capture_t cap;
capture_before(ed, 0, 0, last_row, last_col, &cap);

for (size_t i = count; i > 0; i--) {
search_match_t *m = &matches[i - 1];
buffer_delete_range(&ed->buf, m->row, m->col, m->row, m->col + query_len);
buffer_insert_text(&ed->buf, m->row, m->col, replacement, replacement_len);
}
free(matches);

last_row = ed->buf.num_lines - 1;
last_col = ed->buf.lines[last_row].len;
commit_edit(ed, &cap, last_row, last_col);

ed->cy = 0;
ed->cx = 0;
ed->goal_rx = 0;
ed->sel.active = false;

return count;
}
6 changes: 6 additions & 0 deletions src/editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ typedef enum {
MODE_PROMPT_FIND,
MODE_PROMPT_GOTO,
MODE_PROMPT_SAVE_AS,
MODE_PROMPT_REPLACE_SEARCH,
MODE_PROMPT_REPLACE_WITH,
} editor_mode_t;

typedef struct {
Expand Down Expand Up @@ -54,6 +56,8 @@ typedef struct {
size_t search_start_cx;
search_match_t *search_matches;
size_t search_match_count;
char replace_search[256];
size_t replace_search_len;

char status_msg[256];
const char *filetype;
Expand Down Expand Up @@ -121,4 +125,6 @@ void editor_cut(editor_t *ed);
void editor_paste(editor_t *ed);
void editor_paste_text(editor_t *ed, const char *text, size_t len);

size_t editor_replace_all(editor_t *ed, const char *query, const char *replacement);

#endif /* DAF_EDITOR_H */
45 changes: 42 additions & 3 deletions src/input.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "input.h"

#include <stdlib.h>
#include <string.h>

#include "filetype.h"
#include "search.h"
Expand Down Expand Up @@ -52,6 +53,20 @@ static void enter_goto_mode(editor_t *ed)
ed->prompt_input[0] = '\0';
}

static void enter_replace_mode(editor_t *ed)
{
ed->mode = MODE_PROMPT_REPLACE_SEARCH;
ed->prompt_len = 0;
ed->prompt_input[0] = '\0';
ed->search_start_cy = ed->cy;
ed->search_start_cx = ed->cx;
}

static bool is_find_like_mode(editor_mode_t mode)
{
return mode == MODE_PROMPT_FIND || mode == MODE_PROMPT_REPLACE_SEARCH;
}

static void prompt_append_char(editor_t *ed, unsigned int codepoint)
{
if (codepoint >= 128) {
Expand Down Expand Up @@ -128,6 +143,28 @@ static void confirm_prompt(editor_t *ed)
}
break;

case MODE_PROMPT_REPLACE_SEARCH:
if (ed->prompt_len == 0) {
ed->mode = MODE_EDIT;
editor_clear_search_matches(ed);
editor_set_status(ed, "Cancelled");
break;
}
memcpy(ed->replace_search, ed->prompt_input, ed->prompt_len + 1);
ed->replace_search_len = ed->prompt_len;
ed->prompt_len = 0;
ed->prompt_input[0] = '\0';
ed->mode = MODE_PROMPT_REPLACE_WITH;
break;

case MODE_PROMPT_REPLACE_WITH: {
size_t count = editor_replace_all(ed, ed->replace_search, ed->prompt_input);
ed->mode = MODE_EDIT;
editor_clear_search_matches(ed);
editor_set_status(ed, "Replaced %zu occurrence(s)", count);
break;
}

case MODE_EDIT:
default:
break;
Expand All @@ -137,7 +174,7 @@ static void confirm_prompt(editor_t *ed)
static void process_prompt_key(editor_t *ed, key_event_t key)
{
if (key.type == KEY_ESCAPE) {
if (ed->mode == MODE_PROMPT_FIND) {
if (is_find_like_mode(ed->mode) || ed->mode == MODE_PROMPT_REPLACE_WITH) {
ed->cy = ed->search_start_cy;
ed->cx = ed->search_start_cx;
editor_clear_search_matches(ed);
Expand All @@ -149,7 +186,7 @@ static void process_prompt_key(editor_t *ed, key_event_t key)

if (key.type == KEY_BACKSPACE) {
prompt_backspace(ed);
if (ed->mode == MODE_PROMPT_FIND) {
if (is_find_like_mode(ed->mode)) {
do_incremental_search(ed);
}
return;
Expand All @@ -172,7 +209,7 @@ static void process_prompt_key(editor_t *ed, key_event_t key)

if (key.type == KEY_CHAR && !(key.mods & MOD_CTRL)) {
prompt_append_char(ed, key.codepoint);
if (ed->mode == MODE_PROMPT_FIND) {
if (is_find_like_mode(ed->mode)) {
do_incremental_search(ed);
}
}
Expand Down Expand Up @@ -229,6 +266,8 @@ void input_process_key(editor_t *ed, key_event_t key)
enter_find_mode(ed);
} else if (key.codepoint == CTRL_KEY('g')) {
enter_goto_mode(ed);
} else if (key.codepoint == CTRL_KEY('r')) {
enter_replace_mode(ed);
} else if (key.codepoint == CTRL_KEY('z')) {
editor_undo(ed);
} else if (key.codepoint == CTRL_KEY('y')) {
Expand Down
24 changes: 21 additions & 3 deletions src/render.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,24 @@ static const char *prompt_label(editor_mode_t mode)
return "Go to line: ";
case MODE_PROMPT_SAVE_AS:
return "Save as: ";
case MODE_PROMPT_REPLACE_SEARCH:
return "Replace: ";
case MODE_PROMPT_REPLACE_WITH:
return "With: ";
case MODE_EDIT:
default:
return "";
}
}

static size_t active_search_query_len(const editor_t *ed)
{
if (ed->mode == MODE_PROMPT_REPLACE_WITH) {
return ed->replace_search_len;
}
return ed->prompt_len;
}

static void render_gutter(abuf_t *ab, const editor_t *ed, size_t file_row, bool is_current)
{
int width = editor_gutter_width(ed);
Expand Down Expand Up @@ -110,9 +122,10 @@ static void render_empty_gutter(abuf_t *ab, const editor_t *ed)

static bool byte_in_search_match(const editor_t *ed, size_t row, size_t byte_col)
{
size_t query_len = active_search_query_len(ed);
for (size_t i = 0; i < ed->search_match_count; i++) {
const search_match_t *m = &ed->search_matches[i];
if (m->row == row && byte_col >= m->col && byte_col < m->col + ed->prompt_len) {
if (m->row == row && byte_col >= m->col && byte_col < m->col + query_len) {
return true;
}
}
Expand All @@ -137,7 +150,9 @@ static void render_text_row(abuf_t *ab, const editor_t *ed, size_t file_row)
hl_end = (file_row == r2) ? c2 : line->len;
}
}
bool show_matches = ed->mode == MODE_PROMPT_FIND && ed->search_match_count > 0;
bool show_matches = (ed->mode == MODE_PROMPT_FIND || ed->mode == MODE_PROMPT_REPLACE_SEARCH ||
ed->mode == MODE_PROMPT_REPLACE_WITH) &&
ed->search_match_count > 0;

size_t i = 0;
size_t rx = 0;
Expand Down Expand Up @@ -233,7 +248,10 @@ static void render_message_bar(abuf_t *ab, const editor_t *ed)
if (ed->mode != MODE_EDIT) {
abuf_append_str(ab, prompt_label(ed->mode));
abuf_append(ab, ed->prompt_input, ed->prompt_len);
if (ed->mode == MODE_PROMPT_FIND && ed->prompt_len > 0) {

bool has_query = (ed->mode == MODE_PROMPT_FIND || ed->mode == MODE_PROMPT_REPLACE_SEARCH) &&
ed->prompt_len > 0;
if (has_query || ed->mode == MODE_PROMPT_REPLACE_WITH) {
char count_buf[32];
int n = snprintf(count_buf, sizeof(count_buf), " (%zu found)", ed->search_match_count);
if (n > 0) {
Expand Down
2 changes: 1 addition & 1 deletion tests/meson.build
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
test_names = ['buffer', 'search', 'encoding', 'filetype', 'editor', 'undo', 'clipboard']
test_names = ['buffer', 'search', 'encoding', 'filetype', 'editor', 'undo', 'clipboard', 'replace']

foreach name : test_names
test_exe = executable(
Expand Down
90 changes: 90 additions & 0 deletions tests/test_replace.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* SPDX-FileCopyrightText: AnmiTaliDev <anmitalidev@nuros.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*/

#include <assert.h>
#include <string.h>

#include "editor.h"

static void set_line(editor_t *ed, size_t row, const char *text)
{
buffer_insert_bytes(&ed->buf, row, 0, text, strlen(text));
}

static void test_replace_all_same_length(void)
{
editor_t ed;
editor_init(&ed, 24, 80);
set_line(&ed, 0, "foo bar foo");
buffer_split_line(&ed.buf, 0, 11);
set_line(&ed, 1, "baz foo");

size_t count = editor_replace_all(&ed, "foo", "qux");
assert(count == 3);
assert(memcmp(ed.buf.lines[0].chars, "qux bar qux", 11) == 0);
assert(memcmp(ed.buf.lines[1].chars, "baz qux", 7) == 0);
assert(ed.buf.dirty);

editor_free(&ed);
}

static void test_replace_all_different_length_is_undoable(void)
{
editor_t ed;
editor_init(&ed, 24, 80);
set_line(&ed, 0, "a cat and a cat");

size_t count = editor_replace_all(&ed, "cat", "elephant");
assert(count == 2);
assert(ed.buf.lines[0].len == 25);
assert(memcmp(ed.buf.lines[0].chars, "a elephant and a elephant", 25) == 0);

editor_undo(&ed);
assert(ed.buf.lines[0].len == 15);
assert(memcmp(ed.buf.lines[0].chars, "a cat and a cat", 15) == 0);

editor_redo(&ed);
assert(ed.buf.lines[0].len == 25);
assert(memcmp(ed.buf.lines[0].chars, "a elephant and a elephant", 25) == 0);

editor_free(&ed);
}

static void test_replace_all_no_matches(void)
{
editor_t ed;
editor_init(&ed, 24, 80);
set_line(&ed, 0, "hello world");

size_t count = editor_replace_all(&ed, "missing", "x");
assert(count == 0);
assert(ed.buf.lines[0].len == 11);
assert(memcmp(ed.buf.lines[0].chars, "hello world", 11) == 0);

editor_free(&ed);
}

static void test_replace_all_multiline_replacement(void)
{
editor_t ed;
editor_init(&ed, 24, 80);
set_line(&ed, 0, "one,two");

size_t count = editor_replace_all(&ed, ",", "\n");
assert(count == 1);
assert(ed.buf.num_lines == 2);
assert(memcmp(ed.buf.lines[0].chars, "one", 3) == 0);
assert(memcmp(ed.buf.lines[1].chars, "two", 3) == 0);

editor_free(&ed);
}

int main(void)
{
test_replace_all_same_length();
test_replace_all_different_length_is_undoable();
test_replace_all_no_matches();
test_replace_all_multiline_replacement();
return 0;
}
Loading