diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 95ed228bd..c0d1d74e3 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -492,6 +492,18 @@ Unicode database will be matched. ''' ) +opt('select_by_word_characters_forward', '', + ctype='!select_by_word_characters_forward', + long_text=''' +Characters considered part of a word when extending the selection forward on +double clicking. In addition to these characters any character that is marked +as an alphanumeric character in the Unicode database will be matched. + +If empty (default) :opt:`select_by_word_characters` will be used for both +directions. +''' + ) + opt('click_interval', '-1.0', option_type='float', ctype='time', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 9eb4e557b..9037a12a0 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1163,6 +1163,9 @@ class Parser: def select_by_word_characters(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['select_by_word_characters'] = str(val) + def select_by_word_characters_forward(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['select_by_word_characters_forward'] = str(val) + def selection_background(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['selection_background'] = to_color_or_none(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 566e02aa8..ace890ccd 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -304,6 +304,19 @@ convert_from_opts_select_by_word_characters(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_select_by_word_characters_forward(PyObject *val, Options *opts) { + select_by_word_characters_forward(val, opts); +} + +static void +convert_from_opts_select_by_word_characters_forward(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "select_by_word_characters_forward"); + if (ret == NULL) return; + convert_from_python_select_by_word_characters_forward(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_click_interval(PyObject *val, Options *opts) { opts->click_interval = parse_s_double_to_monotonic_t(val); @@ -1067,6 +1080,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_select_by_word_characters(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_select_by_word_characters_forward(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_click_interval(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_focus_follows_mouse(py_opts, opts); diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index 78b09f202..0a80e8226 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -187,6 +187,12 @@ select_by_word_characters(PyObject *chars, Options *opts) { opts->select_by_word_characters = list_of_chars(chars); } +static void +select_by_word_characters_forward(PyObject *chars, Options *opts) { + free(opts->select_by_word_characters_forward); + opts->select_by_word_characters_forward = list_of_chars(chars); +} + static void tab_bar_style(PyObject *val, Options *opts) { opts->tab_bar_hidden = PyUnicode_CompareWithASCIIString(val, "hidden") == 0 ? true: false; diff --git a/kitty/options/types.py b/kitty/options/types.py index 348600484..01a4bf5a7 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -411,6 +411,7 @@ option_names = ( # {{{ 'scrollback_pager', 'scrollback_pager_history_size', 'select_by_word_characters', + 'select_by_word_characters_forward', 'selection_background', 'selection_foreground', 'shell', @@ -561,6 +562,7 @@ class Options: scrollback_pager: typing.List[str] = ['less', '--chop-long-lines', '--RAW-CONTROL-CHARS', '+INPUT_LINE_NUMBER'] scrollback_pager_history_size: int = 0 select_by_word_characters: str = '@-./_~?&=%+#' + select_by_word_characters_forward: str = '' selection_background: typing.Optional[kitty.fast_data_types.Color] = Color(255, 250, 205) selection_foreground: typing.Optional[kitty.fast_data_types.Color] = Color(0, 0, 0) shell: str = '.' diff --git a/kitty/screen.c b/kitty/screen.c index 43c7076b4..ad13e38f0 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -3222,7 +3222,15 @@ screen_selection_range_for_line(Screen *self, index_type y, index_type *start, i } static bool -is_opt_word_char(char_type ch) { +is_opt_word_char(char_type ch, bool forward) { + if (forward && OPT(select_by_word_characters_forward)) { + for (const char_type *p = OPT(select_by_word_characters_forward); *p; p++) { + if (ch == *p) return true; + } + if (*OPT(select_by_word_characters_forward)) { + return false; + } + } if (OPT(select_by_word_characters)) { for (const char_type *p = OPT(select_by_word_characters); *p; p++) { if (ch == *p) return true; @@ -3232,9 +3240,9 @@ is_opt_word_char(char_type ch) { } static bool -is_char_ok_for_word_extension(Line* line, index_type x) { +is_char_ok_for_word_extension(Line* line, index_type x, bool forward) { char_type ch = line->cpu_cells[x].ch; - if (is_word_char(ch) || is_opt_word_char(ch)) return true; + if (is_word_char(ch) || is_opt_word_char(ch, forward)) return true; // pass : from :// so that common URLs are matched if (ch == ':' && x + 2 < line->xnum && line->cpu_cells[x+1].ch == '/' && line->cpu_cells[x+2].ch == '/') return true; return false; @@ -3247,26 +3255,26 @@ screen_selection_range_for_word(Screen *self, const index_type x, const index_ty Line *line = visual_line_(self, y); *y1 = y; *y2 = y; -#define is_ok(x) is_char_ok_for_word_extension(line, x) - if (!is_ok(x)) { +#define is_ok(x, forward) is_char_ok_for_word_extension(line, x, forward) + if (!is_ok(x, false)) { if (initial_selection) return false; *s = x; *e = x; return true; } start = x; end = x; while(true) { - while(start > 0 && is_ok(start - 1)) start--; + while(start > 0 && is_ok(start - 1, false)) start--; if (start > 0 || !line->attrs.continued || *y1 == 0) break; line = visual_line_(self, *y1 - 1); - if (!is_ok(self->columns - 1)) break; + if (!is_ok(self->columns - 1, false)) break; (*y1)--; start = self->columns - 1; } line = visual_line_(self, *y2); while(true) { - while(end < self->columns - 1 && is_ok(end + 1)) end++; + while(end < self->columns - 1 && is_ok(end + 1, true)) end++; if (end < self->columns - 1 || *y2 >= self->lines - 1) break; line = visual_line_(self, *y2 + 1); - if (!line->attrs.continued || !is_ok(0)) break; + if (!line->attrs.continued || !is_ok(0, true)) break; (*y2)++; end = 0; } *s = start; *e = end; diff --git a/kitty/state.h b/kitty/state.h index 23d6b2f04..21f42a019 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -33,6 +33,7 @@ typedef struct { unsigned int scrollback_pager_history_size; bool scrollback_fill_enlarged_window; char_type *select_by_word_characters; + char_type *select_by_word_characters_forward; color_type url_color, background, foreground, active_border_color, inactive_border_color, bell_border_color, tab_bar_background, tab_bar_margin_color; color_type mark1_foreground, mark1_background, mark2_foreground, mark2_background, mark3_foreground, mark3_background; monotonic_t repaint_delay, input_delay;