From 3a8ae83aa13636679c151027cace905fa87ebd8e Mon Sep 17 00:00:00 2001 From: Albert Cervin Date: Wed, 14 Jun 2023 00:03:47 +0200 Subject: Implement replace + autocomplete Autocomplete is currently a POC and works only with find-file. --- src/dged/buffer.c | 197 +++++++++++++++------- src/dged/buffer.h | 44 ++++- src/dged/display.c | 133 ++++++++------- src/dged/minibuffer.c | 19 ++- src/dged/minibuffer.h | 4 +- src/dged/path.h | 4 +- src/dged/window.c | 73 ++++++++- src/dged/window.h | 8 + src/main/cmds.c | 403 ++++++++++++++++++++++++++++++++-------------- src/main/search-replace.c | 365 +++++++++++++++++++++++++++++++++++++++++ src/main/search-replace.h | 14 ++ 11 files changed, 1007 insertions(+), 257 deletions(-) create mode 100644 src/main/search-replace.c create mode 100644 src/main/search-replace.h (limited to 'src') diff --git a/src/dged/buffer.c b/src/dged/buffer.c index 39a1422..c826401 100644 --- a/src/dged/buffer.c +++ b/src/dged/buffer.c @@ -126,10 +126,13 @@ struct buffer create_internal(char *name, char *filename) { .text = text_create(10), .modified = false, .readonly = false, - .lang = lang_from_id("fnd"), + .lang = + filename != NULL ? lang_from_filename(filename) : lang_from_id("fnd"), .last_write = {0}, }; + VEC_INIT(&b.text_properties, 32); + undo_init(&b.undo, 100); return b; @@ -147,6 +150,7 @@ struct buffer buffer_create(char *name) { } void buffer_destroy(struct buffer *buffer) { + VEC_DESTROY(&buffer->text_properties); text_destroy(buffer->text); buffer->text = NULL; @@ -162,6 +166,7 @@ void buffer_destroy(struct buffer *buffer) { void buffer_clear(struct buffer_view *view) { text_clear(view->buffer->text); view->dot.col = view->dot.line = 0; + view->scroll.col = view->scroll.line = 0; } void buffer_static_init() { @@ -512,7 +517,7 @@ void buffer_beginning_of_line(struct buffer_view *view) { view->dot.col = 0; } void buffer_read_from_file(struct buffer *b) { struct stat sb; - char *fullname = expanduser(b->filename); + char *fullname = to_abspath(b->filename); if (stat(fullname, &sb) == 0) { FILE *file = fopen(fullname, "r"); free(fullname); @@ -546,7 +551,6 @@ void buffer_read_from_file(struct buffer *b) { return; } - b->lang = lang_from_filename(b->filename); undo_push_boundary(&b->undo, (struct undo_boundary){.save_point = true}); } @@ -643,7 +647,7 @@ void search_line(struct text_chunk *chunk, void *userdata) { uint32_t pattern_nchars = utf8_nchars((uint8_t *)data->pattern, pattern_len); char *line = malloc(chunk->nbytes + 1); - memcpy(line, chunk->text, chunk->nbytes); + strncpy(line, chunk->text, chunk->nbytes); line[chunk->nbytes] = '\0'; char *hit = NULL; uint32_t byteidx = 0; @@ -652,7 +656,7 @@ void search_line(struct text_chunk *chunk, void *userdata) { uint32_t begin = utf8_nchars(chunk->text, byteidx); struct match match = (struct match){ .begin = {.col = begin, .line = chunk->line}, - .end = {.col = begin + pattern_nchars, .line = chunk->line}, + .end = {.col = begin + pattern_nchars - 1, .line = chunk->line}, }; VEC_PUSH(&data->matches, match); @@ -660,6 +664,8 @@ void search_line(struct text_chunk *chunk, void *userdata) { // proceed to after match byteidx += pattern_len; } + + free(line); } void buffer_find(struct buffer *buffer, const char *pattern, @@ -832,6 +838,8 @@ struct cmdbuf { struct line_render_hook *line_render_hooks; uint32_t nlinerender_hooks; + + struct buffer *buffer; }; static uint32_t visual_char_width(uint8_t *byte, uint32_t maxlen) { @@ -855,41 +863,6 @@ static uint32_t visual_string_width(uint8_t *txt, uint32_t len, return width; } -static bool is_in_mark(struct buffer_location mark_begin, - struct buffer_location mark_end, - struct buffer_location pos) { - if (mark_begin.line == mark_end.line && mark_begin.col == mark_end.col) { - return false; - } - - if (pos.line >= mark_begin.line && pos.line <= mark_end.line) { - if (pos.line == mark_end.line && pos.col <= mark_end.col && - pos.line == mark_begin.line && pos.col >= mark_begin.col) { - // only one line is marked - return true; - } else if (pos.line == mark_begin.line && pos.line != mark_end.line && - pos.col >= mark_begin.col) { - // we are on the first line marked - return true; - } else if (pos.line == mark_end.line && pos.line != mark_begin.line && - pos.col <= mark_end.col) { - // we are on the last line marked - return true; - } else if (pos.line != mark_begin.line && pos.line != mark_end.line) { - // we are on fully marked lines - return true; - } - } - - return false; -} - -// TODO: temporary -enum property { - Prop_None, - Prop_Marked, -}; - void render_line(struct text_chunk *line, void *userdata) { struct cmdbuf *cmdbuf = (struct cmdbuf *)userdata; uint32_t visual_line = line->line - cmdbuf->scroll.line + cmdbuf->line_offset; @@ -913,7 +886,9 @@ void render_line(struct text_chunk *line, void *userdata) { uint32_t visual_col_start = cmdbuf->left_margin; uint32_t cur_visual_col = visual_col_start; uint32_t start_byte = 0, text_nbytes = 0; - enum property curprop = Prop_None; + struct text_property *properties[16] = {0}; + struct text_property *prev_properties[16] = {0}; + uint32_t prev_nproperties; for (uint32_t cur_byte = start_byte, coli = 0; cur_byte < text_nbytes_scroll && cur_visual_col < cmdbuf->width && coli < line->nchars - cmdbuf->scroll.col; @@ -924,28 +899,56 @@ void render_line(struct text_chunk *line, void *userdata) { uint32_t char_vwidth = visual_char_width(text + cur_byte, bytes_remaining); // calculate character properties - enum property prevprop = curprop; - curprop = cmdbuf->mark_set && - is_in_mark(*begin, *end, - (struct buffer_location){.col = coli, - .line = line->line}) - ? Prop_Marked - : Prop_None; + uint32_t nproperties = 0; + buffer_get_text_properties( + cmdbuf->buffer, + (struct buffer_location){.line = line->line, .col = coli}, properties, + 16, &nproperties); // handle changes to properties - if (curprop != prevprop) { + uint32_t nnew_props = 0; + struct text_property *new_props[16] = {0}; + for (uint32_t propi = 0; propi < nproperties; ++propi) { + if (propi >= prev_nproperties || + prev_properties[propi] != properties[propi]) { + new_props[nnew_props] = properties[propi]; + ++nnew_props; + } + } + + // if we have any new or lost props, flush text up until now + if (nnew_props > 0 || nproperties < prev_nproperties) { command_list_draw_text(cmdbuf->cmds, visual_col_start, visual_line, text + start_byte, cur_byte - start_byte); visual_col_start = cur_visual_col; start_byte = cur_byte; } - if (curprop == Prop_Marked && prevprop == Prop_None) { - command_list_set_index_color_bg(cmdbuf->cmds, 5); - } else if (curprop == Prop_None && curprop != prevprop) { + // apply new properties + for (uint32_t propi = 0; propi < nnew_props; ++propi) { + struct text_property *prop = new_props[propi]; + switch (prop->type) { + case TextProperty_Colors: + struct text_property_colors *colors = &prop->colors; + if (colors->set_bg) { + command_list_set_index_color_bg(cmdbuf->cmds, colors->bg); + } + + if (colors->set_fg) { + command_list_set_index_color_fg(cmdbuf->cmds, colors->fg); + } + break; + } + } + + if (nproperties == 0 && prev_nproperties > 0) { command_list_reset_color(cmdbuf->cmds); } + memcpy(prev_properties, properties, + nproperties * sizeof(struct text_property *)); + prev_nproperties = nproperties; + cur_byte += char_nbytes; text_nbytes += char_nbytes; cur_visual_col += char_vwidth; @@ -1044,9 +1047,9 @@ void linenum_render_hook(struct text_chunk *line_data, uint32_t line, struct command_list *commands, void *userdata) { struct linenumdata *data = (struct linenumdata *)userdata; static char buf[16]; - command_list_set_index_color_bg(commands, 236); - command_list_set_index_color_fg( - commands, line_data->line == data->dot_line ? 253 : 244); + command_list_set_index_color_bg(commands, 8); + command_list_set_index_color_fg(commands, + line_data->line == data->dot_line ? 15 : 7); uint32_t chars = snprintf(buf, 16, "%*d", data->longest_nchars + 1, line_data->line + 1); command_list_draw_text_copy(commands, 0, line, (uint8_t *)buf, chars); @@ -1157,6 +1160,20 @@ void buffer_update(struct buffer_view *view, uint32_t window_id, uint32_t width, struct setting *show_ws = settings_get("editor.show-whitespace"); + if (buffer_region_has_size(view)) { + struct region reg = to_region(view->dot, view->mark); + buffer_add_text_property(view->buffer, reg.begin, reg.end, + (struct text_property){ + .type = TextProperty_Colors, + .colors = + (struct text_property_colors){ + .set_bg = true, + .bg = 5, + .set_fg = false, + }, + }); + } + struct cmdbuf cmdbuf = (struct cmdbuf){ .cmds = commands, .scroll = view->scroll, @@ -1168,6 +1185,7 @@ void buffer_update(struct buffer_view *view, uint32_t window_id, uint32_t width, .mark_set = view->mark_set, .region = to_region(view->dot, view->mark), .show_ws = show_ws != NULL ? show_ws->value.bool_value : true, + .buffer = view->buffer, }; text_for_each_line(view->buffer->text, view->scroll.line, height, render_line, &cmdbuf); @@ -1179,7 +1197,9 @@ void buffer_update(struct buffer_view *view, uint32_t window_id, uint32_t width, for (uint32_t hooki = 0; hooki < nlinehooks; ++hooki) { struct line_render_hook *hook = &line_hooks[hooki]; - hook->empty_callback(linei, commands, hook->userdata); + if (hook->empty_callback != NULL) { + hook->empty_callback(linei, commands, hook->userdata); + } } command_list_draw_repeated(commands, total_margins.left, linei, ' ', @@ -1189,13 +1209,14 @@ void buffer_update(struct buffer_view *view, uint32_t window_id, uint32_t width, // update the visual cursor position to_relative(view, view->dot.line, view->dot.col, &rel_line, &rel_col); uint32_t visual_col = visual_dot_col(view, view->dot.col); + // TODO: fix this shit, should not need to add scroll_col back here // only to subtract it in the function to_relative(view, view->dot.line, visual_col + view->scroll.col, &rel_line, &rel_col); - *relline = rel_line < 0 ? 0 : (uint32_t)rel_line + total_margins.top; - *relcol = rel_col < 0 ? 0 : (uint32_t)rel_col + total_margins.left; + *relline = (rel_line < 0 ? 0 : (uint32_t)rel_line) + total_margins.top; + *relcol = (rel_col < 0 ? 0 : (uint32_t)rel_col) + total_margins.left; } struct text_chunk buffer_get_line(struct buffer *buffer, uint32_t line) { @@ -1206,7 +1227,67 @@ void buffer_view_scroll_down(struct buffer_view *view, uint32_t height) { buffer_goto(view, view->dot.line + height, view->dot.col); scroll(view, height, 0); } + void buffer_view_scroll_up(struct buffer_view *view, uint32_t height) { buffer_goto(view, view->dot.line - height, view->dot.col); scroll(view, -height, 0); } + +void buffer_clear_text_properties(struct buffer *buffer) { + VEC_CLEAR(&buffer->text_properties); +} + +void buffer_add_text_property(struct buffer *buffer, + struct buffer_location start, + struct buffer_location end, + struct text_property property) { + struct text_property_entry entry = { + .start = start, + .end = end, + .property = property, + }; + VEC_PUSH(&buffer->text_properties, entry); +} + +bool buffer_location_is_between(struct buffer_location location, + struct buffer_location start, + struct buffer_location end) { + if (location.line >= start.line && location.line <= end.line) { + if (location.line == end.line && location.col <= end.col && + location.line == start.line && location.col >= start.col) { + // only one line + return true; + } else if (location.line == start.line && location.line != end.line && + location.col >= start.col) { + // we are on the first line + return true; + } else if (location.line == end.line && location.line != start.line && + location.col <= end.col) { + // we are on the last line + return true; + } else if (location.line != end.line && location.line != start.line) { + // we are on lines in between + return true; + } + } + return false; +} + +void buffer_get_text_properties(struct buffer *buffer, + struct buffer_location location, + struct text_property **properties, + uint32_t max_nproperties, + uint32_t *nproperties) { + uint32_t nres = 0; + VEC_FOR_EACH(&buffer->text_properties, struct text_property_entry * prop) { + if (buffer_location_is_between(location, prop->start, prop->end)) { + properties[nres] = &prop->property; + ++nres; + + if (nres == max_nproperties) { + break; + } + } + } + *nproperties = nres; +} diff --git a/src/dged/buffer.h b/src/dged/buffer.h index dad6ef1..28d9797 100644 --- a/src/dged/buffer.h +++ b/src/dged/buffer.h @@ -3,7 +3,6 @@ #include #include -#include "bits/stdint-uintn.h" #include "command.h" #include "lang.h" #include "text.h" @@ -13,6 +12,24 @@ struct keymap; struct command_list; +enum text_property_type { + TextProperty_Colors, +}; + +struct text_property_colors { + bool set_fg; + uint32_t fg; + bool set_bg; + uint32_t bg; +}; + +struct text_property { + enum text_property_type type; + union { + struct text_property_colors colors; + }; +}; + /** * Margins where buffer text should not be */ @@ -90,6 +107,10 @@ struct buffer_location { uint32_t col; }; +bool buffer_location_is_between(struct buffer_location location, + struct buffer_location start, + struct buffer_location end); + struct match { struct buffer_location begin; struct buffer_location end; @@ -125,6 +146,12 @@ void buffer_view_scroll_up(struct buffer_view *view, uint32_t height); void buffer_view_destroy(struct buffer_view *view); +struct text_property_entry { + struct buffer_location start; + struct buffer_location end; + struct text_property property; +}; + /** * A buffer of text that can be modified, read from and written to disk. * @@ -158,6 +185,8 @@ struct buffer { /** Buffer programming language */ struct language lang; + + VEC(struct text_property_entry) text_properties; }; struct buffer buffer_create(char *name); @@ -209,6 +238,19 @@ void buffer_paste(struct buffer_view *view); void buffer_paste_older(struct buffer_view *view); void buffer_cut(struct buffer_view *view); +void buffer_clear_text_properties(struct buffer *buffer); + +void buffer_add_text_property(struct buffer *buffer, + struct buffer_location start, + struct buffer_location end, + struct text_property property); + +void buffer_get_text_properties(struct buffer *buffer, + struct buffer_location location, + struct text_property **properties, + uint32_t max_nproperties, + uint32_t *nproperties); + struct text_chunk buffer_get_line(struct buffer *buffer, uint32_t line); uint32_t buffer_add_update_hook(struct buffer *buffer, update_hook_cb hook, diff --git a/src/dged/display.c b/src/dged/display.c index d9eeb11..cf2e5d5 100644 --- a/src/dged/display.c +++ b/src/dged/display.c @@ -3,6 +3,7 @@ #include "buffer.h" +#include #include #include #include @@ -73,6 +74,8 @@ struct command_list { void *(*allocator)(size_t); char name[16]; + + struct command_list *next_list; }; struct winsize getsize() { @@ -169,19 +172,21 @@ void display_clear(struct display *display) { putbytes(bytes, 3, false); } -struct command_list *command_list_create(uint32_t capacity, +struct command_list *command_list_create(uint32_t initial_capacity, void *(*allocator)(size_t), uint32_t xoffset, uint32_t yoffset, const char *name) { struct command_list *command_list = allocator(sizeof(struct command_list)); - command_list->capacity = capacity; + command_list->capacity = initial_capacity; command_list->ncmds = 0; command_list->xoffset = xoffset; command_list->yoffset = yoffset; + command_list->next_list = NULL; strncpy(command_list->name, name, 15); - command_list->cmds = allocator(sizeof(struct render_command) * capacity); + command_list->cmds = + allocator(sizeof(struct render_command) * initial_capacity); command_list->allocator = allocator; return command_list; @@ -189,33 +194,41 @@ struct command_list *command_list_create(uint32_t capacity, struct render_command *add_command(struct command_list *list, enum render_cmd_type tp) { - if (list->ncmds == list->capacity) { - /* TODO: better. Currently a bit tricky to provide dynamic scaling of this - * since it is initially allocated with the frame allocator that does not - * support realloc. - */ - return NULL; + struct command_list *l = list; + struct command_list *n = l->next_list; + + // scan through lists for one with capacity + while (l->ncmds == l->capacity && n != NULL) { + l = n; + n = l->next_list; + } + + if (l->ncmds == l->capacity && n == NULL) { + l->next_list = command_list_create(l->capacity, l->allocator, l->xoffset, + l->yoffset, l->name); + l = l->next_list; } - struct render_command *cmd = &list->cmds[list->ncmds]; + struct render_command *cmd = &l->cmds[l->ncmds]; cmd->type = tp; switch (tp) { case RenderCommand_DrawText: - cmd->draw_txt = list->allocator(sizeof(struct draw_text_cmd)); + cmd->draw_txt = l->allocator(sizeof(struct draw_text_cmd)); break; case RenderCommand_Repeat: - cmd->repeat = list->allocator(sizeof(struct repeat_cmd)); + cmd->repeat = l->allocator(sizeof(struct repeat_cmd)); break; case RenderCommand_PushFormat: - cmd->push_fmt = list->allocator(sizeof(struct push_fmt_cmd)); + cmd->push_fmt = l->allocator(sizeof(struct push_fmt_cmd)); break; case RenderCommand_SetShowWhitespace: - cmd->show_ws = list->allocator(sizeof(struct show_ws_cmd)); + cmd->show_ws = l->allocator(sizeof(struct show_ws_cmd)); break; case RenderCommand_ClearFormat: break; } - ++list->ncmds; + + ++l->ncmds; return cmd; } @@ -306,58 +319,62 @@ void display_render(struct display *display, uint32_t fmt_stack_len = 3; bool show_whitespace_state = false; - for (uint64_t cmdi = 0; cmdi < cl->ncmds; ++cmdi) { - struct render_command *cmd = &cl->cmds[cmdi]; - switch (cmd->type) { - case RenderCommand_DrawText: { - struct draw_text_cmd *txt_cmd = cmd->draw_txt; - display_move_cursor(display, txt_cmd->row + cl->yoffset, - txt_cmd->col + cl->xoffset); - putbytes(fmt_stack, fmt_stack_len, false); - putbyte('m'); - putbytes(txt_cmd->data, txt_cmd->len, show_whitespace_state); - break; - } + while (cl != NULL) { + + for (uint64_t cmdi = 0; cmdi < cl->ncmds; ++cmdi) { + struct render_command *cmd = &cl->cmds[cmdi]; + switch (cmd->type) { + case RenderCommand_DrawText: { + struct draw_text_cmd *txt_cmd = cmd->draw_txt; + display_move_cursor(display, txt_cmd->row + cl->yoffset, + txt_cmd->col + cl->xoffset); + putbytes(fmt_stack, fmt_stack_len, false); + putbyte('m'); + putbytes(txt_cmd->data, txt_cmd->len, show_whitespace_state); + break; + } - case RenderCommand_Repeat: { - struct repeat_cmd *repeat_cmd = cmd->repeat; - display_move_cursor(display, repeat_cmd->row + cl->yoffset, - repeat_cmd->col + cl->xoffset); - putbytes(fmt_stack, fmt_stack_len, false); - putbyte('m'); - if (show_whitespace_state) { - for (uint32_t i = 0; i < repeat_cmd->nrepeat; ++i) { - putbyte_ws(repeat_cmd->c, show_whitespace_state); + case RenderCommand_Repeat: { + struct repeat_cmd *repeat_cmd = cmd->repeat; + display_move_cursor(display, repeat_cmd->row + cl->yoffset, + repeat_cmd->col + cl->xoffset); + putbytes(fmt_stack, fmt_stack_len, false); + putbyte('m'); + if (show_whitespace_state) { + for (uint32_t i = 0; i < repeat_cmd->nrepeat; ++i) { + putbyte_ws(repeat_cmd->c, show_whitespace_state); + } + } else { + char *buf = malloc(repeat_cmd->nrepeat + 1); + memset(buf, repeat_cmd->c, repeat_cmd->nrepeat); + buf[repeat_cmd->nrepeat] = '\0'; + fputs(buf, stdout); + free(buf); } - } else { - char *buf = malloc(repeat_cmd->nrepeat + 1); - memset(buf, repeat_cmd->c, repeat_cmd->nrepeat); - buf[repeat_cmd->nrepeat] = '\0'; - fputs(buf, stdout); - free(buf); + break; } - break; - } - case RenderCommand_PushFormat: { - struct push_fmt_cmd *fmt_cmd = cmd->push_fmt; + case RenderCommand_PushFormat: { + struct push_fmt_cmd *fmt_cmd = cmd->push_fmt; - fmt_stack[fmt_stack_len] = ';'; - ++fmt_stack_len; + fmt_stack[fmt_stack_len] = ';'; + ++fmt_stack_len; - memcpy(fmt_stack + fmt_stack_len, fmt_cmd->fmt, fmt_cmd->len); - fmt_stack_len += fmt_cmd->len; - break; - } + memcpy(fmt_stack + fmt_stack_len, fmt_cmd->fmt, fmt_cmd->len); + fmt_stack_len += fmt_cmd->len; + break; + } - case RenderCommand_ClearFormat: - fmt_stack_len = 3; - break; + case RenderCommand_ClearFormat: + fmt_stack_len = 3; + break; - case RenderCommand_SetShowWhitespace: - show_whitespace_state = cmd->show_ws->show; - break; + case RenderCommand_SetShowWhitespace: + show_whitespace_state = cmd->show_ws->show; + break; + } } + cl = cl->next_list; } } diff --git a/src/dged/minibuffer.c b/src/dged/minibuffer.c index 0ff32a8..3c5a291 100644 --- a/src/dged/minibuffer.c +++ b/src/dged/minibuffer.c @@ -18,7 +18,8 @@ static struct minibuffer { bool prompt_active; bool clear; - void (*update_callback)(); + void (*update_callback)(void *); + void *update_callback_userdata; } g_minibuffer = {0}; @@ -88,7 +89,7 @@ struct update_hook_result update(struct buffer_view *view, } if (mb->update_callback != NULL) { - mb->update_callback(); + mb->update_callback(mb->update_callback_userdata); } return res; @@ -153,7 +154,8 @@ void minibuffer_set_prompt_internal(const char *fmt, va_list args) { } int32_t minibuffer_prompt_internal(struct command_ctx command_ctx, - void (*update_callback)(), const char *fmt, + void (*update_callback)(void *), + void *userdata, const char *fmt, va_list args) { if (g_minibuffer.buffer == NULL) { return 1; @@ -169,6 +171,7 @@ int32_t minibuffer_prompt_internal(struct command_ctx command_ctx, if (update_callback != NULL) { g_minibuffer.update_callback = update_callback; + g_minibuffer.update_callback_userdata = userdata; } return 0; @@ -178,19 +181,19 @@ int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, ...) { va_list args; va_start(args, fmt); - int32_t r = minibuffer_prompt_internal(command_ctx, NULL, fmt, args); + int32_t r = minibuffer_prompt_internal(command_ctx, NULL, NULL, fmt, args); va_end(args); return r; } int32_t minibuffer_prompt_interactive(struct command_ctx command_ctx, - void (*update_callback)(), - const char *fmt, ...) { + void (*update_callback)(void *), + void *userdata, const char *fmt, ...) { va_list args; va_start(args, fmt); - int32_t r = - minibuffer_prompt_internal(command_ctx, update_callback, fmt, args); + int32_t r = minibuffer_prompt_internal(command_ctx, update_callback, userdata, + fmt, args); va_end(args); return r; diff --git a/src/dged/minibuffer.h b/src/dged/minibuffer.h index 24f54cf..98a4db8 100644 --- a/src/dged/minibuffer.h +++ b/src/dged/minibuffer.h @@ -59,8 +59,8 @@ void minibuffer_echo_timeout(uint32_t timeout, const char *fmt, ...); int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, ...); int32_t minibuffer_prompt_interactive(struct command_ctx command_ctx, - void (*update_callback)(), - const char *fmt, ...); + void (*update_callback)(void *), + void *userdata, const char *fmt, ...); void minibuffer_set_prompt(const char *fmt, ...); diff --git a/src/dged/path.h b/src/dged/path.h index 6e11d6a..da62457 100644 --- a/src/dged/path.h +++ b/src/dged/path.h @@ -29,10 +29,12 @@ static char *expanduser(const char *path) { } static char *to_abspath(const char *path) { + char *exp = expanduser(path); char *p = realpath(path, NULL); if (p != NULL) { + free(exp); return p; } else { - return strdup(path); + return exp; } } diff --git a/src/dged/window.c b/src/dged/window.c index 5ea4085..e928e42 100644 --- a/src/dged/window.c +++ b/src/dged/window.c @@ -36,6 +36,9 @@ static struct windows { static struct window g_minibuffer_window; +static struct window g_popup_window = {0}; +static bool g_popup_visible = false; + void windows_init(uint32_t height, uint32_t width, struct buffer *initial_buffer, struct buffer *minibuffer) { BINTREE_INIT(&g_windows.windows); @@ -88,6 +91,12 @@ struct window *minibuffer_window() { return &g_minibuffer_window; } +struct window *popup_window() { + return &g_popup_window; +} + +bool popup_window_visible() { return g_popup_visible; } + static void window_tree_resize(struct window_node *root, uint32_t height, uint32_t width) { @@ -145,6 +154,21 @@ void windows_resize(uint32_t height, uint32_t width) { } void windows_update(void *(*frame_alloc)(size_t), uint64_t frame_time) { + + struct window *w = &g_minibuffer_window; + w->commands = command_list_create(w->height * w->width, frame_alloc, w->x, + w->y, w->buffer_view.buffer->name); + buffer_update(&w->buffer_view, -1, w->width, w->height, w->commands, + frame_time, &w->relline, &w->relcol); + + if (g_popup_visible) { + w = &g_popup_window; + w->commands = command_list_create(w->height * w->width, frame_alloc, w->x, + w->y, w->buffer_view.buffer->name); + buffer_update(&w->buffer_view, -1, w->width, w->height, w->commands, + frame_time, &w->relline, &w->relcol); + } + struct window_node *n = BINTREE_ROOT(&g_windows.windows); BINTREE_FIRST(n); uint32_t window_id = 0; @@ -163,11 +187,23 @@ void windows_update(void *(*frame_alloc)(size_t), uint64_t frame_time) { BINTREE_NEXT(n); } - struct window *w = &g_minibuffer_window; - w->commands = command_list_create(w->height * w->width, frame_alloc, w->x, - w->y, w->buffer_view.buffer->name); - buffer_update(&w->buffer_view, -1, w->width, w->height, w->commands, - frame_time, &w->relline, &w->relcol); + // clear text props for next frame + n = BINTREE_ROOT(&g_windows.windows); + BINTREE_FIRST(n); + + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w->type == Window_Buffer) { + buffer_clear_text_properties(w->buffer_view.buffer); + } + + BINTREE_NEXT(n); + } + + buffer_clear_text_properties(g_minibuffer_window.buffer_view.buffer); + if (g_popup_visible) { + buffer_clear_text_properties(g_popup_window.buffer_view.buffer); + } } void windows_render(struct display *display) { @@ -182,6 +218,9 @@ void windows_render(struct display *display) { } display_render(display, g_minibuffer_window.commands); + if (g_popup_visible) { + display_render(display, g_popup_window.commands); + } } struct window_node *find_window(struct window *window) { @@ -224,9 +263,16 @@ struct window *windows_get_active() { } void window_set_buffer(struct window *window, struct buffer *buffer) { - window->prev_buffer = window->buffer_view.buffer; - buffer_view_destroy(&window->buffer_view); - window->buffer_view = buffer_view_create(buffer, true, true); + window_set_buffer_e(window, buffer, true, true); +} + +void window_set_buffer_e(struct window *window, struct buffer *buffer, + bool modeline, bool line_numbers) { + if (buffer != window->buffer_view.buffer) { + window->prev_buffer = window->buffer_view.buffer; + buffer_view_destroy(&window->buffer_view); + window->buffer_view = buffer_view_create(buffer, modeline, line_numbers); + } } struct buffer *window_buffer(struct window *window) { @@ -464,3 +510,14 @@ struct window *windows_focus(uint32_t id) { uint32_t window_width(struct window *window) { return window->width; } uint32_t window_height(struct window *window) { return window->height; } + +void windows_show_popup(uint32_t row, uint32_t col, uint32_t width, + uint32_t height) { + g_popup_window.x = col; + g_popup_window.y = row; + g_popup_window.width = width; + g_popup_window.height = height; + g_popup_visible = true; +} + +void windows_close_popup() { g_popup_visible = false; } diff --git a/src/dged/window.h b/src/dged/window.h index be9b952..30c1061 100644 --- a/src/dged/window.h +++ b/src/dged/window.h @@ -22,6 +22,8 @@ void windows_render(struct display *display); struct window *root_window(); struct window *minibuffer_window(); +struct window *popup_window(); +bool popup_window_visible(); void windows_set_active(struct window *window); struct window *windows_focus(uint32_t id); @@ -30,6 +32,8 @@ struct window *windows_focus_next(); struct window *window_find_by_buffer(struct buffer *b); void window_set_buffer(struct window *window, struct buffer *buffer); +void window_set_buffer_e(struct window *window, struct buffer *buffer, + bool modeline, bool line_numbers); struct buffer *window_buffer(struct window *window); struct buffer_view *window_buffer_view(struct window *window); struct buffer *window_prev_buffer(struct window *window); @@ -47,3 +51,7 @@ void window_hsplit(struct window *window, struct window **new_window_a, struct window **new_window_b); void window_vsplit(struct window *window, struct window **new_window_a, struct window **new_window_b); + +void windows_show_popup(uint32_t row, uint32_t col, uint32_t width, + uint32_t height); +void windows_close_popup(); diff --git a/src/main/cmds.c b/src/main/cmds.c index 82fddf0..ecce343 100644 --- a/src/main/cmds.c +++ b/src/main/cmds.c @@ -1,21 +1,32 @@ +#define _DEFAULT_SOURCE +#include #include +#include #include #include #include +#include #include "dged/binding.h" #include "dged/buffer.h" #include "dged/buffers.h" #include "dged/command.h" +#include "dged/display.h" #include "dged/minibuffer.h" #include "dged/path.h" #include "dged/settings.h" #include "bindings.h" +#include "search-replace.h" + +static void abort_completion(); int32_t _abort(struct command_ctx ctx, int argc, const char *argv[]) { + abort_replace(); + abort_completion(); minibuffer_abort_prompt(); buffer_clear_mark(window_buffer_view(ctx.active_window)); + reset_minibuffer_keys(minibuffer_buffer()); minibuffer_echo_timeout(4, "💣 aborted"); return 0; } @@ -31,12 +42,279 @@ int32_t exit_editor(struct command_ctx ctx, int argc, const char *argv[]) { return 0; } +struct completion { + const char *display; + const char *insert; + bool complete; +}; + +uint32_t g_ncompletions = 0; +struct completion g_completions[50] = {0}; + +static void abort_completion() { + if (!minibuffer_focused()) { + reset_buffer_keys(window_buffer(windows_get_active())); + } else { + reset_minibuffer_keys(minibuffer_buffer()); + } + windows_close_popup(); + + for (uint32_t compi = 0; compi < g_ncompletions; ++compi) { + free((void *)g_completions[compi].display); + free((void *)g_completions[compi].insert); + } + g_ncompletions = 0; +} + +int cmp_completions(const void *comp_a, const void *comp_b) { + struct completion *a = (struct completion *)comp_a; + struct completion *b = (struct completion *)comp_b; + return strcmp(a->display, b->display); +} + +static bool is_hidden(const char *filename) { + return filename[0] == '.' && filename[1] != '\0' && filename[1] != '.'; +} + +static void complete_path(const char *path, struct completion results[], + uint32_t nresults_max, uint32_t *nresults) { + uint32_t n = 0; + char *p1 = to_abspath(path); + size_t len = strlen(p1); + char *p2 = strdup(p1); + + if (len == 0) { + goto done; + } + + if (nresults_max == 0) { + goto done; + } + + const char *dir = p1; + const char *file = ""; + + if (dir[len - 1] != '/') { + dir = dirname(p1); + file = basename(p2); + } + + DIR *d = opendir(dir); + if (d == NULL) { + goto done; + } + + errno = 0; + while (n < nresults_max) { + struct dirent *de = readdir(d); + if (de == NULL && errno != 0) { + // skip the erroring entry + errno = 0; + continue; + } else if (de == NULL && errno == 0) { + break; + } + + switch (de->d_type) { + case DT_DIR: + case DT_REG: + case DT_LNK: + if (!is_hidden(de->d_name) && + (strncmp(file, de->d_name, strlen(file)) == 0 || strlen(file) == 0)) { + const char *disp = strdup(de->d_name); + results[n] = (struct completion){ + .display = disp, + .insert = strdup(disp + strlen(file)), + .complete = de->d_type == DT_REG, + }; + ++n; + } + break; + } + } + + closedir(d); + +done: + free(p1); + free(p2); + + qsort(results, n, sizeof(struct completion), cmp_completions); + *nresults = n; +} + +void render_completion_line(struct text_chunk *line_data, uint32_t line, + struct command_list *commands, void *userdata) { + command_list_set_show_whitespace(commands, false); + command_list_draw_repeated(commands, 0, line, ' ', 1); +} + +struct update_hook_result +update_completion_buffer(struct buffer_view *view, + struct command_list *commands, uint32_t width, + uint32_t height, uint64_t frame_time, void *userdata) { + struct text_chunk line = buffer_get_line(view->buffer, view->dot.line); + buffer_add_text_property( + view->buffer, (struct buffer_location){.line = view->dot.line, .col = 0}, + (struct buffer_location){.line = view->dot.line, .col = line.nchars}, + (struct text_property){.type = TextProperty_Colors, + .colors = (struct text_property_colors){ + .set_bg = false, + .bg = 0, + .set_fg = true, + .fg = 4, + }}); + + if (line.allocated) { + free(line.text); + } + + struct update_hook_result res = {0}; + res.margins.left = 1; + res.margins.right = 1; + res.line_render_hook = (struct line_render_hook){ + .callback = render_completion_line, + .empty_callback = NULL, + .userdata = NULL, + }; + + return res; +} + +static int32_t goto_completion(struct command_ctx ctx, int argc, + const char *argv[]) { + void (*movement_fn)(struct buffer_view *) = + (void (*)(struct buffer_view *))ctx.userdata; + struct buffer *b = buffers_find(ctx.buffers, "*completions*"); + + // is it in the popup? + if (b != NULL && window_buffer(popup_window()) == b) { + struct buffer_view *v = window_buffer_view(popup_window()); + movement_fn(v); + + if (v->dot.line >= text_num_lines(b->text)) { + buffer_backward_line(v); + } + } + + return 0; +} + +static int32_t insert_completion(struct command_ctx ctx, int argc, + const char *argv[]) { + struct buffer *b = buffers_find(ctx.buffers, "*completions*"); + // is it in the popup? + if (b != NULL && window_buffer(popup_window()) == b) { + struct buffer_view *cv = window_buffer_view(popup_window()); + + if (cv->dot.line < g_ncompletions) { + char *ins = (char *)g_completions[cv->dot.line].insert; + bool complete = g_completions[cv->dot.line].complete; + size_t inslen = strlen(ins); + if (minibuffer_focused()) { + buffer_add_text(window_buffer_view(minibuffer_window()), ins, inslen); + } else { + buffer_add_text(window_buffer_view(windows_get_active()), ins, inslen); + } + + if (complete) { + minibuffer_execute(); + } + } + } + + return 0; +} + +COMMAND_FN("next-completion", next_completion, goto_completion, + buffer_forward_line); +COMMAND_FN("prev-completion", prev_completion, goto_completion, + buffer_backward_line); +COMMAND_FN("insert-completion", insert_completion, insert_completion, NULL); + +static void on_find_file_input(void *userdata) { + struct buffers *buffers = (struct buffers *)userdata; + struct text_chunk txt = minibuffer_content(); + + struct window *mb = minibuffer_window(); + struct buffer_location mb_dot = window_absolute_cursor_location(mb); + + struct buffer *b = buffers_find(buffers, "*completions*"); + if (b == NULL) { + b = buffers_add(buffers, buffer_create("*completions*")); + buffer_add_update_hook(b, update_completion_buffer, NULL); + window_set_buffer_e(popup_window(), b, false, false); + } + + struct buffer_view *v = window_buffer_view(popup_window()); + + char path[1024]; + strncpy(path, txt.text, txt.nbytes); + path[(txt.nbytes >= 1024 ? 1023 : txt.nbytes)] = '\0'; + + for (uint32_t compi = 0; compi < g_ncompletions; ++compi) { + free((void *)g_completions[compi].display); + free((void *)g_completions[compi].insert); + } + + g_ncompletions = 0; + complete_path(path, g_completions, 50, &g_ncompletions); + + size_t max_width = 0; + struct buffer_location prev_dot = v->dot; + + buffer_clear(v); + if (g_ncompletions > 0) { + for (uint32_t compi = 0; compi < g_ncompletions; ++compi) { + const char *disp = g_completions[compi].display; + size_t width = strlen(disp); + if (width > max_width) { + max_width = width; + } + buffer_add_text(v, (uint8_t *)disp, width); + + // the extra newline feels weird in navigation + if (compi != g_ncompletions - 1) { + buffer_add_text(v, (uint8_t *)"\n", 1); + } + } + + buffer_goto(v, prev_dot.line, prev_dot.col); + if (prev_dot.line >= text_num_lines(b->text)) { + buffer_backward_line(v); + } + + if (!popup_window_visible()) { + struct binding bindings[] = { + ANONYMOUS_BINDING(Ctrl, 'N', &next_completion_command), + ANONYMOUS_BINDING(Ctrl, 'P', &prev_completion_command), + ANONYMOUS_BINDING(ENTER, &insert_completion_command), + }; + buffer_bind_keys(minibuffer_buffer(), bindings, + sizeof(bindings) / sizeof(bindings[0])); + } + + uint32_t width = max_width > 2 ? max_width + 2 : 4, + height = g_ncompletions > 10 ? 10 : g_ncompletions; + windows_show_popup(mb_dot.line - height, mb_dot.col, width, height); + } else { + windows_close_popup(); + } + + if (txt.allocated) { + free(txt.text); + } +} + int32_t find_file(struct command_ctx ctx, int argc, const char *argv[]) { const char *pth = NULL; if (argc == 0) { - return minibuffer_prompt(ctx, "find file: "); + return minibuffer_prompt_interactive(ctx, on_find_file_input, ctx.buffers, + "find file: "); } + abort_completion(); + pth = argv[0]; struct stat sb = {0}; if (stat(pth, &sb) < 0 && errno != ENOENT) { @@ -132,124 +410,6 @@ int32_t switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) { ctx.active_window, ctx.buffers, argc, argv); } -static char *g_last_search = NULL; - -int64_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; - - return linedist * linedist + 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; - } - } -} - -const char *search_prompt(bool reverse) { - const char *txt = "search (down): "; - if (reverse) { - txt = "search (up): "; - } - - return txt; -} - -void do_search(struct buffer_view *view, const char *pattern, bool reverse) { - struct match *matches = NULL; - uint32_t nmatches = 0; - - g_last_search = strdup(pattern); - - struct buffer_view *buffer_view = window_buffer_view(windows_get_active()); - buffer_find(buffer_view->buffer, pattern, &matches, &nmatches); - - // find the "nearest" match - if (nmatches > 0) { - struct match *closest = reverse ? &matches[nmatches - 1] : &matches[0]; - int64_t closest_dist = INT64_MAX; - for (uint32_t matchi = 0; matchi < nmatches; ++matchi) { - struct match *m = &matches[matchi]; - int res = buffer_loc_cmp(m->begin, view->dot); - int64_t dist = matchdist(m, view->dot); - if (((res < 0 && reverse) || (res > 0 && !reverse)) && - dist < closest_dist) { - closest_dist = dist; - closest = m; - } - } - buffer_goto(buffer_view, closest->begin.line, closest->begin.col); - } -} - -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); - memcpy(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); - } - 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()); - do_search(window_buffer_view(ctx.active_window), argv[0], reverse); - - return 0; -} - int32_t timers(struct command_ctx ctx, int argc, const char *argv[]) { struct buffer *b = buffers_add(ctx.buffers, buffer_create("timers")); buffer_set_readonly(b, true); @@ -289,6 +449,7 @@ int32_t buflist_visit_cmd(struct command_ctx ctx, int argc, uint32_t len = end - (char *)text.text; char *bufname = (char *)malloc(len + 1); strncpy(bufname, text.text, len); + bufname[len] = '\0'; struct buffer *target = buffers_find(ctx.buffers, bufname); free(bufname); @@ -364,14 +525,14 @@ void register_global_commands(struct commands *commands, {.name = "run-command-interactive", .fn = run_interactive}, {.name = "switch-buffer", .fn = switch_buffer}, {.name = "abort", .fn = _abort}, - {.name = "find-next", .fn = find, .userdata = "forward"}, - {.name = "find-prev", .fn = find, .userdata = "backward"}, {.name = "timers", .fn = timers}, {.name = "buffer-list", .fn = buffer_list}, {.name = "exit", .fn = exit_editor, .userdata = terminate_cb}}; register_commands(commands, global_commands, sizeof(global_commands) / sizeof(global_commands[0])); + + register_search_replace_commands(commands); } #define BUFFER_WRAPCMD_POS(fn) \ 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 +#include +#include + +#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])); +} diff --git a/src/main/search-replace.h b/src/main/search-replace.h new file mode 100644 index 0000000..d0b2012 --- /dev/null +++ b/src/main/search-replace.h @@ -0,0 +1,14 @@ +struct commands; + +/** + * Abort a replace currently in progress. + */ +void abort_replace(); + +/** + * Register search and replace commands + * + * @param [in] commands Command registry to register search and + * replace commands in. + */ +void register_search_replace_commands(struct commands *commands); -- cgit v1.2.3