]> git.neil.brown.name Git - edlib.git/blob - lib-linefilter.c
Filename completion: don't include trailing slash unless only match.
[edlib.git] / lib-linefilter.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-filter: hide (un)selected lines from display
6  *
7  * This can be layered over render-format or similar and will restrict
8  * which lines are shown, based on some attribute visible at the start
9  * of the line, or the content of the line.  How this content is assessed
10  * can be set by a call to Filter:set, or by setting various attributes.
11  * - filter:match - a string that must appear in the content
12  * - filter:attr  - the text attribute which contains the content
13  * - filter:at_start - whether match must be at start of content
14  * - filter:ignore_case - whether to ignore case when comparing match
15  *                        against content.
16  *
17  * This module doesn't hold any marks on any document.  The marks
18  * held by the renderer should be sufficient.
19  */
20
21 #define _GNU_SOURCE for strcasestr
22 #include <stdlib.h>
23 #include <string.h>
24 #include <ctype.h>
25 #define PANE_DATA_TYPE struct filter_data
26 #include "core.h"
27 #include "misc.h"
28
29 struct filter_data {
30         char *match;
31         int match_len;
32         char *attr;
33         bool at_start;
34         bool ignore_case;
35         bool explicit_set;
36         bool implicit_set;
37 };
38 #include "core-pane.h"
39
40 struct rlcb {
41         struct command c;
42         struct filter_data *fd;
43         int keep, cmp;
44         const char *str;
45         int cursor_offset;
46 };
47
48 static void strip_attrs(char *c safe)
49 {
50         char *n = c;
51         if (*c == ack) {
52                 for (; *c; c++) {
53                         if (*c == ack || *c == etx)
54                                 continue;
55                         if (*c != soh) {
56                                 *n++ = *c;
57                                 continue;
58                         }
59                         while (*c != stx)
60                                 c++;
61                 }
62         } else {
63                 for (; *c ; c++) {
64                         if (*c == '<' && c[1] == '<') {
65                                 *n++ = *c++;
66                                 continue;
67                         }
68                         if (*c != '<') {
69                                 *n++ = *c;
70                                 continue;
71                         }
72                         while (*c != '>')
73                                 c++;
74                 }
75         }
76         *n = 0;
77 }
78
79 DEF_CB(rlcb)
80 {
81         struct rlcb *cb = container_of(ci->comm, struct rlcb, c);
82         char *c = ci->str ? strdup(ci->str) : NULL;
83         if (c && strcmp(ci->key, "attr") != 0)
84                 /* This is a rendered line so we need to strip out attrs. */
85                 strip_attrs(c);
86         if (cb->fd == NULL)
87                 cb->cmp = 0; /* Don't compare, just save */
88         else if (c == NULL)
89                 cb->cmp = -1;
90         else if (cb->fd->match == NULL)
91                 cb->cmp = 0;
92         else if (cb->fd->at_start && cb->fd->ignore_case)
93                 cb->cmp = strncasecmp(c, cb->fd->match, cb->fd->match_len);
94         else if (cb->fd->at_start)
95                 cb->cmp = strncmp(c, cb->fd->match, cb->fd->match_len);
96         else if (cb->fd->ignore_case)
97                 cb->cmp = strcasestr(c, cb->fd->match) ? 0 : 1;
98         else
99                 cb->cmp = strstr(c, cb->fd->match) ? 0 : 1;
100
101         if (cb->cmp == 0 && cb->keep && !cb->str && c && ci->str) {
102                 if (cb->keep > 1)
103                         /* Want the original with markup */
104                         strcpy(c, ci->str);
105                 cb->str = c;
106                 cb->cursor_offset = ci->num;
107         } else
108                 free(c);
109         return 1;
110 }
111
112 static bool check_settings(struct pane *focus safe,
113                            struct filter_data *fd safe)
114 {
115         char *s;
116         bool changed = False;
117         bool v;
118
119         if (fd->explicit_set || fd->implicit_set)
120                 return False;
121
122         s = pane_attr_get(focus, "filter:match");
123         if (s && (!fd->match || strcmp(s, fd->match) != 0)) {
124                 free(fd->match);
125                 fd->match = strdup(s);
126                 fd->match_len = strlen(s);
127                 changed = True;
128         }
129         s = pane_attr_get(focus, "filter:attr");
130         if ((!!s != !!fd->attr) ||
131             (s && fd->attr && strcmp(s, fd->attr) != 0)) {
132                 free(fd->attr);
133                 fd->attr = s ? strdup(s) : NULL;
134                 changed = True;
135         }
136         s = pane_attr_get(focus, "filter:at_start");
137         v = s && *s && strchr("Yy1Tt", *s) != NULL;
138         if (v != fd->at_start) {
139                 changed = True;
140                 fd->at_start = v;
141         }
142
143         s = pane_attr_get(focus, "filter:ignore_case");
144         v = s && *s && strchr("Yy1Tt", *s) != NULL;
145         if (v != fd->ignore_case) {
146                 changed = True;
147                 fd->ignore_case = v;
148         }
149         fd->implicit_set = True;
150
151         return changed;
152 }
153
154 DEF_CMD(render_filter_line)
155 {
156         /* Skip any line that doesn't match, and
157          * return a highlighted version of the first one
158          * that does.
159          * Then skip over any other non-matches.
160          */
161         struct filter_data *fd = ci->home->data;
162         struct rlcb cb;
163         int ret;
164         struct mark *m;
165         struct mark *m2;
166
167         if (!ci->mark)
168                 return Enoarg;
169
170         check_settings(ci->focus, fd);
171
172         m = mark_dup(ci->mark);
173         cb.c = rlcb;
174         cb.fd = fd;
175         cb.str = NULL;
176         cb.keep = 0;
177         cb.cmp = 0;
178
179         do {
180                 mark_to_mark(ci->mark, m);
181                 cb.cmp = 0;
182                 if (fd->attr) {
183                         comm_call(&cb.c, "attr", ci->focus,
184                                   NO_NUMERIC, NULL,
185                                   pane_mark_attr(ci->focus, m, fd->attr));
186                         home_call(ci->home->parent, ci->key, ci->focus,
187                                   NO_NUMERIC, m);
188                 } else
189                         home_call_comm(ci->home->parent, ci->key, ci->focus,
190                                        &cb.c, NO_NUMERIC, m);
191         } while (cb.cmp && !mark_same(ci->mark, m));
192
193         mark_free(m);
194         cb.keep = 2;
195         cb.str = NULL;
196         cb.fd = NULL;
197         m2 = ci->mark2;
198         if (home_call_comm(ci->home->parent, ci->key, ci->focus, &cb.c,
199                            ci->num, ci->mark, NULL, 0, m2) < 0)
200                 return Efail;
201
202         ret = comm_call(ci->comm2, "callback:render", ci->focus,
203                         cb.cursor_offset, NULL, cb.str);
204         free((void*)cb.str);
205         if (m2)
206                 /* Was rendering to find a cursor, don't need to skip */
207                 return ret;
208         /* Need to continue over other non-matching lines */
209         m = mark_dup(ci->mark);
210         cb.keep = 0;
211         cb.str = NULL;
212         cb.fd = fd;
213         do {
214                 /* have a non-match, so move the mark over it. */
215                 mark_to_mark(ci->mark, m);
216                 cb.cmp = 0;
217                 if (fd->attr) {
218                         comm_call(&cb.c, "attr", ci->focus,
219                                   NO_NUMERIC, NULL,
220                                   pane_mark_attr(ci->focus, m, fd->attr));
221                         home_call(ci->home->parent, ci->key, ci->focus,
222                                   NO_NUMERIC, m);
223                 } else
224                         home_call_comm(ci->home->parent, ci->key, ci->focus,
225                                        &cb.c, NO_NUMERIC, m);
226         } while (cb.cmp && !mark_same(ci->mark, m));
227
228         mark_free(m);
229         return ret ?: 1;
230 }
231
232 static int do_filter_line_prev(struct filter_data *fd safe,
233                                struct mark *m safe,
234                                struct pane *home safe,
235                                struct pane *focus safe, int n,
236                                const char **savestr)
237 {
238         /* Move to start of this or previous real line and
239          * check if it passes the filter.
240          * If we get an error, return that. else 0 if it doesn't
241          * pass, else 1.
242          */
243         struct rlcb cb;
244         int ret;
245
246         if (savestr)
247                 *savestr = NULL;
248
249         cb.c = rlcb;
250         cb.str = NULL;
251         cb.fd = fd;
252         cb.cmp = 0;
253
254         ret = home_call(home, "doc:render-line-prev", focus, n, m);
255         if (ret < 0)
256                 /* Probably hit start-of-file */
257                 return ret;
258         if (doc_following(home, m) == WEOF)
259                 /* End of file, no match possible */
260                 return 0;
261
262         /* we must be looking at a possible option for the previous line */
263         cb.keep = !!savestr;
264         cb.str = NULL;
265         if (fd->attr) {
266                 comm_call(&cb.c, "attr", focus,
267                           NO_NUMERIC, NULL,
268                           pane_mark_attr(focus, m, fd->attr));
269         } else {
270                 struct mark *m2 = mark_dup(m);
271
272                 ret = home_call_comm(home, "doc:render-line", focus, &cb.c,
273                                      NO_NUMERIC, m2);
274                 mark_free(m2);
275                 if (ret <= 0)
276                         return Efail;
277         }
278         if (savestr)
279                 *savestr = cb.str;
280
281         return cb.cmp == 0;
282 }
283
284 DEF_CMD(render_filter_prev)
285 {
286         struct filter_data *fd = ci->home->data;
287         struct mark *m = ci->mark;
288         int ret;
289
290         if (!m)
291                 return Enoarg;
292
293         check_settings(ci->focus, fd);
294
295         if (!fd->match)
296                 return Efallthrough;
297
298         /* First, make sure we are at a start-of-line */
299         ret = do_filter_line_prev(fd, m, ci->home->parent, ci->focus, 0, NULL);
300         if (ret < 0)
301                 return ret;
302         while (ret == 0) {
303                 /* That wasn't a matching line, try again */
304                 ret = do_filter_line_prev(fd, m, ci->home->parent, ci->focus,
305                                           1, NULL);
306         }
307         if (!ci->num)
308                 /* Only wanted start of line - found */
309                 return 1;
310         ret = 0;
311         while (ret == 0) {
312                 ret = do_filter_line_prev(fd, m, ci->home->parent, ci->focus,
313                                           1, NULL);
314                 if (ret < 0)
315                         /* Error */
316                         return ret;
317         }
318         return ret;
319 }
320
321 DEF_CMD(filter_changed)
322 {
323         /* Update match info from arg (Filter:set) or pane attrs (unless
324          * Filter:set has been used), then walk over a range of marks
325          * calling Notify:clip as needed, and for Filter:set, call comm2
326          * with the matching string.
327          *
328          * If no marks are given, we walk entire doc.
329          * otherwise find first visible line after all marks, and last before,
330          * and walk that range
331          */
332         struct filter_data *fd = ci->home->data;
333         struct mark *start, *end, *m;
334         struct command *comm = NULL;
335         bool found_one = False;
336
337         if (strcmp(ci->key, "Filter:set") == 0) {
338                 if (!ci->str)
339                         return Enoarg;
340                 call("view:changed", pane_focus(ci->home));
341                 comm = ci->comm2;
342                 fd->explicit_set = True;
343                 free(fd->match);
344                 free(fd->attr);
345                 fd->match = strdup(ci->str);
346                 fd->match_len = strlen(fd->match);
347                 fd->attr = ci->str2 ? strdup(ci->str2) : NULL;
348                 fd->at_start = !!(ci->num & 1);
349                 fd->ignore_case = !!(ci->num & 2);
350         }
351         if (!fd->explicit_set) {
352                 fd->implicit_set = False;
353                 if (check_settings(ci->focus, fd))
354                         /* Something changed */
355                         call("view:changed", pane_focus(ci->home));
356         }
357         if (!fd->match)
358                 return 1;
359
360         start = mark_new(ci->focus);
361         if (!start)
362                 return Efail;
363         if (ci->mark && (!ci->mark2 || ci->mark2->seq > ci->mark->seq))
364                 /* mark is first */
365                 mark_to_mark(start, ci->mark);
366         else if (ci->mark2)
367                 /* mark2 is first */
368                 mark_to_mark(start, ci->mark2);
369         else if (strcmp(ci->key, "Filter:set") == 0)
370                 call("doc:file", ci->focus, -1, start);
371         else {
372                 struct mark *m2;
373                 m = start;
374                 while ((m2 = mark_prev(m)) != NULL)
375                         m = m2;
376                 mark_to_mark(start, m);
377         }
378
379         end = mark_new(ci->focus);
380         if (!end) {
381                 mark_free(start);
382                 return Efail;
383         }
384         if (ci->mark && (!ci->mark2 || ci->mark2->seq < ci->mark->seq))
385                 /* mark is last */
386                 mark_to_mark(end, ci->mark);
387         else if (ci->mark2)
388                 /* mark2 is last */
389                 mark_to_mark(end, ci->mark2);
390         else if (strcmp(ci->key, "Filter:set") == 0)
391                 call("doc:file", ci->focus, 1, end);
392         else {
393                 struct mark *m2;
394                 m = end;
395                 while ((m2 = mark_next(m)) != NULL)
396                         m = m2;
397                 mark_to_mark(end, m);
398         }
399
400         if (call("doc:render-line", ci->focus, -1, end) > 0)
401                 found_one = True;
402
403         m = mark_dup(end);
404         while (m->seq > start->seq || !found_one) {
405                 int ret;
406                 const char *str = NULL;
407                 struct mark *m2 = mark_dup(m);
408
409                 ret = do_filter_line_prev(fd, m, ci->home->parent, ci->focus, 1,
410                                           comm ? &str : NULL);
411                 if (ret > 0) {
412                         /* m is a good line, m2 is like end */
413                         found_one = True;
414                         if (!mark_same(m2, end))
415                                 call("Notify:clip", ci->focus, 0, m2, NULL,
416                                      0, end);
417                         mark_to_mark(end, m);
418                         if (comm && str)
419                                 comm_call(comm, "", ci->focus, 0, m, str);
420                 }
421                 free((void*)str);
422                 mark_free(m2);
423                 if (ret < 0)
424                         break;
425         }
426         /* No matching lines found between m and end, so clip them */
427         if (!mark_same(m, end))
428                 call("Notify:clip", ci->focus, 0, m, NULL, 0, end);
429         mark_free(m);
430         mark_free(start);
431         mark_free(end);
432         if (!found_one)
433                 /* filtered document is now empty - maybe someone cares */
434                 home_call(ci->focus, "Notify:filter:empty", ci->home);
435         return 1;
436 }
437
438 DEF_CMD(eol_cb)
439 {
440         /* don't save anything */
441         return 1;
442 }
443
444 DEF_CMD(filter_eol)
445 {
446         struct filter_data *fd = ci->home->data;
447         int rpt = RPT_NUM(ci);
448         bool one_more = ci->num2 > 0;
449         int line;
450
451         check_settings(ci->focus, fd);
452
453         if (!ci->mark)
454                 return Enoarg;
455         if (rpt < 0)
456                 line = rpt + 1 - one_more;
457         else
458                 line = rpt - 1 + one_more;
459         /* 'line' is which line to go to relative to here */
460         if (line == 0) {
461                 if (rpt < 0)
462                         /* Start of line */
463                         call("doc:EOL", ci->home->parent, -1, ci->mark);
464                 else
465                         /* End of line */
466                         call("doc:EOL", ci->home->parent, 1, ci->mark);
467                 return 1;
468         }
469         /* Must be at start of line for filtering to work */
470         call("doc:EOL", ci->home->parent, -1, ci->mark);
471         while (line < 0) {
472                 int ret;
473                 ret = do_filter_line_prev(fd, ci->mark,
474                                           ci->home->parent, ci->focus, 1, NULL);
475                 if (ret < 0)
476                         line = 0;
477                 if (ret > 0)
478                         line += 1;
479         }
480         while (line > 0) {
481                 struct call_return cr;
482                 cr.c = eol_cb;
483                 if (home_call(ci->home, "doc:render-line",
484                               ci->focus, -1, ci->mark, NULL,
485                               0, NULL, NULL, 0,0, &cr.c) <= 0)
486                         line = 1;
487                 line -= 1;
488         }
489         if ((rpt < 0 && !one_more) || (rpt > 0 && one_more))
490                 /* Target was start of a line, so we are there */
491                 return 1;
492         call("doc:EOL", ci->home->parent, 1, ci->mark);
493         return 1;
494 }
495
496 DEF_CMD(filter_damaged)
497 {
498         // We need this for doc:replaced so that when the
499         // view becomes empty we can Notify:filter:empty.
500         // We used to do it for view:changed as well, but
501         // causes lots of updates for large directories
502         // that are slow, and don't seem to change anything.
503         pane_damaged(ci->home, DAMAGED_VIEW);
504         return Efallthrough;
505 }
506
507 DEF_CMD(filter_attach);
508 DEF_CMD(filter_clone)
509 {
510         struct pane *parent = ci->focus;
511
512         filter_attach.func(ci);
513         pane_clone_children(ci->home, parent->focus);
514         return 1;
515 }
516
517 static struct map *filter_map;
518 DEF_LOOKUP_CMD(filter_handle, filter_map);
519
520 static void filter_register_map(void)
521 {
522         if (filter_map)
523                 return;
524
525         filter_map = key_alloc();
526         key_add(filter_map, "doc:render-line", &render_filter_line);
527         key_add(filter_map, "doc:render-line-prev", &render_filter_prev);
528         key_add(filter_map, "Clone", &filter_clone);
529         key_add(filter_map, "doc:EOL", &filter_eol);
530         key_add(filter_map, "Filter:set", &filter_changed);
531         // handling view:changed seems to cause unnecesary
532         // work.
533         // key_add(filter_map, "view:changed", &filter_damaged);
534         key_add(filter_map, "doc:replaced", &filter_damaged);
535         key_add(filter_map, "Refresh:view", &filter_changed);
536 }
537
538 REDEF_CMD(filter_attach)
539 {
540         struct pane *filter;
541
542         filter_register_map();
543
544         filter = pane_register(ci->focus, 0, &filter_handle.c);
545         if (!filter)
546                 return Efail;
547         pane_damaged(filter, DAMAGED_VIEW);
548         call("doc:request:doc:replaced", filter);
549
550         return comm_call(ci->comm2, "", filter);
551 }
552
553 void edlib_init(struct pane *ed safe)
554 {
555         call_comm("global-set-command", ed, &filter_attach,
556                   0, NULL, "attach-linefilter");
557 }