commit b7f7a516c405f90fb1294a6b7cf4463373294cb0
parent 4e39cee7c2670c566dd05abc926f01aa8444e018
Author: lumidify <nobody@lumidify.org>
Date: Sat, 22 Oct 2022 11:47:14 +0200
Support highlighting entire words when searching and replacing
Diffstat:
14 files changed, 134 insertions(+), 45 deletions(-)
diff --git a/buffer.c b/buffer.c
@@ -274,6 +274,7 @@ buffer_set_hard_line_based(ledit_buffer *buffer, int hl) {
buffer->hard_line_based = hl;
}
+/* FIXME: lock view if others are locked! */
void
buffer_add_view(ledit_buffer *buffer, ledit_mode mode, size_t line, size_t pos, long scroll_offset) {
size_t new_num = add_sz(buffer->views_num, 1);
diff --git a/configparser.c b/configparser.c
@@ -522,6 +522,34 @@ destroy_theme_string(ledit_common *common, void *obj) {
free(*obj_str);
}
+static int
+parse_theme_bool(
+ ledit_common *common,
+ void *obj, const char *val, size_t val_len, char *key,
+ char *filename, size_t line, size_t line_offset, char **errstr) {
+ (void)common;
+ int *num = (int *)obj;
+ if (str_array_equal("true", val, val_len)) {
+ *num = 1;
+ return 0;
+ } else if (str_array_equal("false", val, val_len)) {
+ *num = 0;
+ return 0;
+ }
+ *errstr = print_fmt(
+ "%s: Invalid boolean '%.*s' "
+ "for '%s' at line %zu, position %zu",
+ filename, val_len, val, key, line, line_offset
+ );
+ return 1;
+}
+
+static void
+destroy_theme_bool(ledit_common *common, void *obj) {
+ (void)common;
+ (void)obj;
+}
+
/* FIXME: This interface is absolutely horrible - it's mainly this way to reuse the
theme array for the destroy function */
/* If theme is NULL, a new theme is loaded, else it is destroyed */
@@ -560,6 +588,7 @@ load_destroy_theme(ledit_common *common, ast_list *theme_list, ledit_theme *them
{"bar-fmt", &theme->bar_fmt, &parse_theme_string, &destroy_theme_string, BAR_FMT, default_init},
{"scrollbar-fg", &theme->scrollbar_fg, &parse_theme_color, &destroy_theme_color, SCROLLBAR_FG, default_init},
{"scrollbar-bg", &theme->scrollbar_bg, &parse_theme_color, &destroy_theme_color, SCROLLBAR_BG, default_init},
+ {"highlight-search", &theme->highlight_search, &parse_theme_bool, &destroy_theme_bool, HIGHLIGHT_SEARCH, default_init},
};
if (default_init)
diff --git a/configparser.h b/configparser.h
@@ -10,6 +10,7 @@ typedef struct {
int scrollbar_width;
int scrollbar_step;
int text_size;
+ int highlight_search;
XftColor text_fg;
XftColor text_bg;
XftColor cursor_fg;
diff --git a/keys_basic.c b/keys_basic.c
@@ -1,3 +1,4 @@
+/* FIXME: all movement commands that modify the selection should first check if the selection is valid! */
/* FIXME: should motion callbacks be ignored in visual mode as they currently are? */
/* FIXME: check allowed modes at beginning of functions */
/* FIXME: the stacks here are shared for all views which can cause weird
@@ -180,8 +181,8 @@ static struct basic_key_cb basic_key_cb_map[] = {
{"delete-to-eol", &delete_to_eol, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
{"enter-commandedit", &enter_commandedit, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
{"enter-insert", &enter_insert, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|VISUAL},
- {"enter-searchedit-backwards", &enter_searchedit_backward, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
- {"enter-searchedit-forwards", &enter_searchedit_forward, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
+ {"enter-searchedit-backwards", &enter_searchedit_backward, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT|VISUAL},
+ {"enter-searchedit-forwards", &enter_searchedit_forward, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT|VISUAL},
{"enter-visual", &enter_visual, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
{"return-to-normal", &return_to_normal, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
{"find-char-backwards", &find_char_backwards, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
@@ -224,8 +225,8 @@ static struct basic_key_cb basic_key_cb_map[] = {
{"scroll-lines-up", &scroll_lines_up, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
{"scroll-with-cursor-down", &scroll_with_cursor_down, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
{"scroll-with-cursor-up", &scroll_with_cursor_up, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
- {"search-next", &key_search_next, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
- {"search-previous", &key_search_prev, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
+ {"search-next", &key_search_next, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT|VISUAL},
+ {"search-previous", &key_search_prev, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT|VISUAL},
{"show-line", &show_line, KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
{"switch-selection-end", &switch_selection_end, KEY_FLAG_JUMP_TO_CURSOR, VISUAL},
{"toggle-hard-line-based", &toggle_hard_line_based, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
@@ -695,8 +696,8 @@ delete_selection(ledit_view *view) {
);
paste_buffer_line_based = 0;
view->sel_valid = 0;
- view->sel.line1 = view->sel.line2 = 0;
- view->sel.byte1 = view->sel.byte2 = 0;
+ view->sel.line1 = view->sel.line2 = view->cur_line;
+ view->sel.byte1 = view->sel.byte2 = view->cur_index;
view_wipe_line_cursor_attrs(view, view->cur_line);
return 1;
}
@@ -1797,6 +1798,7 @@ move_cursor_left_right(ledit_view *view, int dir, int allow_illegal_index) {
} else {
view->cur_index = new_index;
if (view->mode == VISUAL) {
+ /* FIXME: check if view->sel_valid and only use it then (also change in other places) */
view_set_selection(view, view->sel.line1, view->sel.byte1, view->cur_line, new_index);
} else if (view->mode == INSERT && view->sel_valid) {
/* FIXME: I guess this is unnecessary now that no
diff --git a/keys_command.c b/keys_command.c
@@ -355,20 +355,36 @@ next_replace_pos(
/* returns whether keys should continue being captured */
static int
move_to_next_substitution(ledit_view *view) {
- view_wipe_line_cursor_attrs(view, view->cur_line);
+ ledit_theme *theme = config_get_theme();
+ if (view->mode == NORMAL)
+ view_wipe_line_cursor_attrs(view, view->cur_line);
+ else if (view->mode == VISUAL)
+ view_wipe_selection(view);
if (!next_replace_pos(view, sub_state.line, sub_state.byte, sub_state.max_line, &sub_state.line, &sub_state.byte)) {
+ /* FIXME: why are these set here? */
view->cur_line = sub_state.line;
view->cur_index = sub_state.byte;
- if (view->mode == NORMAL)
+ if (view->mode == NORMAL) {
+ view->cur_index = view_get_legal_normal_pos(view, view->cur_line, view->cur_index);
view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
+ } else if (view->mode == VISUAL) {
+ view_set_selection(view, view->cur_line, view->cur_index, view->cur_line, view->cur_index);
+ }
window_show_message(view->window, "No more matches", -1);
buffer_unlock_all_views(view->buffer);
return 0;
}
+ if (theme->highlight_search && view->mode != VISUAL) {
+ view_wipe_line_cursor_attrs(view, view->cur_line);
+ view_set_mode(view, VISUAL);
+ }
view->cur_line = sub_state.line;
view->cur_index = sub_state.byte;
- if (view->mode == NORMAL)
+ if (view->mode == NORMAL) {
view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
+ } else if (view->mode == VISUAL && theme->highlight_search) {
+ view_set_selection(view, view->cur_line, view->cur_index, view->cur_line, view->cur_index + sub_state.slen);
+ }
window_show_message(view->window, "Replace? (y/Y/n/N)", -1);
view_ensure_cursor_shown(view);
return 1;
@@ -402,7 +418,10 @@ substitute_single(ledit_view *view) {
static void
substitute_all_remaining(ledit_view *view) {
- view_wipe_line_cursor_attrs(view, view->cur_line);
+ if (view->mode == NORMAL)
+ view_wipe_line_cursor_attrs(view, view->cur_line);
+ else if (view->mode == VISUAL)
+ view_wipe_selection(view);
size_t min_line = SIZE_MAX;
while (next_replace_pos(view, sub_state.line, sub_state.byte, sub_state.max_line, &sub_state.line, &sub_state.byte)) {
if (sub_state.line < min_line)
@@ -424,6 +443,8 @@ substitute_all_remaining(ledit_view *view) {
/* this doesn't need to be added to the undo stack since it's called on undo/redo anyways */
view->cur_index = view_get_legal_normal_pos(view, view->cur_line, view->cur_index);
view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
+ } else if (view->mode == VISUAL) {
+ view_set_selection(view, view->cur_line, view->cur_index, view->cur_line, view->cur_index);
}
view_ensure_cursor_shown(view);
buffer_unlock_all_views(view->buffer);
@@ -462,6 +483,8 @@ handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2) {
}
c++;
}
+ free(sep);
+ sep = NULL;
free(sub_state.search);
free(sub_state.replace);
sub_state.search = ledit_strdup(cmd);
@@ -477,12 +500,6 @@ handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2) {
sub_state.num = 0;
sub_state.start_group = 1;
- /* trying to perform substitution in visual mode would make
- it unnecessarily complicated */
- if (view->mode == VISUAL) {
- view_wipe_selection(view);
- view_set_mode(view, NORMAL);
- }
if (confirm) {
buffer_lock_all_views_except(view->buffer, view, "Ongoing substitution in other view.");
view->cur_command_type = CMD_SUBSTITUTE;
@@ -490,7 +507,6 @@ handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2) {
} else {
substitute_all_remaining(view);
}
- free(sep);
return 0;
error:
window_show_message(view->window, "Invalid command", -1);
@@ -871,13 +887,24 @@ edit_nextsearch(ledit_view *view, char *key_text, size_t len, size_t lang_index)
return 1;
}
+/* FIXME: the current "highlight_search" support is a bit weird and will probably conflict
+ in some way if other support for visual mode (e.g. only search in selection) is added */
/* FIXME: support visual mode, i.e. change selection to new place? */
void
search_next(ledit_view *view) {
view_wipe_line_cursor_attrs(view, view->cur_line);
- search_state ret = ledit_search_next(view, &view->cur_line, &view->cur_index);
- if (view->mode == NORMAL)
+ size_t len = 0;
+ search_state ret = ledit_search_next(view, &view->cur_line, &view->cur_index, &len);
+ ledit_theme *theme = config_get_theme();
+ /* FIXME: figure out key stack handling when modes are also changed here */
+ if (theme->highlight_search && len > 0 && (ret == SEARCH_NORMAL || ret == SEARCH_WRAPPED)) {
+ view_set_mode(view, VISUAL);
+ view_set_selection(view, view->cur_line, view->cur_index, view->cur_line, view->cur_index + len);
+ } else if (view->mode == VISUAL) {
+ view_set_selection(view, view->cur_line, view->cur_index, view->cur_line, view->cur_index);
+ } else if (view->mode == NORMAL) {
view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
+ }
view_ensure_cursor_shown(view);
if (ret != SEARCH_NORMAL)
window_show_message(view->window, search_state_to_str(ret), -1);
@@ -886,9 +913,17 @@ search_next(ledit_view *view) {
void
search_prev(ledit_view *view) {
view_wipe_line_cursor_attrs(view, view->cur_line);
- search_state ret = ledit_search_prev(view, &view->cur_line, &view->cur_index);
- if (view->mode == NORMAL)
+ size_t len = 0;
+ search_state ret = ledit_search_prev(view, &view->cur_line, &view->cur_index, &len);
+ ledit_theme *theme = config_get_theme();
+ if (theme->highlight_search && len > 0 && (ret == SEARCH_NORMAL || ret == SEARCH_WRAPPED)) {
+ view_set_mode(view, VISUAL);
+ view_set_selection(view, view->cur_line, view->cur_index, view->cur_line, view->cur_index + len);
+ } else if (view->mode == VISUAL) {
+ view_set_selection(view, view->cur_line, view->cur_index, view->cur_line, view->cur_index);
+ } if (view->mode == NORMAL) {
view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
+ }
view_ensure_cursor_shown(view);
if (ret != SEARCH_NORMAL)
window_show_message(view->window, search_state_to_str(ret), -1);
diff --git a/ledit.1 b/ledit.1
@@ -1,5 +1,3 @@
-.\" WARNING: Some parts of this are stolen shamelessly from OpenBSD's
-.\" vi(1) manpage!
.Dd May 27, 2022
.Dt LEDIT 1
.Os
diff --git a/leditrc.5 b/leditrc.5
@@ -1,4 +1,4 @@
-.Dd September 2, 2022
+.Dd October 22, 2022
.Dt LEDITRC 5
.Os
.Sh NAME
@@ -169,6 +169,12 @@ Default: #CCCCCC
.It Ar scrollbar-fg
Color of scrollbar handle.
Default: #000000
+.It Ar highlight-search
+Whether entire words should be highlighted when searching or replacing with confirmation (true/false).
+Note that the mode is automatically switched to visual when this is set and a word needs to be highlighted.
+This is a bit weird, but in order to keep everything a bit more consistent, selections are curently
+only allowed in visual mode.
+Default: true
.El
.Sh BINDINGS
The key bindings may be configured by assigning
@@ -423,10 +429,10 @@ In visual mode, the selection range is automatically pasted into the line editor
so commands can be performed on it.
.It Ar enter-insert Op normal, visual
Enter insert mode.
-.It Ar enter-searchedit-backwards Op normal, insert
+.It Ar enter-searchedit-backwards Op normal, insert, visual
Open the line editor for searching backwards.
Note that no regex is currently supported.
-.It Ar enter-searchedit-forwards Op normal, insert
+.It Ar enter-searchedit-forwards Op normal, insert, visual
Open the line editor for searching forwards.
Note that no regex is currently supported.
.It Ar enter-visual Op normal, insert
@@ -652,9 +658,9 @@ Move
.Ar num
lines up, attempting to leave the cursor in its current line and character position.
Note that this command works with soft lines, regardless of the current mode.
-.It Ar search-next Op normal, insert
+.It Ar search-next Op normal, insert, visual
Move to the next search result.
-.It Ar search-previous Op normal, insert
+.It Ar search-previous Op normal, insert, visual
Move to the previous search result.
.It Ar show-line Op normal, visual, insert
Show the current file name, whether the buffer has been modified since the last
diff --git a/leditrc.example b/leditrc.example
@@ -18,6 +18,7 @@ theme = {
scrollbar-step = 20
scrollbar-bg = CCCCCC
scrollbar-fg = 000000
+ highlight-search = true
}
bindings = {
@@ -66,8 +67,8 @@ bindings = {
bind enter-commandedit text ":" modes normal|visual
bind enter-searchedit-backwards text "?" modes normal
bind enter-searchedit-forwards text "/" modes normal
- bind search-next text "n" modes normal
- bind search-previous text "N" modes normal
+ bind search-next text "n" modes normal|visual
+ bind search-previous text "N" modes normal|visual
bind undo text "u" modes normal
bind redo text "U" modes normal
bind repeat-command text "." modes normal
diff --git a/search.c b/search.c
@@ -7,6 +7,7 @@
/* FIXME: make sure only whole utf8 chars are matched */
static char *last_search = NULL;
+static size_t last_search_len = 0;
enum {
FORWARD,
BACKWARD
@@ -22,6 +23,7 @@ set_search_forward(char *pattern) {
last_dir = FORWARD;
free(last_search);
last_search = ledit_strdup(pattern);
+ last_search_len = strlen(last_search);
}
void
@@ -29,12 +31,14 @@ set_search_backward(char *pattern) {
last_dir = BACKWARD;
free(last_search);
last_search = ledit_strdup(pattern);
+ last_search_len = strlen(last_search);
}
static search_state
-search_forward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
+search_forward(ledit_view *view, size_t *line_ret, size_t *byte_ret, size_t *len_ret) {
*line_ret = view->cur_line;
*byte_ret = view->cur_index;
+ *len_ret = last_search_len;
/* if last_search is empty, strstr will find the ending '\0' */
if (last_search == NULL || last_search[0] == '\0')
return SEARCH_NO_PATTERN;
@@ -81,9 +85,10 @@ search_forward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
/* FIXME: this is insanely inefficient */
/* FIXME: just go backwards char-by-char and compare */
static search_state
-search_backward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
+search_backward(ledit_view *view, size_t *line_ret, size_t *byte_ret, size_t *len_ret) {
*line_ret = view->cur_line;
*byte_ret = view->cur_index;
+ *len_ret = last_search_len;
if (last_search == NULL || last_search[0] == '\0')
return SEARCH_NO_PATTERN;
size_t line = view->cur_line;
@@ -145,19 +150,19 @@ search_backward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
}
search_state
-ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
+ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret, size_t *len_ret) {
if (last_dir == FORWARD)
- return search_forward(view, line_ret, byte_ret);
+ return search_forward(view, line_ret, byte_ret, len_ret);
else
- return search_backward(view, line_ret, byte_ret);
+ return search_backward(view, line_ret, byte_ret, len_ret);
}
search_state
-ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
+ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret, size_t *len_ret) {
if (last_dir == FORWARD)
- return search_backward(view, line_ret, byte_ret);
+ return search_backward(view, line_ret, byte_ret, len_ret);
else
- return search_forward(view, line_ret, byte_ret);
+ return search_forward(view, line_ret, byte_ret, len_ret);
}
char *
diff --git a/search.h b/search.h
@@ -13,8 +13,8 @@ typedef enum {
void search_cleanup(void);
void set_search_forward(char *pattern);
void set_search_backward(char *pattern);
-search_state ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret);
-search_state ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret);
+search_state ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret, size_t *len_ret);
+search_state ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret, size_t *len_ret);
/*
* Get a string corresponding to a search_state.
diff --git a/theme_config.h b/theme_config.h
@@ -45,5 +45,8 @@ static const char *SCROLLBAR_STEP = "20";
static const char *SCROLLBAR_BG = "#CCCCCC";
/* color of scrollbar handle */
static const char *SCROLLBAR_FG = "#000000";
+/* whether to highlight entire words when searching/replacing */
+/* FIXME: this should maybe be in a separate "general config" section */
+static const char *HIGHLIGHT_SEARCH = "true";
#endif /* _THEME_CONFIG_H_ */
diff --git a/util.c b/util.c
@@ -50,7 +50,7 @@ sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2) {
}
int
-str_array_equal(char *terminated, char *array, size_t len) {
+str_array_equal(const char *terminated, const char *array, size_t len) {
if (!strncmp(terminated, array, len)) {
/* this is kind of inefficient, but there's no way to know
otherwise if strncmp just stopped comparing after a '\0' */
diff --git a/util.h b/util.h
@@ -32,7 +32,7 @@ void sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2);
* Returns non-zero if they are equal, 0 otherwise.
*/
/* Note: this doesn't work if array contains '\0'. */
-int str_array_equal(char *terminated, char *array, size_t len);
+int str_array_equal(const char *terminated, const char *array, size_t len);
#define LEDIT_MIN(x, y) ((x) < (y) ? (x) : (y))
#define LEDIT_MAX(x, y) ((x) > (y) ? (x) : (y))
diff --git a/view.c b/view.c
@@ -24,6 +24,9 @@
#include "assert.h"
#include "configparser.h"
+/* FIXME: handle selections better - it can happen that the cursor is moved independently of
+ the selection, leading to weird "jumping selection" - this should be made impossible */
+
/* Basic attributes set for all text. */
static PangoAttrList *basic_attrs = NULL;
@@ -1746,9 +1749,14 @@ view_wipe_selection(ledit_view *view) {
}
}
view->sel_valid = 0;
- view->sel.line1 = view->sel.line2 = 0;
- view->sel.byte1 = view->sel.byte2 = 0;
- view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
+ /* FIXME: check what makes most sense here - this used to be set to 0, but that
+ caused "jumping effects" because some functions (cursor movement) use these
+ values without checking view->sel_valid before (which should actually be
+ changed because it is an error) */
+ view->sel.line1 = view->sel.line2 = view->cur_line;
+ view->sel.byte1 = view->sel.byte2 = view->cur_index;
+ if (view->mode == NORMAL)
+ view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
}
void