]> git.neil.brown.name Git - edlib.git/blobdiff - lib-linecount.c
TODO: clean out done items.
[edlib.git] / lib-linecount.c
index 771b920415a5dbe0288f45ab6a01da6fc52bbd88..98ef67dcefe48ae35b98a69c8bd000bd047940ee 100644 (file)
@@ -1,15 +1,16 @@
 /*
- * Copyright Neil Brown ©2015-2022 <neil@brown.name>
+ * Copyright Neil Brown ©2015-2023 <neil@brown.name>
  * May be distributed under terms of GPLv2 - see file:COPYING
  *
  * line/word/char count.
  *
  * This module can be attached to a Document to count lines/words/chars.
  *
- * It attaches active marks every 100 lines or so and records the
- * counts between the marks.  These are stored as attributes
- * 'lines' 'words' 'chars'.
- * When a change is notified, the attributes are cleared.
+ * It attaches an active mark at the start, then one every 100 lines or so
+ * and records the counts between the marks.  These are stored as attributes
+ * 'lines' 'words' 'chars' on the mark at the start of the range.
+ * When a change is notified, the attributes one the preceeding
+ * mark are cleared.
  * When a count is requested, all marks from top-of-file to target
  * are examined.  If attributes are not present they are calculated.
  * Then they are summed.
  *
  * When CountLines is called on a doc-pane, pane attributes are set
  * to record the number of lines, words, chars.
- * When it is calld on a mark in the pane attributes are set on the
+ * When it is called on a mark in the pane, attributes are set on the
  * mark to indicate the line, work and char where the mark is.
  * These are always at least 1.
+ *
+ * Alternately, the pane can be attaching in the view stack so that it
+ * applies to the view rather than the document.  This is useful when
+ * There are views imposed that dramatically alter the number of
+ * lines/words, or that hide parts of the document that really shouldn't
+ * be counted.  The view on an RFC2822 email or the results of a notmuch
+ * search are good and current examples.
  */
 
 #include <unistd.h>
 #include <wctype.h>
 #include <string.h>
 
+#define PANE_DATA_TYPE struct count_info
 #include "core.h"
+struct count_info {
+       int view_num;
+};
+#include "core-pane.h"
 
 static struct map *linecount_map;
 DEF_LOOKUP_CMD(handle_count_lines, linecount_map);
 
