From ea1aad3c8c50961d146f206fa37b2707d3196464 Mon Sep 17 00:00:00 2001 From: NeilBrown Date: Tue, 19 Sep 2023 20:49:52 +1000 Subject: [PATCH] Add lib-rangetrack Split the rangetracking code out of lib-autospell.py and place it in lib-rangetrack.c. This will allow it to be used by other modules that want on-demand parsing of content. Signed-off-by: NeilBrown --- DOC/TODO.md | 7 +- Makefile | 2 +- data/modules.ini | 2 + lib-rangetrack.c | 322 ++++++++++++++++++++++++++++++++++++++++ python/lib-autospell.py | 237 ++++++----------------------- 5 files changed, 376 insertions(+), 194 deletions(-) create mode 100644 lib-rangetrack.c diff --git a/DOC/TODO.md b/DOC/TODO.md index fd535293..5fd1d681 100644 --- a/DOC/TODO.md +++ b/DOC/TODO.md @@ -9,6 +9,9 @@ the file. ### Triage +- [ ] unknown keysequence should be reported so e.g. if keyboard + is is Greek mode, then I will be told that Cx-b doesn't work +- [ ] menubar doesn't redraw background when resized wider. - [X] open second x11 window, use selections. Close it. command in x11selection_Xcb gets freed??? - [X] adding new lines at end of doc in x11 leaves phantom underline @@ -45,7 +48,7 @@ the file. ### Medium -- [ ] split range management out of autospell so it can be used by other +- [X] split range management out of autospell so it can be used by other modules. - [ ] make it easy for a make-search command to search backwards - [ ] Make a start on CUA mode with mouse/menu/selection support. @@ -824,7 +827,7 @@ Module features - [ ] Some way for 'c-mode' to report where comments are so they can be spell-checked - [ ] drop-down with options - [ ] unify UI with dynamic-completion -- [ ] split range management out of autospell so it can be used by other +- [X] split range management out of autospell so it can be used by other modules. ### calculator diff --git a/Makefile b/Makefile index 2b676f18..bda901a7 100644 --- a/Makefile +++ b/Makefile @@ -87,7 +87,7 @@ SHOBJ = O/doc-text.o O/doc-dir.o O/doc-docs.o \ O/lib-x11selection-xcb.o O/display-x11-xcb.o \ O/lib-linefilter.o O/lib-wiggle.o O/lib-aspell.o O/lib-calc.o \ O/lib-menu.o O/lib-unicode-names.o O/lib-askpass.o \ - O/lib-test-markup.o O/lib-menubar.o \ + O/lib-test-markup.o O/lib-menubar.o O/lib-rangetrack.o \ O/lang-python.o \ O/mode-emacs.o O/emacs-search.o \ O/display-ncurses.o diff --git a/data/modules.ini b/data/modules.ini index 6f7d7346..3cb81a36 100644 --- a/data/modules.ini +++ b/data/modules.ini @@ -165,3 +165,5 @@ lib-test-markup = lib-menubar = attach-menubar + +lib-rangetrack = rangetrack:new diff --git a/lib-rangetrack.c b/lib-rangetrack.c new file mode 100644 index 00000000..bd47b00e --- /dev/null +++ b/lib-rangetrack.c @@ -0,0 +1,322 @@ +/* + * Copyright Neil Brown ©2023 + * May be distributed under terms of GPLv2 - see file:COPYING + * + * rangetrack: track ranges of a document which have been processed + * in some why, such a spell-check or syntax-highlight or other + * parsing. + * + * rangetrack will attach a pane to the target document to store + * marks and other state. It can track an arbitrary set of different + * range types. + * + * rangetrack:new : start tracking ranges on 'focus' document. + * str is the name of the range set. + * rangetrack:add : record that mark to mark2 are a valid range + * rangetrack:remove : record that from mark to mark2 are no longer valid + * rangetrack:choose : report a subrange for mark..mark2 which is not + * currently valid. + * + */ + +#define PANE_DATA_TYPE struct rangetrack_data + +#include "core.h" +struct rangetrack_data { + struct rci { + const char *set safe; + struct pane *owner safe; + int view; + struct rci *next; + } *info; +}; +#include "core-pane.h" + +static struct rci *find_set(const struct cmd_info *ci safe) +{ + struct rangetrack_data *rtd = ci->home->data; + struct rci *i; + + for (i = rtd->info; i; i = i->next) { + if (ci->str && strcmp(ci->str, i->set) == 0) + return i; + } + return NULL; +} + +static void add_set(struct pane *home safe, const char *set safe, + struct pane *focus safe) +{ + struct rangetrack_data *rtd = home->data; + struct rci *i; + + alloc(i, pane); + i->set = strdup(set); + i->owner = focus; + pane_add_notify(home, focus, "Notify:Close"); + i->view = call("doc:add-view", home) - 1; + i->next = rtd->info; + rtd->info = i; +} + +DEF_CMD(rangetrack_notify_close) +{ + struct rangetrack_data *rtd = ci->home->data; + struct rci **ip; + + for (ip = &rtd->info; *ip; ip = &(*ip)->next) { + struct rci *i; + if ((*ip)->owner != ci->focus) + continue; + i = *ip; + *ip = i->next; + free((void*)i->set); + unalloc(i, pane); + break; + } + return Efallthrough; +} + +DEF_CMD_CLOSED(rangetrack_close) +{ + struct rangetrack_data *rtd = ci->home->data; + struct rci *i; + + while ((i = rtd->info) != NULL) { + rtd->info = i->next; + free((void*)i->set); + unalloc(i, pane); + } + return 1; +} + +DEF_CMD(rangetrack_new) +{ + struct rci *i = find_set(ci); + + if (!ci->str) + return Enoarg; + if (i) + return Efalse; + add_set(ci->home, ci->str, ci->focus); + return 1; +} + +DEF_CMD(rangetrack_add) +{ + struct rci *i = find_set(ci); + struct mark *start = ci->mark; + struct mark *end = ci->mark2; + struct mark *m, *m1, *m2; + + if (!i) + return Efail; + if (!start || !end) + /* Testing if configured already */ + return 1; + m1 = vmark_at_or_before(ci->home, start, i->view, ci->home); + if (m1 && attr_find(m1->attrs, "start")) + m1 = vmark_next(m1); + else if (m1 && mark_same(m1, start)) + /* m1 is an end-of-range. Can move m1 down to cover range */ + ; + else + /* Must create a new mark, or move a later mark up */ + m1 = NULL; + m2 = vmark_at_or_before(ci->home, end, i->view, ci->home); + if (m2 && attr_find(m2->attrs, "start") == NULL) { + if (mark_same(m2, end)) + /* Can move the start of this range earlier */ + m2 = vmark_prev(m2); + else + /* end not in rnage, must create mark or move + * earlier mark down + */ + m2 = NULL; + } + /* If m2, then move it backwards - no need to create */ + if (!m1 && !m2) { + /* no overlaps, create a new region */ + m1 = vmark_new(ci->home, i->view, ci->home); + if (!m1) + return Efail; + mark_to_mark(m1, start); + m2 = vmark_new(ci->home, i->view, ci->home); + if (!m2) + return Efail; + mark_to_mark(m2, end); + attr_set_str(&m1->attrs, "start", "yes"); + } else if (m1 && !m2) { + /* Can move m1 dow n to end, removing anything in the way */ + m = vmark_next(m1); + while (m && mark_ordered_or_same(m, end)) { + mark_free(m); + m = vmark_next(m1); + } + mark_to_mark(m1, end); + } else if (!m1 && m2) { + /* Can move m2 up to start, removing things */ + m = vmark_prev(m2); + while (m && mark_ordered_or_same(start, m)) { + mark_free(m); + m = vmark_prev(m2); + } + mark_to_mark(m2, start); + } else if (m1 && m2) { + /* Can remove all from m1 to m2 inclusive */ + while (m1 && mark_ordered_not_same(m1, m2)) { + m = vmark_next(m1); + mark_free(m1); + m1 = m; + } + mark_free(m2); + } + return 1; +} + +DEF_CMD(rangetrack_clear) +{ + struct rci *i = find_set(ci); + struct mark *start = ci->mark; + struct mark *end = ci->mark2; + struct mark *m1, *m2; + + if (!i) + return Efail; + if (!start || !end) { + start = vmark_first(ci->home, i->view, ci->home); + end = vmark_last(ci->home, i->view, ci->home); + } + if (!start || !end) + return 1; + + m1 = vmark_at_or_before(ci->home, start, i->view, ci->home); + + if (!m1 || attr_find(m1->attrs, "start") == NULL) { + /* Immediately after start is not active, so the + * earlierst we might need to remove is the next + * mark, or possibly the very first mark. + */ + if (m1) + m1 = vmark_next(m1); + else + m1 = vmark_first(ci->home, i->view, ci->home); + if (!m1 || mark_ordered_or_same(end, m1)) + /* Nothing to remove */ + return 1; + } else { + /* From m1 to start are in a range and should stay + * there. Split the range from 'm1' at 'start' + */ + m1 = vmark_new(ci->home, i->view, ci->home); + if (!m1) + return Efail; + mark_to_mark(m1, start); + m1 = mark_dup_view(m1); + /* Ensure this m1 is after the previous one */ + mark_step(m1, 1); + attr_set_str(&m1->attrs, "start", "yes"); + } + /* m is now the start of an active section that is within + * start-end and should be removed */ + m2 = vmark_at_or_before(ci->home, end, i->view, ci->home); + if (m2 && mark_same(m2, end) && attr_find(m2->attrs, "start")) + /* This section is entirely after end, so not interesting */ + m2 = vmark_prev(m2); + if (m2 && attr_find(m2->attrs, "start")) { + /* end is within an active secion that needs to be split */ + m2 = vmark_new(ci->home, i->view, ci->home); + if (!m2) + return Efail; + mark_to_mark(m2, end); + attr_set_str(&m2->attrs, "start", "yes"); + m2 = mark_dup_view(m2); + mark_step(m2, 0); + } + if (!m2) + return Efail; + /* m2 is now the end of an active section that needs to bie discarded */ + while (m1 && mark_ordered_not_same(m1, m2)) { + struct mark *m = m1; + m1 = vmark_next(m1); + mark_free(m); + } + mark_free(m2); + call(strconcat(ci->home, "doc:notify:rangetrack:recheck-", i->set), + ci->home); + return 1; +} + +DEF_CMD(rangetrack_choose) +{ + struct rci *i = find_set(ci); + struct mark *start = ci->mark; + struct mark *end = ci->mark2; + struct mark *m1, *m2; + + if (!i) + return Efail; + if (!start || !end) + return Enoarg; + /* Contract start-end so that none of it is in-range */ + m1 = vmark_at_or_before(ci->home, start, i->view, ci->home); + if (m1 && attr_find(m1->attrs, "start") == NULL) + /* Start is not in-range, end must not exceed m1 */ + m2 = vmark_next(m1); + else if (m1) { + /* m1 is in-range, move it forward */ + m1 = vmark_next(m1); + if (m1) { + mark_to_mark(start, m1); + m2 = vmark_next(m1); + } else { + /* Should be impossible */ + m2 = start; + } + } else { + /* Start is before all ranges */ + m2 = vmark_first(ci->home, i->view, ci->home); + } + if (m2 && mark_ordered_not_same(m2, end)) + mark_to_mark(end, m2); + return 1; +} + +static struct map *rangetrack_map safe; +DEF_LOOKUP_CMD(rangetrack_handle, rangetrack_map); + +DEF_CMD(rangetrack_attach) +{ + struct pane *doc = call_ret(pane, "doc:get-doc", ci->focus); + const char *set = ci->str; + struct pane *p; + + if (!set) + return Enoarg; + if (!doc) + return Efail; + if (call("doc:notify:rangetrack:new", ci->focus, 0, NULL, set) > 0) + return 1; + p = pane_register(doc, 0, &rangetrack_handle.c); + if (!p) + return Efail; + pane_add_notify(p, doc, "rangetrack:new"); + pane_add_notify(p, doc, "rangetrack:add"); + pane_add_notify(p, doc, "rangetrack:clear"); + pane_add_notify(p, doc, "rangetrack:choose"); + add_set(p, set, ci->focus); + return 1; +} + +void edlib_init(struct pane *ed safe) +{ + call_comm("global-set-command", ed, &rangetrack_attach, + 0, NULL, "rangetrack:new"); + rangetrack_map = key_alloc(); + key_add(rangetrack_map, "Close", &rangetrack_close); + key_add(rangetrack_map, "Notify:Close", &rangetrack_notify_close); + key_add(rangetrack_map, "rangetrack:new", &rangetrack_new); + key_add(rangetrack_map, "rangetrack:add", &rangetrack_add); + key_add(rangetrack_map, "rangetrack:clear", &rangetrack_clear); + key_add(rangetrack_map, "rangetrack:choose", &rangetrack_choose); +} diff --git a/python/lib-autospell.py b/python/lib-autospell.py index f68d4d36..79c6c3d8 100644 --- a/python/lib-autospell.py +++ b/python/lib-autospell.py @@ -15,185 +15,11 @@ # We add ranges when a word is checked. This might merge with existing # ranges. # We need to find an unchecked section in some range to start checking. -# This is all managed by: -# remove_range(focus, viewnum, attr, start, end) -# add_range(focus, viewnum, attr, start, end) -# choose_range(focus, viewnum, attr, start, end) - changes start and end to be -# a contiguous unchecked section in the range +# This is all managed with the help of rangetrack import edlib import os -def show_range(action, focus, viewnum, attr): - edlib.LOG("range:", attr, action) - f,l = focus.vmarks(viewnum) - while f: - edlib.LOG(" ", f, f[attr]) - f = f.next() - edlib.LOG("done", action) - -def remove_range(focus, viewnum, attr, start, end): - m = focus.vmark_at_or_before(viewnum, start) - if not m or not m[attr]: - # Immediately after start is not active, so the earliest we might need - # to remove is the next mark, or possibly the very first - if m: - m = m.next() - else: - m, l = focus.vmarks(viewnum) - if not m or m >= end: - # Nothing to remove - return - elif start < m: - # From m to start are in a range and should stay there. - # Split the range from 'm' at 'start' - m = edlib.Mark(focus, view = viewnum) - m.to_mark(start) - m = edlib.Mark(orig=m, owner = focus) - # ensure the m is after the previous one - m.step(1) - m[attr] = 'yes' - # m is now the start of an active section that is within start-end - # and should be removed - m2 = focus.vmark_at_or_before(viewnum, end) - if m2 and m2 == end and m2[attr]: - # this section is entirely after end, so not interesting - m2 = m2.prev() - if m2 and m2[attr]: - # end is within an active section that needs to be split - m2 = edlib.Mark(focus, view = viewnum) - m2.to_mark(end) - m2[attr] = 'yes' - m2 = edlib.Mark(orig=m2, owner=focus) - m2.step(0) - # m2 is now the end of an active section that needs to be - # discarded - while m < m2: - old = m - m = m.next() - old.release() - m2.release() - return - -def add_range(focus, viewnum, attr, start, end): - m1 = focus.vmark_at_or_before(viewnum, start) - if m1 and m1[attr]: - m1 = m1.next() - # Range ending at m1 can be extended to cover start->end - elif m1 and m1 == start: - # can move m1 down to cover range - pass - else: - m1 = None - # must create new mark, or move a later mark up - m2 = focus.vmark_at_or_before(viewnum, end) - if m2 and not m2[attr]: - if m2 == end: - m2 = m2.prev() - # can move m2 earlier - else: - # end not in range, must create mark or move earlier down - m2 = None - # if m2, then can move it backwards. No need to create - if not m1 and not m2: - # no overlaps, create new region - m1 = edlib.Mark(focus, viewnum) - m1.to_mark(start) - m2 = edlib.Mark(orig=m1, owner=focus) - m2.to_mark(end) - m1[attr] = 'yes' - elif m1 and not m2: - # can move m1 down to end, removing anything in the way - m = m1.next() - while m and m <= end: - m.release() - m = m1.next() - m1.to_mark(end) - elif not m1 and m2: - # can move m2 up to start, removing things - m = m2.prev() - while m and m >= start: - m.release() - m = m2.prev() - m2.to_mark(start) - else: - # can remove all from m1 to m2 inclusive - while m1 < m2: - m = m1.next() - m1.release() - m1 = m - m2.release() - -def choose_range(focus, viewnum, attr, start, end): - # contract start-end so that none of it is in-range - m1 = focus.vmark_at_or_before(viewnum, start) - if m1 and not m1[attr]: - m2 = m1.next() - # start not in-range, end must not exceed m1 - elif m1 and m1[attr]: - # start is in range - move it forward - m1 = m1.next() - if m1: - start.to_mark(m1) - m2 = m1.next() - else: - # error - m2 = start - else: - m2, l = focus.vmarks(viewnum) - if m2 and m2 < end: - end.to_mark(m2) - -class autospell_monitor(edlib.Pane): - # autospell_monitor attaches to a document and tracks ranges - # that have been spell-checked. Sends notifications when there - # is a change, so viewers can do the checking. Only views know - # mode-specific details - def __init__(self, focus): - edlib.Pane.__init__(self, focus) - self.view = self.call("doc:add-view") - 1 - self.call("doc:request:doc:replaced") - self.call("doc:request:spell:mark-checked") - self.call("doc:request:spell:choose-range") - self.call("doc:request:spell:dict-changed") - - def doc_replace(self, key, focus, mark, mark2, num2, **a): - "handle-list/doc:replaced/spell:dict-changed" - if not mark: - mark = edlib.Mark(focus) - focus.call("doc:set-ref", mark, 1); - if not mark2: - mark2 = edlib.Mark(focus) - focus.call("doc:set-ref", mark2, 0); - - # mark2 might have been the start-of-word, but not any longer - # So any spell-incorrect must be cleared as normal checking - # only affects first char of a word. - focus.call("doc:set-attr", mark2, "render:spell-incorrect", - None); - # Need to capture adjacent words, and avoid zero-size gap - mark = mark.dup() - focus.prev(mark) - mark2 = mark2.dup() - focus.next(mark2) - - remove_range(self, self.view, "spell:start", mark, mark2) - - self.call("doc:notify:spell:recheck") - return 1 - - def handle_checked(self, key, mark, mark2, **a): - "handle:spell:mark-checked" - if mark and mark2: - add_range(self, self.view, 'spell:start', mark, mark2) - return 1 - - def handle_choose(self, key, mark, mark2, **a): - "handle:spell:choose-range" - if mark and mark2: - choose_range(self, self.view, 'spell:start', mark, mark2) - return 1 - class autospell_view(edlib.Pane): def __init__(self, focus): edlib.Pane.__init__(self, focus) @@ -203,8 +29,9 @@ class autospell_view(edlib.Pane): self.vstart = None self.vend = None self.menu = None - self.call("doc:request:spell:recheck") + self.call("doc:request:rangetrack:recheck-autospell") self.call("doc:request:doc:replaced") + self.call("doc:request:spell:dict-changed") # trigger render-lines refresh notification pt = focus.call("doc:point", ret='mark') focus.call("render:request:reposition", pt) @@ -228,7 +55,8 @@ class autospell_view(edlib.Pane): if not str1 or not mark or not comm2: return edlib.Efallthrough if str1 == "render:spell-incorrect": - comm2("cb", focus, int(str2), mark, "fg:red-80,underline,action-menu:autospell-menu", 120) + comm2("cb", focus, int(str2), mark, + "fg:red-80,underline,action-menu:autospell-menu", 120) return edlib.Efallthrough def handle_click(self, key, focus, mark, xy, str1, **a): @@ -242,7 +70,8 @@ class autospell_view(edlib.Pane): self.thisword = w mp.call("menu-add", "[Insert in dict]", "+") mp.call("menu-add", "[Accept for now]", "!") - focus.call("Spell:Suggest", w, lambda key, str1, **a: mp.call("menu-add", str1)) + focus.call("Spell:Suggest", w, + lambda key, str1, **a: mp.call("menu-add", str1)) mp.call("doc:file", -1) self.menu = mp self.add_notify(mp, "Notify:Close") @@ -276,20 +105,45 @@ class autospell_view(edlib.Pane): return 1 def handle_recheck(self, key, **a): - "handle:spell:recheck" + "handle:rangetrack:recheck-autospell" self.sched() + return 1 + + def handle_dict_changed(self, key, focus, mark, mark2, num2, **a): + "handle:spell:dict-changed" + # clear everything + self.call("doc:notify:rangetrack:clear", "autospell") + return 1 def handle_replace(self, key, focus, mark, mark2, num2, **a): "handle:doc:replaced" if not mark or not mark2: + # clear everything + self.call("doc:notify:rangetrack:clear", "autospell") return 1 + + # mark2 might have been the start-of-word, but not any longer + # So any spell-incorrect must be cleared as normal checking + # only affects first char of a word. + focus.call("doc:set-attr", mark2, "render:spell-incorrect", + None); + # if change at either end of view, extend view until reposition message + # If we don't get a render:resposition message then probably the + # rendered didn't see the mark move, but we did, because the change + # was between the marks? Should I test more precisely for that FIXME if mark < self.vstart and mark2 >= self.vstart: self.vstart.to_mark(mark) - self.sched() if mark2 > self.vend and mark <= self.vend: self.vend.to_mark(mark2) - self.sched() + + # Need to capture adjacent words, and avoid zero-size gap + mark = mark.dup() + focus.prev(mark) + mark2 = mark2.dup() + focus.next(mark2) + self.call("doc:notify:rangetrack:clear", "autospell", mark, mark2) + return 1 def reposition(self, key, mark, mark2, **a): @@ -298,9 +152,12 @@ class autospell_view(edlib.Pane): self.vstart = mark.dup() self.vend = mark2.dup() if (not self.helper_attached and - not self.call("doc:notify:spell:mark-checked")): - self.call("doc:get-doc", autospell_attach_helper) - self.helper_attached = True + not self.call("doc:notify:rangetrack:add", + "autospell")): + if self.call("rangetrack:new", "autospell") > 0: + self.helper_attached = True + else: + pass # FIXME self.sched() return edlib.Efallthrough @@ -315,7 +172,8 @@ class autospell_view(edlib.Pane): return edlib.Efalse start = self.vstart.dup() end = self.vend.dup() - self.call("doc:notify:spell:choose-range", start, end) + self.call("doc:notify:rangetrack:choose", "autospell", + start, end) if start >= end: # nothing to do return edlib.Efalse @@ -334,7 +192,8 @@ class autospell_view(edlib.Pane): focus.call("Spell:NextWord", ed) st = ed.dup() word = focus.call("Spell:ThisWord", ed, st, ret='str') - self.call("doc:notify:spell:mark-checked", start, ed) + self.call("doc:notify:rangetrack:add", "autospell", + start, ed) start = ed if word: ret = focus.call("Spell:Check", word) @@ -357,10 +216,6 @@ def autospell_attach(key, focus, comm2, **a): comm2("callback", p) return 1 -def autospell_attach_helper(key, focus, **a): - p = autospell_monitor(focus) - return 1 - def autospell_activate(key, focus, comm2, **a): autospell_view(focus) @@ -370,4 +225,4 @@ def autospell_activate(key, focus, comm2, **a): edlib.editor.call("global-set-command", "attach-autospell", autospell_attach) edlib.editor.call("global-set-command", "interactive-cmd-autospell", - autospell_activate) + autospell_activate) -- 2.39.5