--- /dev/null
+/*
+ * Copyright Neil Brown ©2023 <neil@brown.name>
+ * 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);
+}
# 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)
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)
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):
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")
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):
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
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
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)
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)
edlib.editor.call("global-set-command", "attach-autospell", autospell_attach)
edlib.editor.call("global-set-command", "interactive-cmd-autospell",
- autospell_activate)
+ autospell_activate)