diff --git a/docs/marks.rst b/docs/marks.rst index 3e0345565..4650f9406 100644 --- a/docs/marks.rst +++ b/docs/marks.rst @@ -57,6 +57,26 @@ You can also use the facilities for :doc:`remote-control` to dynamically add/remove markers. +Scrolling to marks +-------------------- + +kitty has an action to scroll to the next line that contains a mark. You can +use it by mapping it to some shortcut in :file:`kitty.conf`:: + + map ctrl+p scroll_to_mark prev + map ctrl+n scroll_to_mark next + +Then pressing :kbd:`ctrl+p` will scroll to the first line in the scrollback +buffer above the current top line that contains a mark. Pressing :kbd:`ctrl+n` +will scroll to show the first line below the current last line that contains +a mark. If you wish to jump to a mark of a specific type, you can add that to +the mapping:: + + map ctrl+1 scroll_to_mark prev 1 + +Which will scroll only to marks of type 1. + + The full syntax for creating marks ------------------------------------- diff --git a/kitty/config.py b/kitty/config.py index 30905aea9..0f669f2f1 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -277,6 +277,22 @@ def toggle_marker(func, rest): return func, list(parse_marker_spec(ftype, parts)) +@func_with_args('scroll_to_mark') +def scroll_to_mark(func, rest): + parts = rest.split() + if not parts or not rest: + return func, [True, 0] + if len(parts) == 1: + q = parts[0].lower() + if q in ('prev', 'previous', 'next'): + return func, [q != 'next', 0] + try: + return func, [True, max(0, min(int(q), 3))] + except Exception: + raise ValueError('{} is not a valid scroll_to_mark destination'.format(rest)) + return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))] + + def parse_key_action(action): parts = action.strip().split(maxsplit=1) func = parts[0] diff --git a/kitty/line.c b/kitty/line.c index a2b4b01dd..1a4961c26 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -665,6 +665,15 @@ __eq__(Line *a, Line *b) { return a->xnum == b->xnum && memcmp(a->cpu_cells, b->cpu_cells, sizeof(CPUCell) * a->xnum) == 0 && memcmp(a->gpu_cells, b->gpu_cells, sizeof(GPUCell) * a->xnum) == 0; } +bool +line_has_mark(Line *line, attrs_type mark) { + for (index_type x = 0; x < line->xnum; x++) { + attrs_type m = (line->gpu_cells[x].attrs >> MARK_SHIFT) & MARK_MASK; + if (m && (!mark || mark == m)) return true; + } + return false; +} + static inline void report_marker_error(PyObject *marker) { if (!PyObject_HasAttrString(marker, "error_reported")) { diff --git a/kitty/lineops.h b/kitty/lineops.h index d2f0671a5..2109ed49a 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -91,6 +91,7 @@ void historybuf_mark_line_dirty(HistoryBuf *self, index_type y); void historybuf_refresh_sprite_positions(HistoryBuf *self); void historybuf_clear(HistoryBuf *self); void mark_text_in_line(PyObject *marker, Line *line); +bool line_has_mark(Line *, attrs_type mark); #define as_text_generic(args, container, get_line, lines, columns) { \ diff --git a/kitty/screen.c b/kitty/screen.c index 99b8d7ecc..ddf0e8707 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2293,6 +2293,40 @@ set_marker(Screen *self, PyObject *args) { Py_RETURN_NONE; } + +static PyObject* +scroll_to_next_mark(Screen *self, PyObject *args) { + int backwards = 1; + unsigned int mark = 0; + if (!PyArg_ParseTuple(args, "|Ip", &mark, &backwards)) return NULL; + if (!screen_has_marker(self) || self->linebuf == self->alt_linebuf) Py_RETURN_FALSE; + if (backwards) { + for (unsigned int y = self->scrolled_by; y < self->historybuf->count; y++) { + historybuf_init_line(self->historybuf, y, self->historybuf->line); + if (line_has_mark(self->historybuf->line, mark)) { + screen_history_scroll(self, y - self->scrolled_by + 1, true); + Py_RETURN_TRUE; + } + } + } else { + Line *line; + for (unsigned int y = self->scrolled_by; y > 0; y--) { + if (y > self->lines) { + historybuf_init_line(self->historybuf, y - self->lines, self->historybuf->line); + line = self->historybuf->line; + } else { + linebuf_init_line(self->linebuf, self->lines - y); + line = self->linebuf->line; + } + if (line_has_mark(line, mark)) { + screen_history_scroll(self, self->scrolled_by - y + 1, false); + Py_RETURN_TRUE; + } + } + } + Py_RETURN_FALSE; +} + static PyObject* marked_cells(Screen *self, PyObject *o UNUSED) { PyObject *ans = PyList_New(0); @@ -2414,6 +2448,7 @@ static PyMethodDef methods[] = { MND(copy_colors_from, METH_O) MND(set_marker, METH_VARARGS) MND(marked_cells, METH_NOARGS) + MND(scroll_to_next_mark, METH_VARARGS) {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, {NULL} /* Sentinel */ diff --git a/kitty/window.py b/kitty/window.py index 1b11ea595..e598f5bec 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -631,4 +631,7 @@ class Window: if self.current_marker_spec is not None: self.screen.set_marker() self.current_marker_spec = None + + def scroll_to_mark(self, prev=True, mark=0): + self.screen.scroll_to_next_mark(mark, prev) # }}} diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 9743e047e..38c81a53a 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -474,3 +474,17 @@ class TestScreen(BaseTest): s.set_marker(marker_from_function(mark_x)) self.ae(s.marked_cells(), [(0, 1, 1), (2, 1, 2), (4, 1, 3)]) + s = self.create_screen(lines=5, scrollback=10) + for i in range(15): + s.draw(str(i)) + if i != 14: + s.carriage_return(), s.linefeed() + s.set_marker(marker_from_regex(r'\d+', 3)) + for i in range(10): + self.assertTrue(s.scroll_to_next_mark()) + self.ae(s.scrolled_by, i + 1) + self.ae(s.scrolled_by, 10) + for i in range(10): + self.assertTrue(s.scroll_to_next_mark(0, False)) + self.ae(s.scrolled_by, 10 - i - 1) + self.ae(s.scrolled_by, 0)