diff options
Diffstat (limited to 'src/main/search-replace.c')
| -rw-r--r-- | src/main/search-replace.c | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/src/main/search-replace.c b/src/main/search-replace.c new file mode 100644 index 0000000..828ce32 --- /dev/null +++ b/src/main/search-replace.c @@ -0,0 +1,365 @@ +#include <stdbool.h> +#include <stdint.h> +#include <string.h> + +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/command.h" +#include "dged/minibuffer.h" +#include "dged/window.h" + +#include "bindings.h" +#include "search-replace.h" + +static struct replace { + char *replace; + struct match *matches; + uint32_t nmatches; + uint32_t current_match; +} g_current_replace = {0}; + +void abort_replace() { + reset_minibuffer_keys(minibuffer_buffer()); + free(g_current_replace.matches); + free(g_current_replace.replace); + g_current_replace.matches = NULL; + g_current_replace.replace = NULL; + g_current_replace.nmatches = 0; + minibuffer_abort_prompt(); +} + +uint64_t matchdist(struct match *match, struct buffer_location loc) { + struct buffer_location begin = match->begin; + + int64_t linedist = (int64_t)begin.line - (int64_t)loc.line; + int64_t coldist = (int64_t)begin.col - (int64_t)loc.col; + + // arbitrary row scaling, best effort to avoid counting line length + return (linedist * linedist) * 1e6 + coldist * coldist; +} + +int buffer_loc_cmp(struct buffer_location loc1, struct buffer_location loc2) { + if (loc1.line < loc2.line) { + return -1; + } else if (loc1.line > loc2.line) { + return 1; + } else { + if (loc1.col < loc2.col) { + return -1; + } else if (loc1.col > loc2.col) { + return 1; + } else { + return 0; + } + } +} + +static void highlight_matches(struct buffer *buffer, struct match *matches, + uint32_t nmatches, uint32_t current) { + for (uint32_t matchi = 0; matchi < nmatches; ++matchi) { + struct match *m = &matches[matchi]; + if (matchi == current) { + buffer_add_text_property( + buffer, m->begin, m->end, + (struct text_property){.type = TextProperty_Colors, + .colors = (struct text_property_colors){ + .set_bg = true, + .bg = 3, + .set_fg = true, + .fg = 0, + }}); + + } else { + buffer_add_text_property( + buffer, m->begin, m->end, + (struct text_property){.type = TextProperty_Colors, + .colors = (struct text_property_colors){ + .set_bg = true, + .bg = 6, + .set_fg = true, + .fg = 0, + }}); + } + } +} + +static int32_t replace_next(struct command_ctx ctx, int argc, + const char *argv[]) { + struct replace *state = &g_current_replace; + struct buffer_view *buffer_view = window_buffer_view(windows_get_active()); + + struct match *m = &state->matches[state->current_match]; + buffer_set_mark_at(buffer_view, m->begin.line, m->begin.col); + buffer_goto(buffer_view, m->end.line, m->end.col + 1); + buffer_add_text(buffer_view, state->replace, strlen(state->replace)); + + ++state->current_match; + + if (state->current_match == state->nmatches) { + abort_replace(); + } else { + m = &state->matches[state->current_match]; + buffer_goto(buffer_view, m->begin.line, m->begin.col); + highlight_matches(buffer_view->buffer, state->matches, state->nmatches, + state->current_match); + } + + return 0; +} + +static int32_t skip_next(struct command_ctx ctx, int argc, const char *argv[]) { + struct replace *state = &g_current_replace; + + struct buffer_view *buffer_view = window_buffer_view(windows_get_active()); + struct match *m = &state->matches[state->current_match]; + buffer_goto(buffer_view, m->end.line, m->end.col + 1); + + ++state->current_match; + + if (state->current_match == state->nmatches) { + abort_replace(); + } else { + m = &state->matches[state->current_match]; + buffer_goto(buffer_view, m->begin.line, m->begin.col); + highlight_matches(buffer_view->buffer, state->matches, state->nmatches, + state->current_match); + } + + return 0; +} + +COMMAND_FN("replace-next", replace_next, replace_next, NULL); +COMMAND_FN("skip-next", skip_next, skip_next, NULL); + +static int cmp_matches(const void *m1, const void *m2) { + struct match *match1 = (struct match *)m1; + struct match *match2 = (struct match *)m2; + struct buffer_location dot = window_buffer_view(windows_get_active())->dot; + uint64_t dist1 = matchdist(match1, dot); + uint64_t dist2 = matchdist(match2, dot); + + int loc1 = buffer_loc_cmp(match1->begin, dot); + int loc2 = buffer_loc_cmp(match2->begin, dot); + + int64_t score1 = dist1 * loc1; + int64_t score2 = dist2 * loc2; + + if (score1 > 0 && score2 > 0) { + return score1 < score2 ? -1 : score1 > score2 ? 1 : 0; + } else if (score1 < 0 && score2 > 0) { + return 1; + } else if (score1 > 0 && score2 < 0) { + return -1; + } else { // both matches are behind dot + return score1 < score2 ? 1 : score1 > score2 ? -1 : 0; + } +} + +static int32_t replace(struct command_ctx ctx, int argc, const char *argv[]) { + if (argc == 0) { + return minibuffer_prompt(ctx, "find: "); + } + + if (argc == 1) { + command_ctx_push_arg(&ctx, argv[0]); + return minibuffer_prompt(ctx, "replace with: "); + } + + struct buffer_view *buffer_view = window_buffer_view(windows_get_active()); + struct match *matches = NULL; + uint32_t nmatches = 0; + buffer_find(buffer_view->buffer, argv[0], &matches, &nmatches); + + if (nmatches == 0) { + minibuffer_echo_timeout(4, "%s not found", argv[0]); + free(matches); + return 0; + } + + // sort matches + qsort(matches, nmatches, sizeof(struct match), cmp_matches); + + g_current_replace = (struct replace){ + .replace = strdup(argv[1]), + .matches = matches, + .nmatches = nmatches, + .current_match = 0, + }; + + struct match *m = &g_current_replace.matches[0]; + buffer_goto(buffer_view, m->begin.line, m->begin.col); + highlight_matches(buffer_view->buffer, g_current_replace.matches, + g_current_replace.nmatches, 0); + + struct binding bindings[] = { + ANONYMOUS_BINDING(None, 'y', &replace_next_command), + ANONYMOUS_BINDING(None, 'n', &skip_next_command), + ANONYMOUS_BINDING(Ctrl, 'M', &replace_next_command), + }; + buffer_bind_keys(minibuffer_buffer(), bindings, + sizeof(bindings) / sizeof(bindings[0])); + + return minibuffer_prompt(ctx, "replace? [yn] "); +} + +static char *g_last_search = NULL; + +const char *search_prompt(bool reverse) { + const char *txt = "search (down): "; + if (reverse) { + txt = "search (up): "; + } + + return txt; +} + +struct closest_match { + bool found; + struct match closest; +}; + +static struct closest_match find_closest(struct buffer_view *view, + const char *pattern, bool highlight, + bool reverse) { + struct match *matches = NULL; + uint32_t nmatches = 0; + struct closest_match res = { + .found = false, + .closest = {0}, + }; + + buffer_find(view->buffer, pattern, &matches, &nmatches); + + // find the "nearest" match + if (nmatches > 0) { + res.found = true; + struct match *closest = &matches[0]; + int64_t closest_dist = INT64_MAX; + for (uint32_t matchi = 0; matchi < nmatches; ++matchi) { + struct match *m = &matches[matchi]; + + if (highlight) { + buffer_add_text_property( + view->buffer, m->begin, m->end, + (struct text_property){.type = TextProperty_Colors, + .colors = (struct text_property_colors){ + .set_bg = true, + .bg = 6, + .set_fg = true, + .fg = 0, + }}); + } + int res = buffer_loc_cmp(m->begin, view->dot); + uint64_t dist = matchdist(m, view->dot); + if (((res < 0 && reverse) || (res > 0 && !reverse)) && + dist < closest_dist) { + closest_dist = dist; + closest = m; + } + } + + if (highlight) { + buffer_add_text_property( + view->buffer, closest->begin, closest->end, + (struct text_property){.type = TextProperty_Colors, + .colors = (struct text_property_colors){ + .set_bg = true, + .bg = 3, + .set_fg = true, + .fg = 0, + }}); + } + res.closest = *closest; + } + + free(matches); + return res; +} + +void do_search(struct buffer_view *view, const char *pattern, bool reverse) { + g_last_search = strdup(pattern); + + struct buffer_view *buffer_view = window_buffer_view(windows_get_active()); + struct closest_match m = find_closest(buffer_view, pattern, true, reverse); + + // find the "nearest" match + if (m.found) { + buffer_goto(buffer_view, m.closest.begin.line, m.closest.begin.col); + } else { + minibuffer_echo_timeout(4, "%s not found", pattern); + } +} + +int32_t search_interactive(struct command_ctx ctx, int argc, + const char *argv[]) { + const char *pattern = NULL; + if (minibuffer_content().nbytes == 0) { + // recall the last search, if any + if (g_last_search != NULL) { + struct buffer_view *view = window_buffer_view(minibuffer_window()); + buffer_clear(view); + buffer_add_text(view, (uint8_t *)g_last_search, strlen(g_last_search)); + pattern = g_last_search; + } + } else { + struct text_chunk content = minibuffer_content(); + char *p = malloc(content.nbytes + 1); + strncpy(p, content.text, content.nbytes); + p[content.nbytes] = '\0'; + pattern = p; + } + + minibuffer_set_prompt(search_prompt(*(bool *)ctx.userdata)); + + if (pattern != NULL) { + // ctx.active_window would be the minibuffer window + do_search(window_buffer_view(windows_get_active()), pattern, + *(bool *)ctx.userdata); + free((char *)pattern); + } + return 0; +} + +static bool search_dir_backward = true; +static bool search_dir_forward = false; + +COMMAND_FN("search-forward", search_forward, search_interactive, + &search_dir_forward); +COMMAND_FN("search-backward", search_backward, search_interactive, + &search_dir_backward); + +int32_t find(struct command_ctx ctx, int argc, const char *argv[]) { + bool reverse = strcmp((char *)ctx.userdata, "backward") == 0; + if (argc == 0) { + struct binding bindings[] = { + ANONYMOUS_BINDING(Ctrl, 'S', &search_forward_command), + ANONYMOUS_BINDING(Ctrl, 'R', &search_backward_command), + }; + buffer_bind_keys(minibuffer_buffer(), bindings, + sizeof(bindings) / sizeof(bindings[0])); + return minibuffer_prompt(ctx, search_prompt(reverse)); + } + + reset_minibuffer_keys(minibuffer_buffer()); + struct text_chunk content = minibuffer_content(); + char *pattern = malloc(content.nbytes + 1); + strncpy(pattern, content.text, content.nbytes); + pattern[content.nbytes] = '\0'; + + do_search(window_buffer_view(ctx.active_window), pattern, reverse); + free(pattern); + + return 0; +} + +void register_search_replace_commands(struct commands *commands) { + struct command search_replace_commands[] = { + {.name = "find-next", .fn = find, .userdata = "forward"}, + {.name = "find-prev", .fn = find, .userdata = "backward"}, + {.name = "replace", .fn = replace}, + }; + + register_commands(commands, search_replace_commands, + sizeof(search_replace_commands) / + sizeof(search_replace_commands[0])); +} |