-struct count_info {
-       int view_num;
+static const int batch_marks = 10;
+
+struct clcb {
+       int lines, words, chars;
+       int inword;
+       int *linep safe, *wordp safe, *charp safe;
+       int add_marks;
+       struct mark *start;
+       struct mark *end;
+       struct command c;
+       struct pane *owner safe;
 };
 
-static void do_count(struct pane *p safe, struct mark *start safe, struct mark *end,
+DEF_CB(clcb)
+{
+       struct clcb *cl = container_of(ci->comm, struct clcb, c);
+       wint_t ch = ci->num;
+       struct mark *m = ci->mark;
+       struct count_info *cli = cl->owner->data;
+       const char *s;
+       int i = 0;
+
+       if (!m)
+               return Enoarg;
+
+       while (1) {
+               cl->chars += 1;
+               if (is_eol(ch))
+                       cl->lines += 1;
+               if (!cl->inword && (iswprint(ch) && !iswspace(ch))) {
+                       cl->inword = 1;
+                       cl->words += 1;
+               } else if (cl->inword && !(iswprint(ch) && !iswspace(ch)))
+                       cl->inword = 0;
+               if (cl->add_marks &&
+                   cl->start &&
+                   (cl->lines >= 100 || cl->words >= 1000 || cl->chars >= 10000 ||
+                    pane_too_long(cl->owner, 0)))
+                       break;
+               if (!ci->str || i >= ci->num2)
+                       return i+1;
+               s = ci->str + i;
+               ch = get_utf8(&s, ci->str + ci->num2);
+               if (ch == WEOF || ch == WERR)
+                       return i+1;
+               i = s - ci->str;
+       }
+
+       if (i > 0) {
+               /* m isn't where we are, so we cannot update
+                * anything yet - need to return an get called again
+                */
+               return i+1;
+       }
+       attr_set_int(mark_attr(cl->start), "lines", cl->lines);
+       attr_set_int(mark_attr(cl->start), "words", cl->words);
+       attr_set_int(mark_attr(cl->start), "chars", cl->chars);
+       *cl->linep += cl->lines;
+       *cl->wordp += cl->words;
+       *cl->charp += cl->chars;
+       cl->lines = 0;
+       cl->words = 0;
+       cl->chars = 0;
+       cl->start = vmark_new(ci->focus, cli->view_num, cl->owner);
+       if (cl->start)
+               mark_to_mark(cl->start, m);
+       if (cl->add_marks > 1 && pane_too_long(cl->owner, 0))
+               cl->add_marks = 1;
+       cl->add_marks -= 1;
+       if (!cl->add_marks)
+               /* Added enough marks, abort */
+               return Efalse;
+       return 1;
+}
+
+static void do_count(struct pane *p safe, struct pane *owner safe,
+                    struct mark *start safe, struct mark *end,
                     int *linep safe, int *wordp safe, int *charp safe, int add_marks)
 {
        /* if 'end' is NULL, go all the way to EOF */
-       int lines = 0;
-       int words = 0;
-       int chars = 0;
-       int inword = 0;
-       wint_t ch;
-       struct mark *m;
+       struct clcb cl;
 
-       if (add_marks)
-               m = mark_dup_view(start);
-       else
-               m = mark_dup(start);
+       cl.lines = 0;
+       cl.words = 0;
+       cl.chars = 0;
+       cl.inword = 0;
+       cl.end = end;
+       cl.start = start;
+       cl.add_marks = add_marks;
+       cl.c = clcb;
+       cl.linep = linep;
+       cl.wordp = wordp;
+       cl.charp = charp;
+       cl.owner = owner;
 
        *linep = 0;
        *wordp = 0;
        *charp = 0;
-       while ((end == NULL || (mark_ordered_not_same(m, end))) &&
-              (ch = doc_next(p, m)) != WEOF) {
-               chars += 1;
-               if (is_eol(ch))
-                       lines += 1;
-               if (!inword && (iswprint(ch) && !iswspace(ch))) {
-                       inword = 1;
-                       words += 1;
-               } else if (inword && !(iswprint(ch) && !iswspace(ch)))
-                       inword = 0;
-               if (add_marks &&
-                   (lines >= 100 || words > 1000 || chars > 10000) &&
-                   (end == NULL || (mark_ordered_not_same(m, end)))) {
-                       /* leave a mark here and keep going */
-                       attr_set_int(mark_attr(start), "lines", lines);
-                       attr_set_int(mark_attr(start), "words", words);
-                       attr_set_int(mark_attr(start), "chars", chars);
-                       start = m;
-                       *linep += lines;
-                       *wordp += words;
-                       *charp += chars;
-                       lines = words = chars = 0;
-                       m = mark_dup_view(m);
-               }
+       if (call_comm("doc:content", p, &cl.c, 0, start, NULL, 0, end) <= 0 ||
+           (add_marks && cl.add_marks == 0))
+               return;
+
+       if (cl.add_marks && cl.start && cl.start != start && cl.chars == 0) {
+               mark_free(cl.start);
+               cl.start = NULL;
        }
-       if (add_marks) {
-               attr_set_int(mark_attr(start), "lines", lines);
-               attr_set_int(mark_attr(start), "words", words);
-               attr_set_int(mark_attr(start), "chars", chars);
+       if (cl.add_marks && cl.start) {
+               attr_set_int(mark_attr(cl.start), "lines", cl.lines);
+               attr_set_int(mark_attr(cl.start), "words", cl.words);
+               attr_set_int(mark_attr(cl.start), "chars", cl.chars);
        }
-       *linep += lines;
-       *wordp += words;
-       *charp += chars;
-       mark_free(m);
+       *linep += cl.lines;
+       *wordp += cl.words;
+       *charp += cl.chars;
+}
+
+DEF_CMD(linecount_restart)
+{
+       pane_call(ci->home, "CountLinesAsync", pane_focus(ci->focus), 1);
+       return Efalse;
 }
 
 static int need_recalc(struct pane *p safe, struct mark *m)
@@ -104,83 +180,81 @@ static int need_recalc(struct pane *p safe, struct mark *m)
                return 1;
        if (!attr_find(*mark_attr(m), "lines"))
                ret = 1;
-       while (1) {
-               next = vmark_next(m);
-               if (!next)
-                       break;
-               if (is_eol(doc_prior(p, next)) &&
-                   attr_find_int(*mark_attr(next), "lines") > 10)
-                       break;
-               /* discard next - we'll find or create another */
+       next = vmark_next(m);
+       if (next && attr_find_int(*mark_attr(m), "lines") < 20) {
+               /* This is tiny, recalc */
+               attr_del(mark_attr(m), "lines");
                mark_free(next);
                ret = 1;
        }
+       if (ret)
+               /* The background task needs to be stopped */
+               call_comm("event:free", p, &linecount_restart);
        return ret;
 }
 
 static void count_calculate(struct pane *p safe,
-                           struct mark *start, struct mark *end,
-                           struct pane *owner safe, int type)
+                           struct mark *end,
+                           struct pane *owner safe, int type,
+                           bool sync)
 {
        int lines, words, chars, l, w, c;
        struct mark *m, *m2;
+       char *disable;
+
+       if (edlib_testing(p))
+               sync = True;
 
+       disable = pane_attr_get(p, "linecount-disable");
+       if (disable && strcmp(disable, "yes") == 0) {
+               if (end) {
+                       attr_set_str(&end->attrs, "line", "??");
+                       attr_set_str(&end->attrs, "word", "??");
+                       attr_set_str(&end->attrs, "char", "??");
+               }
+               attr_set_str(&p->attrs, "lines", "-");
+               attr_set_str(&p->attrs, "words", "-");
+               attr_set_str(&p->attrs, "chars", "-");
+               return;
+       }
+
+       if (!end && attr_find(p->attrs, "lines"))
+               /* nothing to do */
+               return;
+
+       if (end && !attr_find(p->attrs, "lines") && !sync)
+               /* We don't have totals, so do that first.
+                * When asked again, we will be able to find
+                * the mark quickly.
+                */
+               end = NULL;
+
+       pane_set_time(owner);
        m = vmark_first(p, type, owner);
-       if (m == NULL) {
-               /* No marks yet, let's make some */
+       if (m == NULL || doc_prior(p, m) != WEOF) {
+               /* No mark at doc start, make some */
                m = vmark_new(p, type, owner);
                if (!m)
                        return;
-               do_count(p, m, NULL, &l, &w, &c, 1);
-       }
-       if (doc_prior(p, m) != WEOF) {
-               /* no mark at start of file */
-               m2 = vmark_new(p, type, owner);
-               if (!m2)
+               call("doc:set-ref", p, 1, m);
+               do_count(p, owner, m, vmark_next(m), &l, &w, &c, sync ? -1 : batch_marks);
+               if (!sync) {
+                       call_comm("event:on-idle", owner, &linecount_restart);
                        return;
-               do_count(p, m2, m, &l, &w, &c, 1);
-               m = m2;
-       }
-
-       if (start) {
-               /* find the first mark that isn't before 'start', and count
-                * from there.
-                */
-               while (m && mark_ordered_not_same(m, start)) {
-                       /* Force and update to make sure spacing stays sensible */
-                       if (need_recalc(p, m))
-                               /* need to update this one */
-                               do_count(p, m, vmark_next(m), &l, &w, &c, 1);
-
-                       m = vmark_next(m);
-               }
-               if (!m) {
-                       /* fell off the end, just count directly */
-                       do_count(p, start, end, &lines, &words, &chars, 0);
-                       goto done;
                }
        }
-       if (need_recalc(p, m))
-               /* need to update this one */
-               do_count(p, m, vmark_next(m), &l, &w, &c, 1);
 
-       /* 'm' is not before 'start', it might be after.
-        * if 'm' is not before 'end' either, just count from
-        * start to end.
-        */
-       if (end && m->seq >= end->seq) {
-               do_count(p, start?:m, end, &lines, &words, &chars, 0);
-               goto done;
+       if (need_recalc(owner, m)) {
+               /* need to update this one */
+               do_count(p, owner, m, vmark_next(m), &l, &w, &c, sync ? -1 : batch_marks);
+               if (!sync) {
+                       call_comm("event:on-idle", owner, &linecount_restart);
+                       return;
+               }
        }
-
-       /* OK, 'm' is between 'start' and 'end'.
-        * So count from start to m, then add totals from m and subsequent.
-        * Then count to 'end'.
+       /* Add totals from m to before end. Then count to 'end'.
         */
-       if (!start || mark_same(m, start))
-               lines = words = chars = 0;
-       else
-               do_count(p, start, m, &lines, &words, &chars, 0);
+       lines = words = chars = 0;
        while ((m2 = vmark_next(m)) != NULL &&
               (!end || m2->seq < end->seq)) {
                /* Need everything from m to m2 */
@@ -188,8 +262,13 @@ static void count_calculate(struct pane *p safe,
                words += attr_find_int(*mark_attr(m), "words");
                chars += attr_find_int(*mark_attr(m), "chars");
                m = m2;
-               if (need_recalc(p, m))
-                       do_count(p, m, vmark_next(m), &l, &w, &c, 1);
+               if (!need_recalc(owner, m))
+                       continue;
+               do_count(p, owner, m, vmark_next(m), &l, &w, &c, sync ? -1 : batch_marks);
+               if (!sync || pane_too_long(owner, 0)) {
+                       call_comm("event:on-idle", owner, &linecount_restart);
+                       return;
+               }
        }
        /* m is the last mark before end */
        if (!end) {
@@ -197,12 +276,12 @@ static void count_calculate(struct pane *p safe,
                words += attr_find_int(*mark_attr(m), "words");
                chars += attr_find_int(*mark_attr(m), "chars");
        } else if (!mark_same(m, end)) {
-               do_count(p, m, end, &l, &w, &c, 0);
+               do_count(p, owner, m, end, &l, &w, &c, 0);
                lines += l;
                words += w;
                chars += c;
        }
-done:
+
        if (end) {
                struct attrset **attrs = &end->attrs;
                attr_set_int(attrs, "line", lines + 1);
@@ -212,6 +291,8 @@ done:
                attr_set_int(&p->attrs, "lines", lines);
                attr_set_int(&p->attrs, "words", words);
                attr_set_int(&p->attrs, "chars", chars);
+               if (!edlib_testing(p))
+                       pane_notify("doc:status-changed", p);
        }
 }
 
@@ -220,6 +301,8 @@ DEF_CMD(linecount_close)
        struct pane *d = ci->focus;
        struct count_info *cli = ci->home->data;
        struct mark *m;
+
+       call_comm("event:free", ci->home, &linecount_restart);
        while ((m = vmark_first(d, cli->view_num, ci->home)) != NULL)
                mark_free(m);
        home_call(d, "doc:del-view", ci->home, cli->view_num);
@@ -231,28 +314,63 @@ DEF_CMD(linecount_notify_replace)
 {
        struct pane *d = ci->focus;
        struct count_info *cli = ci->home->data;
-       if (ci->mark) {
-               struct mark *end;
+       struct mark *m, *m2;
 
-               end = vmark_at_or_before(d, ci->mark, cli->view_num, ci->home);
-               if (end) {
-                       attr_del(mark_attr(end), "lines");
-                       attr_del(mark_attr(end), "words");
-                       attr_del(mark_attr(end), "chars");
-               }
-               attr_del(&d->attrs, "lines");
-               attr_del(&d->attrs, "words");
-               attr_del(&d->attrs, "chars");
-       }
-       return 1;
+       if (ci->mark && !ci->mark2)
+               /* I might not care about this one... */
+               return Efallthrough;
+
+       attr_del(&d->attrs, "lines");
+       attr_del(&d->attrs, "words");
+       attr_del(&d->attrs, "chars");
+
+       if (ci->mark)
+               m = vmark_at_or_before(d, ci->mark, cli->view_num, ci->home);
+       else
+               m = vmark_first(d, cli->view_num, ci->home);
+       if (!m)
+               return Efallthrough;
+
+       attr_del(mark_attr(m), "lines");
+       attr_del(mark_attr(m), "words");
+       attr_del(mark_attr(m), "chars");
+
+       while ((m2 = vmark_next(m)) != NULL &&
+              (!ci->mark2 || mark_ordered_or_same(m2, ci->mark2)))
+               mark_free(m2);
+
+       call_comm("event:free", ci->home, &linecount_restart);
+       return Efallthrough;
 }
 
 DEF_CMD(linecount_notify_count)
 {
        struct pane *d = ci->focus;
        struct count_info *cli = ci->home->data;
-       /* Option mark is "mark2" as "mark" gets the "point" */
-       count_calculate(d, NULL, ci->mark2, ci->home, cli->view_num);
+       /* Option mark is "mark2" as "mark" gets the "point" so is never NULL */
+       /* num==1 means we don't want to wait for precision */
+       bool sync = ci->mark2 && ci->num != 1;
+
+       count_calculate(d, ci->mark2, ci->home, cli->view_num,
+                       sync);
+       return 1;
+}
+
+DEF_CMD(linecount_view_count)
+{
+       struct pane *d = ci->focus;
+       struct count_info *cli = ci->home->data;
+       bool sync = strcmp(ci->key, "CountLines") == 0;
+
+       if (strcmp(ci->key, "CountLinesAsync") == 0)
+               sync = False;
+
+       if (ci->mark && ci->str && strcmp(ci->str, "goto:line") == 0 &&
+           ci->num != NO_NUMERIC) {
+               pane_call(ci->home, "doc:GotoLine", d, ci->num, ci->mark);
+       }
+       count_calculate(d, ci->mark, ci->home, cli->view_num,
+                       sync);
        return 1;
 }
 
@@ -267,7 +385,8 @@ DEF_CMD(linecount_notify_goto)
        if (!ci->mark)
                return 1;
 
-       /* FIXME I might need to recalculate here */
+       /* Ensure counts are up-to-date */
+       count_calculate(d, NULL, ci->home, cli->view_num, True);
        m = vmark_first(d, cli->view_num, ci->home);
        if (!m)
                return 1;
@@ -294,48 +413,74 @@ DEF_CMD(linecount_notify_goto)
 
 DEF_CMD(count_lines)
 {
-       char *type = pane_attr_get(ci->focus, "doc-type");
-       char *view = pane_attr_get(ci->focus, "view-default");
-       /* FIXME this type-check is a HACK */
-       if (type && strcmp(type, "email") == 0)
-               return 1;
-       if (view && strcmp(view, "make-viewer") == 0)
-               return 1;
+       int async = strcmp(ci->key, "CountLinesAsync") == 0;
+
        /* FIXME optimise this away most of the time */
-       if (call("doc:notify:doc:CountLines", ci->focus) == 0) {
+       if (call("doc:notify:doc:CountLines", ci->focus, 1) == 0) {
                /* No counter in place, add one */
                struct count_info *cli;
                struct pane *p;
 
-               alloc(cli, pane);
-               p = pane_register(NULL, 0, &handle_count_lines.c, cli);
+               p = pane_register(pane_root(ci->focus), 0,
+                                 &handle_count_lines.c);
                if (!p)
                        return Efail;
+               cli = p->data;
                cli->view_num = home_call(ci->focus, "doc:add-view", p) - 1;
                home_call(ci->focus, "doc:request:doc:replaced", p);
                home_call(ci->focus, "doc:request:doc:CountLines", p);
                home_call(ci->focus, "doc:request:doc:GotoLine", p);
                home_call(ci->focus, "doc:request:Notify:Close", p);
-               call("doc:notify:doc:CountLines", ci->focus, 0, ci->mark);
+               call("doc:notify:doc:CountLines", ci->focus, 1);
        }
        if (ci->mark) {
                if (ci->str && strcmp(ci->str, "goto:line") == 0 &&
                    ci->num != NO_NUMERIC) {
-                       call("doc:notify:doc:GotoLine", ci->focus, ci->num, NULL, NULL,
-                            0, ci->mark);
+                       call("doc:notify:doc:GotoLine", ci->focus, ci->num,
+                            ci->mark);
                }
-               call("doc:notify:doc:CountLines", ci->focus, 0, NULL, NULL,
+               call("doc:notify:doc:CountLines", ci->focus, async, NULL, NULL,
                     0, ci->mark);
        }
        if (ci->mark2)
-               call("doc:notify:doc:CountLines", ci->focus, 0, NULL, NULL,
+               call("doc:notify:doc:CountLines", ci->focus, async, NULL, NULL,
                     0, ci->mark2);
        return 1;
 }
 
+DEF_CMD(linecount_attach)
+{
+       struct count_info *cli;
+       struct pane *p;
+
+       p = pane_register(ci->focus, 0, &handle_count_lines.c);
+       if (!p)
+               return Efail;
+       cli = p->data;
+       cli->view_num = home_call(p, "doc:add-view", p) - 1;
+       call("doc:request:doc:replaced", p);
+       call("doc:request:Notify:Close", p);
+       call_comm("event:on-idle", p, &linecount_restart, 1);
+
+       comm_call(ci->comm2, "cb", p);
+       return 1;
+}
+
+DEF_CMD(linecount_clone)
+{
+       struct pane *p;
+
+       p = comm_call_ret(pane, &linecount_attach, "attach", ci->focus);
+       pane_clone_children(ci->home, p);
+       return 1;
+}
+
 void edlib_init(struct pane *ed safe)
 {
        call_comm("global-set-command", ed, &count_lines, 0, NULL, "CountLines");
+       call_comm("global-set-command", ed, &count_lines, 0, NULL, "CountLinesAsync");
+       call_comm("global-set-command", ed, &linecount_attach, 0, NULL,
+                 "attach-line-count");
 
        if (linecount_map)
                return;
@@ -345,5 +490,10 @@ void edlib_init(struct pane *ed safe)
        key_add(linecount_map, "doc:replaced", &linecount_notify_replace);
        key_add(linecount_map, "doc:CountLines", &linecount_notify_count);
        key_add(linecount_map, "doc:GotoLine", &linecount_notify_goto);
-       key_add(linecount_map, "Free", &edlib_do_free);
+
+       /* For view-attached version */
+       key_add(linecount_map, "CountLines", &linecount_view_count);
+       key_add(linecount_map, "CountLinesAsync", &linecount_view_count);
+       //key_add(linecount_map, "view:changed", &linecount_notify_replace);
+       key_add(linecount_map, "Clone", &linecount_clone);
 }