From 78dc93721d29e44c5ac9ffa3f06e44bfad63bf69 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Aug 2020 12:32:47 +0530 Subject: [PATCH] Add tests for hyperlink storage --- kitty/hyperlink.c | 34 +++++++++++++++++--- kitty/hyperlink.h | 3 ++ kitty/line.c | 13 +++++++- kitty/screen.c | 17 ++++++++++ kitty_tests/screen.py | 73 +++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 133 insertions(+), 7 deletions(-) diff --git a/kitty/hyperlink.c b/kitty/hyperlink.c index 1589b7890..45b445def 100644 --- a/kitty/hyperlink.c +++ b/kitty/hyperlink.c @@ -10,6 +10,8 @@ #include #define MAX_KEY_LEN 2048 +#define MAX_ID_LEN 256 +#define MAX_ADDS_BEFORE_GC 256 #undef uthash_fatal #define uthash_fatal(msg) fatal(msg) @@ -69,8 +71,8 @@ free_hyperlink_pool(HYPERLINK_POOL_HANDLE h) { } -static void -garbage_collect_pool(Screen *screen) { +void +screen_garbage_collect_hyperlink_pool(Screen *screen) { HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool; pool->num_of_adds_since_garbage_collection = 0; if (!pool->max_link_id) return; @@ -99,14 +101,14 @@ get_id_for_hyperlink(Screen *screen, const char *id, const char *url) { if (!url) return 0; HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool; static char key[MAX_KEY_LEN] = {0}; - size_t keylen = snprintf(key, MAX_KEY_LEN-1, "%s:%s", id ? id : "", url); + size_t keylen = snprintf(key, MAX_KEY_LEN-1, "%.*s:%s", MAX_ID_LEN, id ? id : "", url); HyperLinkEntry *s = NULL; if (pool->hyperlinks) { HASH_FIND_STR(pool->hyperlinks, key, s); if (s) return s->id; } hyperlink_id_type new_id = 0; - if (pool->num_of_adds_since_garbage_collection >= 256) garbage_collect_pool(screen); + if (pool->num_of_adds_since_garbage_collection >= MAX_ADDS_BEFORE_GC) screen_garbage_collect_hyperlink_pool(screen); if (pool->max_link_id >= HYPERLINK_MAX_NUMBER) { log_error("Too many hyperlinks, discarding oldest, this means some hyperlinks might be incorrect"); new_id = pool->hyperlinks->id; @@ -124,3 +126,27 @@ get_id_for_hyperlink(Screen *screen, const char *id, const char *url) { pool->num_of_adds_since_garbage_collection++; return s->id; } + +const char* +get_hyperlink_for_id(Screen *screen, hyperlink_id_type id) { + HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool; + HyperLinkEntry *s, *tmp; + HASH_ITER(hh, pool->hyperlinks, s, tmp) { + if (s->id == id) return strstr(s->key, ":") + 1; + } + return NULL; +} + + +PyObject* +screen_hyperlinks_as_list(Screen *screen) { + HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool; + PyObject *ans = PyList_New(0); + HyperLinkEntry *s, *tmp; + HASH_ITER(hh, pool->hyperlinks, s, tmp) { + PyObject *e = Py_BuildValue("sH", s->key, s->id); + PyList_Append(ans, e); + Py_DECREF(e); + } + return ans; +} diff --git a/kitty/hyperlink.h b/kitty/hyperlink.h index c7959676c..5f96e29a7 100644 --- a/kitty/hyperlink.h +++ b/kitty/hyperlink.h @@ -13,3 +13,6 @@ void free_hyperlink_pool(HYPERLINK_POOL_HANDLE); void clear_hyperlink_pool(HYPERLINK_POOL_HANDLE); hyperlink_id_type get_id_for_hyperlink(Screen*, const char*, const char*); hyperlink_id_type remap_hyperlink_ids(Screen *self, hyperlink_id_type *map); +PyObject* screen_hyperlinks_as_list(Screen *screen); +void screen_garbage_collect_hyperlink_pool(Screen *screen); +const char* get_hyperlink_for_id(Screen *screen, hyperlink_id_type id); diff --git a/kitty/line.c b/kitty/line.c index aa0a3c59c..127cf5fc1 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -817,12 +817,22 @@ end: Py_RETURN_NONE; } - // Boilerplate {{{ static PyObject* copy_char(Line* self, PyObject *args); #define copy_char_doc "copy_char(src, to, dest) -> Copy the character at src to to the character dest in the line `to`" +#define hyperlink_ids_doc "hyperlink_ids() -> Tuple of hyper link ids at every cell" +static PyObject* +hyperlink_ids(Line *self, PyObject *args UNUSED) { + PyObject *ans = PyTuple_New(self->xnum); + for (index_type x = 0; x < self->xnum; x++) { + PyTuple_SET_ITEM(ans, x, PyLong_FromUnsignedLong(self->cpu_cells[x].hyperlink_id)); + } + return ans; +} + + static PyObject * richcmp(PyObject *obj1, PyObject *obj2, int op); @@ -845,6 +855,7 @@ static PyMethodDef methods[] = { METHOD(set_attribute, METH_VARARGS) METHOD(as_ansi, METH_NOARGS) METHOD(is_continued, METH_NOARGS) + METHOD(hyperlink_ids, METH_NOARGS) METHOD(width, METH_O) METHOD(url_start_at, METH_O) METHOD(url_end_at, METH_VARARGS) diff --git a/kitty/screen.c b/kitty/screen.c index f6dfdbfb9..fa1711a20 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1911,6 +1911,20 @@ deactivate_overlay_line(Screen *self) { #define WRAP2(name, defval1, defval2) static PyObject* name(Screen *self, PyObject *args) { unsigned int a=defval1, b=defval2; if(!PyArg_ParseTuple(args, "|II", &a, &b)) return NULL; screen_##name(self, a, b); Py_RETURN_NONE; } #define WRAP2B(name) static PyObject* name(Screen *self, PyObject *args) { unsigned int a, b; int p; if(!PyArg_ParseTuple(args, "IIp", &a, &b, &p)) return NULL; screen_##name(self, a, b, (bool)p); Py_RETURN_NONE; } +WRAP0(garbage_collect_hyperlink_pool) + +static PyObject* +hyperlinks_as_list(Screen *self, PyObject *args UNUSED) { + return screen_hyperlinks_as_list(self); +} + +static PyObject* +hyperlink_for_id(Screen *self, PyObject *val) { + unsigned long id = PyLong_AsUnsignedLong(val); + if (id > HYPERLINK_MAX_NUMBER) { PyErr_SetString(PyExc_IndexError, "Out of bounds"); return NULL; } + return Py_BuildValue("s", get_hyperlink_for_id(self, id)); +} + static PyObject* set_pending_timeout(Screen *self, PyObject *val) { if (!PyFloat_Check(val)) { PyErr_SetString(PyExc_TypeError, "timeout must be a float"); return NULL; } @@ -2638,6 +2652,9 @@ static PyMethodDef methods[] = { MND(erase_in_line, METH_VARARGS) MND(erase_in_display, METH_VARARGS) MND(scroll_until_cursor, METH_NOARGS) + MND(hyperlinks_as_list, METH_NOARGS) + MND(garbage_collect_hyperlink_pool, METH_NOARGS) + MND(hyperlink_for_id, METH_O) METHOD(current_char_width, METH_NOARGS) MND(insert_lines, METH_VARARGS) MND(delete_lines, METH_VARARGS) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 82dd7d563..a2886e25d 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -2,9 +2,12 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal +from kitty.fast_data_types import ( + DECAWM, DECCOLM, DECOM, IRM, Cursor, parse_bytes +) +from kitty.marks import marker_from_function, marker_from_regex + from . import BaseTest -from kitty.fast_data_types import DECAWM, IRM, Cursor, DECCOLM, DECOM -from kitty.marks import marker_from_regex, marker_from_function class TestScreen(BaseTest): @@ -516,3 +519,69 @@ class TestScreen(BaseTest): self.ae(s.marked_cells(), cells(8)) s.set_marker(marker_from_regex('\t', 3)) self.ae(s.marked_cells(), cells(*range(8))) + + def test_hyperlinks(self): + s = self.create_screen() + self.ae(s.line(0).hyperlink_ids(), tuple(0 for x in range(s.columns))) + + def set_link(url=None, id=None): + parse_bytes(s, '\x1b]8;id={};{}\x1b\\'.format(id or '', url or '').encode('utf-8')) + + set_link('url-a', 'a') + self.ae(s.line(0).hyperlink_ids(), tuple(0 for x in range(s.columns))) + s.draw('a') + self.ae(s.line(0).hyperlink_ids(), (1,) + tuple(0 for x in range(s.columns - 1))) + s.draw('bc') + self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 0)) + set_link() + s.draw('d') + self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 0)) + set_link('url-a', 'a') + s.draw('efg') + self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 1)) + self.ae(s.line(1).hyperlink_ids(), (1, 1, 0, 0, 0)) + set_link('url-b') + s.draw('hij') + self.ae(s.line(1).hyperlink_ids(), (1, 1, 2, 2, 2)) + set_link() + self.ae([('a:url-a', 1), (':url-b', 2)], s.hyperlinks_as_list()) + s.garbage_collect_hyperlink_pool() + self.ae([('a:url-a', 1), (':url-b', 2)], s.hyperlinks_as_list()) + for i in range(s.lines + 2): + s.linefeed() + s.garbage_collect_hyperlink_pool() + self.ae([('a:url-a', 1), (':url-b', 2)], s.hyperlinks_as_list()) + for i in range(s.lines * 2): + s.linefeed() + s.garbage_collect_hyperlink_pool() + self.assertFalse(s.hyperlinks_as_list()) + set_link('url-a', 'x') + s.draw('a') + set_link('url-a', 'y') + s.draw('a') + set_link() + self.ae([('x:url-a', 1), ('y:url-a', 2)], s.hyperlinks_as_list()) + + s = self.create_screen() + set_link('u' * 2048) + s.draw('a') + self.ae([(':' + 'u' * 2045, 1)], s.hyperlinks_as_list()) + s = self.create_screen() + set_link('u' * 2048, 'i' * 300) + s.draw('a') + self.ae([('i'*256 + ':' + 'u' * (2045 - 256), 1)], s.hyperlinks_as_list()) + + s = self.create_screen() + set_link('1'), s.draw('1') + set_link('2'), s.draw('2') + set_link('3'), s.draw('3') + s.cursor.x = 1 + set_link(), s.draw('X') + self.ae(s.line(0).hyperlink_ids(), (1, 0, 3, 0, 0)) + self.ae([(':1', 1), (':2', 2), (':3', 3)], s.hyperlinks_as_list()) + s.garbage_collect_hyperlink_pool() + self.ae([(':1', 1), (':3', 2)], s.hyperlinks_as_list()) + set_link('3'), s.draw('3') + self.ae([(':1', 1), (':3', 2)], s.hyperlinks_as_list()) + set_link('4'), s.draw('4') + self.ae([(':1', 1), (':3', 2), (':4', 3)], s.hyperlinks_as_list())