From 67d49f98b00d9489b47904a09406e7405b762f5e Mon Sep 17 00:00:00 2001 From: AnmiTaliDev Date: Thu, 9 Jul 2026 14:12:29 +0500 Subject: [PATCH] feat: Highlight all search matches Add search_find_all() to collect every occurrence of the query, underline them all on screen while the Find prompt is open, and show a match count next to it. Document the Ctrl-F next-match behavior in the README. Closes #9 --- README.md | 2 +- src/editor.c | 14 ++++++++++++ src/editor.h | 6 ++++++ src/input.c | 8 +++++++ src/render.c | 28 ++++++++++++++++++++++++ src/search.c | 35 ++++++++++++++++++++++++++++++ src/search.h | 7 ++++++ tests/test_search.c | 52 ++++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 150 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f5405b..b8df854 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If no file is given, the editor opens an empty, unnamed buffer. | Tab / Shift-Tab | Insert tab / indent or dedent the selection | | Ctrl-S | Save (prompts for a name if the file is new) | | Ctrl-Q | Quit (press again to discard unsaved changes) | -| Ctrl-F | Find (Enter confirms, Ctrl-F finds next, Esc cancels) | +| 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-Z / Ctrl-Y | Undo / redo | | Ctrl-C / Ctrl-X | Copy / cut the selection (also pushed to the system clipboard) | diff --git a/src/editor.c b/src/editor.c index 2a35de1..f0844f5 100644 --- a/src/editor.c +++ b/src/editor.c @@ -219,6 +219,7 @@ void editor_free(editor_t *ed) free(ed->undo_stack); free(ed->redo_stack); free(ed->clipboard); + free(ed->search_matches); buffer_free(&ed->buf); } @@ -239,6 +240,7 @@ void editor_open(editor_t *ed, const char *filename) ed->sel.active = false; clear_edit_stack(&ed->undo_stack, &ed->undo_count); clear_edit_stack(&ed->redo_stack, &ed->redo_count); + editor_clear_search_matches(ed); } void editor_update_screen_size(editor_t *ed, int screen_rows, int screen_cols) @@ -323,6 +325,18 @@ void editor_clear_selection(editor_t *ed) ed->sel.active = false; } +void editor_set_search_matches(editor_t *ed, search_match_t *matches, size_t count) +{ + free(ed->search_matches); + ed->search_matches = matches; + ed->search_match_count = count; +} + +void editor_clear_search_matches(editor_t *ed) +{ + editor_set_search_matches(ed, NULL, 0); +} + static void begin_or_extend_selection(editor_t *ed, bool extend) { if (extend) { diff --git a/src/editor.h b/src/editor.h index 437dfcd..55abc8d 100644 --- a/src/editor.h +++ b/src/editor.h @@ -9,6 +9,7 @@ #include #include "buffer.h" +#include "search.h" typedef enum { MODE_EDIT, @@ -51,6 +52,8 @@ typedef struct { size_t prompt_len; size_t search_start_cy; size_t search_start_cx; + search_match_t *search_matches; + size_t search_match_count; char status_msg[256]; const char *filetype; @@ -87,6 +90,9 @@ bool editor_has_selection(const editor_t *ed); void editor_selection_range(const editor_t *ed, size_t *r1, size_t *c1, size_t *r2, size_t *c2); void editor_clear_selection(editor_t *ed); +void editor_set_search_matches(editor_t *ed, search_match_t *matches, size_t count); +void editor_clear_search_matches(editor_t *ed); + void editor_move_left(editor_t *ed, bool extend); void editor_move_right(editor_t *ed, bool extend); void editor_move_up(editor_t *ed, bool extend); diff --git a/src/input.c b/src/input.c index 34dd4f4..91e4a6f 100644 --- a/src/input.c +++ b/src/input.c @@ -77,14 +77,20 @@ static void do_incremental_search(editor_t *ed) if (ed->prompt_len == 0) { ed->cy = ed->search_start_cy; ed->cx = ed->search_start_cx; + editor_clear_search_matches(ed); return; } + size_t row, col; if (search_find_forward(&ed->buf, ed->search_start_cy, ed->search_start_cx, ed->prompt_input, &row, &col)) { ed->cy = row; ed->cx = col; } + + size_t count; + search_match_t *matches = search_find_all(&ed->buf, ed->prompt_input, &count); + editor_set_search_matches(ed, matches, count); } static void confirm_prompt(editor_t *ed) @@ -92,6 +98,7 @@ static void confirm_prompt(editor_t *ed) switch (ed->mode) { case MODE_PROMPT_FIND: ed->mode = MODE_EDIT; + editor_clear_search_matches(ed); editor_set_status(ed, "Ln %zu, Col %zu", ed->cy + 1, ed->cx + 1); break; @@ -133,6 +140,7 @@ static void process_prompt_key(editor_t *ed, key_event_t key) if (ed->mode == MODE_PROMPT_FIND) { ed->cy = ed->search_start_cy; ed->cx = ed->search_start_cx; + editor_clear_search_matches(ed); } ed->mode = MODE_EDIT; editor_set_status(ed, "Cancelled"); diff --git a/src/render.c b/src/render.c index a0f151d..b73e30e 100644 --- a/src/render.c +++ b/src/render.c @@ -100,6 +100,17 @@ static void render_empty_gutter(abuf_t *ab, const editor_t *ed) abuf_append_str(ab, "~"); } +static bool byte_in_search_match(const editor_t *ed, size_t row, size_t byte_col) +{ + 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) { + return true; + } + } + return false; +} + static void render_text_row(abuf_t *ab, const editor_t *ed, size_t file_row) { const line_t *line = &ed->buf.lines[file_row]; @@ -118,19 +129,26 @@ 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; size_t i = 0; size_t rx = 0; int emitted = 0; bool hl_active = false; + bool match_active = false; while (i < line->len && emitted < text_cols) { unsigned char c = (unsigned char)line->chars[i]; bool should_hl = row_has_hl && i >= hl_start && i < hl_end; + bool should_match = show_matches && byte_in_search_match(ed, file_row, i); if (should_hl != hl_active) { abuf_append_str(ab, should_hl ? "\x1b[7m" : "\x1b[27m"); hl_active = should_hl; } + if (should_match != match_active) { + abuf_append_str(ab, should_match ? "\x1b[4m" : "\x1b[24m"); + match_active = should_match; + } if (c == '\t') { size_t width = DAF_TAB_STOP - (rx % DAF_TAB_STOP); @@ -159,6 +177,9 @@ static void render_text_row(abuf_t *ab, const editor_t *ed, size_t file_row) if (hl_active) { abuf_append_str(ab, "\x1b[27m"); } + if (match_active) { + abuf_append_str(ab, "\x1b[24m"); + } } static void render_status_bar(abuf_t *ab, const editor_t *ed) @@ -204,6 +225,13 @@ 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) { + char count_buf[32]; + int n = snprintf(count_buf, sizeof(count_buf), " (%zu found)", ed->search_match_count); + if (n > 0) { + abuf_append(ab, count_buf, (size_t)n); + } + } } else { size_t len = strlen(ed->status_msg); size_t max_len = ed->screen_cols > 0 ? (size_t)ed->screen_cols : 0; diff --git a/src/search.c b/src/search.c index 42f2e47..4da4878 100644 --- a/src/search.c +++ b/src/search.c @@ -6,6 +6,8 @@ #include +#include "common.h" + static long find_substring(const char *hay, size_t hay_len, const char *needle, size_t needle_len, size_t from) { @@ -54,3 +56,36 @@ bool search_find_forward(const buffer_t *buf, size_t start_row, size_t start_col return false; } + +search_match_t *search_find_all(const buffer_t *buf, const char *query, size_t *out_count) +{ + size_t query_len = strlen(query); + search_match_t *matches = NULL; + size_t count = 0; + size_t cap = 0; + + if (query_len > 0) { + for (size_t row = 0; row < buf->num_lines; row++) { + const line_t *line = &buf->lines[row]; + size_t from = 0; + for (;;) { + long pos = find_substring(line->chars, line->len, query, query_len, from); + if (pos < 0) { + break; + } + if (count == cap) { + size_t new_cap = cap == 0 ? 16 : cap * 2; + matches = xrealloc(matches, new_cap * sizeof(search_match_t)); + cap = new_cap; + } + matches[count].row = row; + matches[count].col = (size_t)pos; + count++; + from = (size_t)pos + query_len; + } + } + } + + *out_count = count; + return matches; +} diff --git a/src/search.h b/src/search.h index 7dcbbbf..c6bc711 100644 --- a/src/search.h +++ b/src/search.h @@ -10,7 +10,14 @@ #include "buffer.h" +typedef struct { + size_t row; + size_t col; +} search_match_t; + bool search_find_forward(const buffer_t *buf, size_t start_row, size_t start_col, const char *query, size_t *out_row, size_t *out_col); +search_match_t *search_find_all(const buffer_t *buf, const char *query, size_t *out_count); + #endif /* DAF_SEARCH_H */ diff --git a/tests/test_search.c b/tests/test_search.c index 5e32260..3eab9e5 100644 --- a/tests/test_search.c +++ b/tests/test_search.c @@ -3,11 +3,12 @@ */ #include +#include #include "buffer.h" #include "search.h" -int main(void) +static void test_find_forward(void) { buffer_t buf; buffer_init(&buf); @@ -31,5 +32,54 @@ int main(void) assert(!search_find_forward(&buf, 0, 0, "", &row, &col)); buffer_free(&buf); +} + +static void test_find_all(void) +{ + buffer_t buf; + buffer_init(&buf); + buffer_insert_bytes(&buf, 0, 0, "foo bar", 7); + buffer_split_line(&buf, 0, 7); + buffer_insert_bytes(&buf, 1, 0, "baz foo", 7); + + size_t count = 0; + search_match_t *matches = search_find_all(&buf, "foo", &count); + assert(count == 2); + assert(matches[0].row == 0 && matches[0].col == 0); + assert(matches[1].row == 1 && matches[1].col == 4); + free(matches); + + matches = search_find_all(&buf, "missing", &count); + assert(count == 0); + assert(matches == NULL); + + matches = search_find_all(&buf, "", &count); + assert(count == 0); + assert(matches == NULL); + + buffer_free(&buf); +} + +static void test_find_all_non_overlapping(void) +{ + buffer_t buf; + buffer_init(&buf); + buffer_insert_bytes(&buf, 0, 0, "aaaa", 4); + + size_t count = 0; + search_match_t *matches = search_find_all(&buf, "aa", &count); + assert(count == 2); + assert(matches[0].col == 0); + assert(matches[1].col == 2); + free(matches); + + buffer_free(&buf); +} + +int main(void) +{ + test_find_forward(); + test_find_all(); + test_find_all_non_overlapping(); return 0; }