2 * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3 * May be distributed under terms of GPLv2 - see file:COPYING
5 * line/word/char count.
7 * This module can be attached to a Document to count lines/words/chars.
9 * It attaches an active mark at the start, then one every 100 lines or so
10 * and records the counts between the marks. These are stored as attributes
11 * 'lines' 'words' 'chars' on the mark at the start of the range.
12 * When a change is notified, the attributes one the preceeding
14 * When a count is requested, all marks from top-of-file to target
15 * are examined. If attributes are not present they are calculated.
16 * Then they are summed.
17 * The text from the last active mark at the target is always calculated.
19 * When recalculating a range, we drop a new mark every 100 lines.
20 * When we find a mark the needs updating, we discard it if previous mark is
21 * closer than 10 lines.
23 * When CountLines is called on a doc-pane, pane attributes are set
24 * to record the number of lines, words, chars.
25 * When it is called on a mark in the pane, attributes are set on the
26 * mark to indicate the line, work and char where the mark is.
27 * These are always at least 1.
29 * Alternately, the pane can be attaching in the view stack so that it
30 * applies to the view rather than the document. This is useful when
31 * There are views imposed that dramatically alter the number of
32 * lines/words, or that hide parts of the document that really shouldn't
33 * be counted. The view on an RFC2822 email or the results of a notmuch
34 * search are good and current examples.
43 #define PANE_DATA_TYPE struct count_info
48 #include "core-pane.h"
50 static struct map *linecount_map;
51 DEF_LOOKUP_CMD(handle_count_lines, linecount_map);
53 static const int batch_marks = 10;
56 int lines, words, chars;
58 int *linep safe, *wordp safe, *charp safe;
63 struct pane *owner safe;
68 struct clcb *cl = container_of(ci->comm, struct clcb, c);
70 struct mark *m = ci->mark;
71 struct count_info *cli = cl->owner->data;
82 if (!cl->inword && (iswprint(ch) && !iswspace(ch))) {
85 } else if (cl->inword && !(iswprint(ch) && !iswspace(ch)))
89 (cl->lines >= 100 || cl->words >= 1000 || cl->chars >= 10000 ||
90 pane_too_long(cl->owner, 0)))
92 if (!ci->str || i >= ci->num2)
95 ch = get_utf8(&s, ci->str + ci->num2);
96 if (ch == WEOF || ch == WERR)
102 /* m isn't where we are, so we cannot update
103 * anything yet - need to return an get called again
107 attr_set_int(mark_attr(cl->start), "lines", cl->lines);
108 attr_set_int(mark_attr(cl->start), "words", cl->words);
109 attr_set_int(mark_attr(cl->start), "chars", cl->chars);
110 *cl->linep += cl->lines;
111 *cl->wordp += cl->words;
112 *cl->charp += cl->chars;
116 cl->start = vmark_new(ci->focus, cli->view_num, cl->owner);
118 mark_to_mark(cl->start, m);
119 if (cl->add_marks > 1 && pane_too_long(cl->owner, 0))
123 /* Added enough marks, abort */
128 static void do_count(struct pane *p safe, struct pane *owner safe,
129 struct mark *start safe, struct mark *end,
130 int *linep safe, int *wordp safe, int *charp safe, int add_marks)
132 /* if 'end' is NULL, go all the way to EOF */
141 cl.add_marks = add_marks;
151 if (call_comm("doc:content", p, &cl.c, 0, start, NULL, 0, end) <= 0 ||
152 (add_marks && cl.add_marks == 0))
155 if (cl.add_marks && cl.start && cl.start != start && cl.chars == 0) {
159 if (cl.add_marks && cl.start) {
160 attr_set_int(mark_attr(cl.start), "lines", cl.lines);
161 attr_set_int(mark_attr(cl.start), "words", cl.words);
162 attr_set_int(mark_attr(cl.start), "chars", cl.chars);
169 DEF_CMD(linecount_restart)
171 pane_call(ci->home, "CountLinesAsync", pane_focus(ci->focus), 1);
175 static int need_recalc(struct pane *p safe, struct mark *m)
181 if (!attr_find(*mark_attr(m), "lines"))
183 next = vmark_next(m);
184 if (next && attr_find_int(*mark_attr(m), "lines") < 20) {
185 /* This is tiny, recalc */
186 attr_del(mark_attr(m), "lines");
191 /* The background task needs to be stopped */
192 call_comm("event:free", p, &linecount_restart);
196 static void count_calculate(struct pane *p safe,
198 struct pane *owner safe, int type,
201 int lines, words, chars, l, w, c;
205 if (edlib_testing(p))
208 disable = pane_attr_get(p, "linecount-disable");
209 if (disable && strcmp(disable, "yes") == 0) {
211 attr_set_str(&end->attrs, "line", "??");
212 attr_set_str(&end->attrs, "word", "??");
213 attr_set_str(&end->attrs, "char", "??");
215 attr_set_str(&p->attrs, "lines", "-");
216 attr_set_str(&p->attrs, "words", "-");
217 attr_set_str(&p->attrs, "chars", "-");
221 if (!end && attr_find(p->attrs, "lines"))
225 if (end && !attr_find(p->attrs, "lines") && !sync)
226 /* We don't have totals, so do that first.
227 * When asked again, we will be able to find
232 pane_set_time(owner);
233 m = vmark_first(p, type, owner);
234 if (m == NULL || doc_prior(p, m) != WEOF) {
235 /* No mark at doc start, make some */
236 m = vmark_new(p, type, owner);
239 call("doc:set-ref", p, 1, m);
240 do_count(p, owner, m, vmark_next(m), &l, &w, &c, sync ? -1 : batch_marks);
242 call_comm("event:on-idle", owner, &linecount_restart);
247 if (need_recalc(owner, m)) {
248 /* need to update this one */
249 do_count(p, owner, m, vmark_next(m), &l, &w, &c, sync ? -1 : batch_marks);
251 call_comm("event:on-idle", owner, &linecount_restart);
255 /* Add totals from m to before end. Then count to 'end'.
257 lines = words = chars = 0;
258 while ((m2 = vmark_next(m)) != NULL &&
259 (!end || m2->seq < end->seq)) {
260 /* Need everything from m to m2 */
261 lines += attr_find_int(*mark_attr(m), "lines");
262 words += attr_find_int(*mark_attr(m), "words");
263 chars += attr_find_int(*mark_attr(m), "chars");
265 if (!need_recalc(owner, m))
267 do_count(p, owner, m, vmark_next(m), &l, &w, &c, sync ? -1 : batch_marks);
268 if (!sync || pane_too_long(owner, 0)) {
269 call_comm("event:on-idle", owner, &linecount_restart);
273 /* m is the last mark before end */
275 lines += attr_find_int(*mark_attr(m), "lines");
276 words += attr_find_int(*mark_attr(m), "words");
277 chars += attr_find_int(*mark_attr(m), "chars");
278 } else if (!mark_same(m, end)) {
279 do_count(p, owner, m, end, &l, &w, &c, 0);
286 struct attrset **attrs = &end->attrs;
287 attr_set_int(attrs, "line", lines + 1);
288 attr_set_int(attrs, "word", words + 1);
289 attr_set_int(attrs, "char", chars + 1);
291 attr_set_int(&p->attrs, "lines", lines);
292 attr_set_int(&p->attrs, "words", words);
293 attr_set_int(&p->attrs, "chars", chars);
294 if (!edlib_testing(p))
295 pane_notify("doc:status-changed", p);
299 DEF_CMD(linecount_close)
301 struct pane *d = ci->focus;
302 struct count_info *cli = ci->home->data;
305 call_comm("event:free", ci->home, &linecount_restart);
306 while ((m = vmark_first(d, cli->view_num, ci->home)) != NULL)
308 home_call(d, "doc:del-view", ci->home, cli->view_num);
309 pane_close(ci->home);
313 DEF_CMD(linecount_notify_replace)
315 struct pane *d = ci->focus;
316 struct count_info *cli = ci->home->data;
319 if (ci->mark && !ci->mark2)
320 /* I might not care about this one... */
323 attr_del(&d->attrs, "lines");
324 attr_del(&d->attrs, "words");
325 attr_del(&d->attrs, "chars");
328 m = vmark_at_or_before(d, ci->mark, cli->view_num, ci->home);
330 m = vmark_first(d, cli->view_num, ci->home);
334 attr_del(mark_attr(m), "lines");
335 attr_del(mark_attr(m), "words");
336 attr_del(mark_attr(m), "chars");
338 while ((m2 = vmark_next(m)) != NULL &&
339 (!ci->mark2 || mark_ordered_or_same(m2, ci->mark2)))
342 call_comm("event:free", ci->home, &linecount_restart);
346 DEF_CMD(linecount_notify_count)
348 struct pane *d = ci->focus;
349 struct count_info *cli = ci->home->data;
350 /* Option mark is "mark2" as "mark" gets the "point" so is never NULL */
351 /* num==1 means we don't want to wait for precision */
352 bool sync = ci->mark2 && ci->num != 1;
354 count_calculate(d, ci->mark2, ci->home, cli->view_num,
359 DEF_CMD(linecount_view_count)
361 struct pane *d = ci->focus;
362 struct count_info *cli = ci->home->data;
363 bool sync = strcmp(ci->key, "CountLines") == 0;
365 if (strcmp(ci->key, "CountLinesAsync") == 0)
368 if (ci->mark && ci->str && strcmp(ci->str, "goto:line") == 0 &&
369 ci->num != NO_NUMERIC) {
370 pane_call(ci->home, "doc:GotoLine", d, ci->num, ci->mark);
372 count_calculate(d, ci->mark, ci->home, cli->view_num,
377 DEF_CMD(linecount_notify_goto)
379 struct pane *d = ci->focus;
380 struct count_info *cli = ci->home->data;
388 /* Ensure counts are up-to-date */
389 count_calculate(d, NULL, ci->home, cli->view_num, True);
390 m = vmark_first(d, cli->view_num, ci->home);
394 while ((m2 = vmark_next(m)) != NULL &&
395 (l = attr_find_int(*mark_attr(m), "lines")) >= 0 &&
396 lineno + l < ci->num) {
400 mark_to_mark(ci->mark, m);
401 if (lineno == ci->num) {
402 /* might not be at start of line */
403 while ((ch = doc_prior(d, ci->mark)) != WEOF &&
405 doc_prev(d, ci->mark);
407 while (lineno < ci->num && (ch = doc_next(d, ci->mark)) != WEOF) {
416 int async = strcmp(ci->key, "CountLinesAsync") == 0;
418 /* FIXME optimise this away most of the time */
419 if (call("doc:notify:doc:CountLines", ci->focus, 1) == 0) {
420 /* No counter in place, add one */
421 struct count_info *cli;
424 p = pane_register(pane_root(ci->focus), 0,
425 &handle_count_lines.c);
429 cli->view_num = home_call(ci->focus, "doc:add-view", p) - 1;
430 home_call(ci->focus, "doc:request:doc:replaced", p);
431 home_call(ci->focus, "doc:request:doc:CountLines", p);
432 home_call(ci->focus, "doc:request:doc:GotoLine", p);
433 home_call(ci->focus, "doc:request:Notify:Close", p);
434 call("doc:notify:doc:CountLines", ci->focus, 1);
437 if (ci->str && strcmp(ci->str, "goto:line") == 0 &&
438 ci->num != NO_NUMERIC) {
439 call("doc:notify:doc:GotoLine", ci->focus, ci->num,
442 call("doc:notify:doc:CountLines", ci->focus, async, NULL, NULL,
446 call("doc:notify:doc:CountLines", ci->focus, async, NULL, NULL,
451 DEF_CMD(linecount_attach)
453 struct count_info *cli;
456 p = pane_register(ci->focus, 0, &handle_count_lines.c);
460 cli->view_num = home_call(p, "doc:add-view", p) - 1;
461 call("doc:request:doc:replaced", p);
462 call("doc:request:Notify:Close", p);
463 call_comm("event:on-idle", p, &linecount_restart, 1);
465 comm_call(ci->comm2, "cb", p);
469 DEF_CMD(linecount_clone)
473 p = comm_call_ret(pane, &linecount_attach, "attach", ci->focus);
474 pane_clone_children(ci->home, p);
478 void edlib_init(struct pane *ed safe)
480 call_comm("global-set-command", ed, &count_lines, 0, NULL, "CountLines");
481 call_comm("global-set-command", ed, &count_lines, 0, NULL, "CountLinesAsync");
482 call_comm("global-set-command", ed, &linecount_attach, 0, NULL,
483 "attach-line-count");
488 linecount_map = key_alloc();
489 key_add(linecount_map, "Notify:Close", &linecount_close);
490 key_add(linecount_map, "doc:replaced", &linecount_notify_replace);
491 key_add(linecount_map, "doc:CountLines", &linecount_notify_count);
492 key_add(linecount_map, "doc:GotoLine", &linecount_notify_goto);
494 /* For view-attached version */
495 key_add(linecount_map, "CountLines", &linecount_view_count);
496 key_add(linecount_map, "CountLinesAsync", &linecount_view_count);
497 //key_add(linecount_map, "view:changed", &linecount_notify_replace);
498 key_add(linecount_map, "Clone", &linecount_clone);