]> git.neil.brown.name Git - edlib.git/blob - lib-linecount.c
Draw:image: support scale and crop.
[edlib.git] / lib-linecount.c
1 /*
2  * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * line/word/char count.
6  *
7  * This module can be attached to a Document to count lines/words/chars.
8  *
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
13  * mark are cleared.
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.
18  *
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.
22  *
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.
28  *
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.
35  */
36
37 #include <unistd.h>
38 #include <stdlib.h>
39 #include <wchar.h>
40 #include <wctype.h>
41 #include <string.h>
42
43 #define PANE_DATA_TYPE struct count_info
44 #include "core.h"
45 struct count_info {
46         int view_num;
47 };
48 #include "core-pane.h"
49
50 static struct map *linecount_map;
51 DEF_LOOKUP_CMD(handle_count_lines, linecount_map);
52
53 static const int batch_marks = 10;
54
55 struct clcb {
56         int lines, words, chars;
57         int inword;
58         int *linep safe, *wordp safe, *charp safe;
59         int add_marks;
60         struct mark *start;
61         struct mark *end;
62         struct command c;
63         struct pane *owner safe;
64 };
65
66 DEF_CB(clcb)
67 {
68         struct clcb *cl = container_of(ci->comm, struct clcb, c);
69         wint_t ch = ci->num;
70         struct mark *m = ci->mark;
71         struct count_info *cli = cl->owner->data;
72         const char *s;
73         int i = 0;
74
75         if (!m)
76                 return Enoarg;
77
78         while (1) {
79                 cl->chars += 1;
80                 if (is_eol(ch))
81                         cl->lines += 1;
82                 if (!cl->inword && (iswprint(ch) && !iswspace(ch))) {
83                         cl->inword = 1;
84                         cl->words += 1;
85                 } else if (cl->inword && !(iswprint(ch) && !iswspace(ch)))
86                         cl->inword = 0;
87                 if (cl->add_marks &&
88                     cl->start &&
89                     (cl->lines >= 100 || cl->words >= 1000 || cl->chars >= 10000 ||
90                      pane_too_long(cl->owner, 0)))
91                         break;
92                 if (!ci->str || i >= ci->num2)
93                         return i+1;
94                 s = ci->str + i;
95                 ch = get_utf8(&s, ci->str + ci->num2);
96                 if (ch == WEOF || ch == WERR)
97                         return i+1;
98                 i = s - ci->str;
99         }
100
101         if (i > 0) {
102                 /* m isn't where we are, so we cannot update
103                  * anything yet - need to return an get called again
104                  */
105                 return i+1;
106         }
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;
113         cl->lines = 0;
114         cl->words = 0;
115         cl->chars = 0;
116         cl->start = vmark_new(ci->focus, cli->view_num, cl->owner);
117         if (cl->start)
118                 mark_to_mark(cl->start, m);
119         if (cl->add_marks > 1 && pane_too_long(cl->owner, 0))
120                 cl->add_marks = 1;
121         cl->add_marks -= 1;
122         if (!cl->add_marks)
123                 /* Added enough marks, abort */
124                 return Efalse;
125         return 1;
126 }
127
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)
131 {
132         /* if 'end' is NULL, go all the way to EOF */
133         struct clcb cl;
134
135         cl.lines = 0;
136         cl.words = 0;
137         cl.chars = 0;
138         cl.inword = 0;
139         cl.end = end;
140         cl.start = start;
141         cl.add_marks = add_marks;
142         cl.c = clcb;
143         cl.linep = linep;
144         cl.wordp = wordp;
145         cl.charp = charp;
146         cl.owner = owner;
147
148         *linep = 0;
149         *wordp = 0;
150         *charp = 0;
151         if (call_comm("doc:content", p, &cl.c, 0, start, NULL, 0, end) <= 0 ||
152             (add_marks && cl.add_marks == 0))
153                 return;
154
155         if (cl.add_marks && cl.start && cl.start != start && cl.chars == 0) {
156                 mark_free(cl.start);
157                 cl.start = NULL;
158         }
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);
163         }
164         *linep += cl.lines;
165         *wordp += cl.words;
166         *charp += cl.chars;
167 }
168
169 DEF_CMD(linecount_restart)
170 {
171         pane_call(ci->home, "CountLinesAsync", pane_focus(ci->focus), 1);
172         return Efalse;
173 }
174
175 static int need_recalc(struct pane *p safe, struct mark *m)
176 {
177         struct mark *next;
178         int ret = 0;
179         if (!m)
180                 return 1;
181         if (!attr_find(*mark_attr(m), "lines"))
182                 ret = 1;
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");
187                 mark_free(next);
188                 ret = 1;
189         }
190         if (ret)
191                 /* The background task needs to be stopped */
192                 call_comm("event:free", p, &linecount_restart);
193         return ret;
194 }
195
196 static void count_calculate(struct pane *p safe,
197                             struct mark *end,
198                             struct pane *owner safe, int type,
199                             bool sync)
200 {
201         int lines, words, chars, l, w, c;
202         struct mark *m, *m2;
203         char *disable;
204
205         if (edlib_testing(p))
206                 sync = True;
207
208         disable = pane_attr_get(p, "linecount-disable");
209         if (disable && strcmp(disable, "yes") == 0) {
210                 if (end) {
211                         attr_set_str(&end->attrs, "line", "??");
212                         attr_set_str(&end->attrs, "word", "??");
213                         attr_set_str(&end->attrs, "char", "??");
214                 }
215                 attr_set_str(&p->attrs, "lines", "-");
216                 attr_set_str(&p->attrs, "words", "-");
217                 attr_set_str(&p->attrs, "chars", "-");
218                 return;
219         }
220
221         if (!end && attr_find(p->attrs, "lines"))
222                 /* nothing to do */
223                 return;
224
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
228                  * the mark quickly.
229                  */
230                 end = NULL;
231
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);
237                 if (!m)
238                         return;
239                 call("doc:set-ref", p, 1, m);
240                 do_count(p, owner, m, vmark_next(m), &l, &w, &c, sync ? -1 : batch_marks);
241                 if (!sync) {
242                         call_comm("event:on-idle", owner, &linecount_restart);
243                         return;
244                 }
245         }
246
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);
250                 if (!sync) {
251                         call_comm("event:on-idle", owner, &linecount_restart);
252                         return;
253                 }
254         }
255         /* Add totals from m to before end. Then count to 'end'.
256          */
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");
264                 m = m2;
265                 if (!need_recalc(owner, m))
266                         continue;
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);
270                         return;
271                 }
272         }
273         /* m is the last mark before end */
274         if (!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);
280                 lines += l;
281                 words += w;
282                 chars += c;
283         }
284
285         if (end) {
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);
290         } else {
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);
296         }
297 }
298
299 DEF_CMD(linecount_close)
300 {
301         struct pane *d = ci->focus;
302         struct count_info *cli = ci->home->data;
303         struct mark *m;
304
305         call_comm("event:free", ci->home, &linecount_restart);
306         while ((m = vmark_first(d, cli->view_num, ci->home)) != NULL)
307                 mark_free(m);
308         home_call(d, "doc:del-view", ci->home, cli->view_num);
309         pane_close(ci->home);
310         return 1;
311 }
312
313 DEF_CMD(linecount_notify_replace)
314 {
315         struct pane *d = ci->focus;
316         struct count_info *cli = ci->home->data;
317         struct mark *m, *m2;
318
319         if (ci->mark && !ci->mark2)
320                 /* I might not care about this one... */
321                 return Efallthrough;
322
323         attr_del(&d->attrs, "lines");
324         attr_del(&d->attrs, "words");
325         attr_del(&d->attrs, "chars");
326
327         if (ci->mark)
328                 m = vmark_at_or_before(d, ci->mark, cli->view_num, ci->home);
329         else
330                 m = vmark_first(d, cli->view_num, ci->home);
331         if (!m)
332                 return Efallthrough;
333
334         attr_del(mark_attr(m), "lines");
335         attr_del(mark_attr(m), "words");
336         attr_del(mark_attr(m), "chars");
337
338         while ((m2 = vmark_next(m)) != NULL &&
339                (!ci->mark2 || mark_ordered_or_same(m2, ci->mark2)))
340                 mark_free(m2);
341
342         call_comm("event:free", ci->home, &linecount_restart);
343         return Efallthrough;
344 }
345
346 DEF_CMD(linecount_notify_count)
347 {
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;
353
354         count_calculate(d, ci->mark2, ci->home, cli->view_num,
355                         sync);
356         return 1;
357 }
358
359 DEF_CMD(linecount_view_count)
360 {
361         struct pane *d = ci->focus;
362         struct count_info *cli = ci->home->data;
363         bool sync = strcmp(ci->key, "CountLines") == 0;
364
365         if (strcmp(ci->key, "CountLinesAsync") == 0)
366                 sync = False;
367
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);
371         }
372         count_calculate(d, ci->mark, ci->home, cli->view_num,
373                         sync);
374         return 1;
375 }
376
377 DEF_CMD(linecount_notify_goto)
378 {
379         struct pane *d = ci->focus;
380         struct count_info *cli = ci->home->data;
381         int lineno, l;
382         struct mark *m, *m2;
383         wint_t ch;
384
385         if (!ci->mark)
386                 return 1;
387
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);
391         if (!m)
392                 return 1;
393         lineno = 1;
394         while ((m2 = vmark_next(m)) != NULL &&
395                (l = attr_find_int(*mark_attr(m), "lines")) >= 0 &&
396                lineno + l < ci->num) {
397                 m = m2;
398                 lineno += l;
399         }
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 &&
404                        !is_eol(ch))
405                         doc_prev(d, ci->mark);
406         }
407         while (lineno < ci->num && (ch = doc_next(d, ci->mark)) != WEOF) {
408                 if (is_eol(ch))
409                         lineno += 1;
410         }
411         return 1;
412 }
413
414 DEF_CMD(count_lines)
415 {
416         int async = strcmp(ci->key, "CountLinesAsync") == 0;
417
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;
422                 struct pane *p;
423
424                 p = pane_register(pane_root(ci->focus), 0,
425                                   &handle_count_lines.c);
426                 if (!p)
427                         return Efail;
428                 cli = p->data;
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);
435         }
436         if (ci->mark) {
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,
440                              ci->mark);
441                 }
442                 call("doc:notify:doc:CountLines", ci->focus, async, NULL, NULL,
443                      0, ci->mark);
444         }
445         if (ci->mark2)
446                 call("doc:notify:doc:CountLines", ci->focus, async, NULL, NULL,
447                      0, ci->mark2);
448         return 1;
449 }
450
451 DEF_CMD(linecount_attach)
452 {
453         struct count_info *cli;
454         struct pane *p;
455
456         p = pane_register(ci->focus, 0, &handle_count_lines.c);
457         if (!p)
458                 return Efail;
459         cli = p->data;
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);
464
465         comm_call(ci->comm2, "cb", p);
466         return 1;
467 }
468
469 DEF_CMD(linecount_clone)
470 {
471         struct pane *p;
472
473         p = comm_call_ret(pane, &linecount_attach, "attach", ci->focus);
474         pane_clone_children(ci->home, p);
475         return 1;
476 }
477
478 void edlib_init(struct pane *ed safe)
479 {
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");
484
485         if (linecount_map)
486                 return;
487
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);
493
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);
499 }