2 * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3 * May be distributed under terms of GPLv2 - see file:COPYING
5 * line-filter: hide (un)selected lines from display
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
17 * This module doesn't hold any marks on any document. The marks
18 * held by the renderer should be sufficient.
21 #define _GNU_SOURCE for strcasestr
25 #define PANE_DATA_TYPE struct filter_data
38 #include "core-pane.h"
42 struct filter_data *fd;
48 static void strip_attrs(char *c safe)
53 if (*c == ack || *c == etx)
64 if (*c == '<' && c[1] == '<') {
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. */
87 cb->cmp = 0; /* Don't compare, just save */
90 else if (cb->fd->match == NULL)
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;
99 cb->cmp = strstr(c, cb->fd->match) ? 0 : 1;
101 if (cb->cmp == 0 && cb->keep && !cb->str && c && ci->str) {
103 /* Want the original with markup */
106 cb->cursor_offset = ci->num;
112 static bool check_settings(struct pane *focus safe,
113 struct filter_data *fd safe)
116 bool changed = False;
119 if (fd->explicit_set || fd->implicit_set)
122 s = pane_attr_get(focus, "filter:match");
123 if (s && (!fd->match || strcmp(s, fd->match) != 0)) {
125 fd->match = strdup(s);
126 fd->match_len = strlen(s);
129 s = pane_attr_get(focus, "filter:attr");
130 if ((!!s != !!fd->attr) ||
131 (s && fd->attr && strcmp(s, fd->attr) != 0)) {
133 fd->attr = s ? strdup(s) : NULL;
136 s = pane_attr_get(focus, "filter:at_start");
137 v = s && *s && strchr("Yy1Tt", *s) != NULL;
138 if (v != fd->at_start) {
143 s = pane_attr_get(focus, "filter:ignore_case");
144 v = s && *s && strchr("Yy1Tt", *s) != NULL;
145 if (v != fd->ignore_case) {
149 fd->implicit_set = True;
154 DEF_CMD(render_filter_line)
156 /* Skip any line that doesn't match, and
157 * return a highlighted version of the first one
159 * Then skip over any other non-matches.
161 struct filter_data *fd = ci->home->data;
170 check_settings(ci->focus, fd);
172 m = mark_dup(ci->mark);
180 mark_to_mark(ci->mark, m);
183 comm_call(&cb.c, "attr", ci->focus,
185 pane_mark_attr(ci->focus, m, fd->attr));
186 home_call(ci->home->parent, ci->key, ci->focus,
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));
198 if (home_call_comm(ci->home->parent, ci->key, ci->focus, &cb.c,
199 ci->num, ci->mark, NULL, 0, m2) < 0)
202 ret = comm_call(ci->comm2, "callback:render", ci->focus,
203 cb.cursor_offset, NULL, cb.str);
206 /* Was rendering to find a cursor, don't need to skip */
208 /* Need to continue over other non-matching lines */
209 m = mark_dup(ci->mark);
214 /* have a non-match, so move the mark over it. */
215 mark_to_mark(ci->mark, m);
218 comm_call(&cb.c, "attr", ci->focus,
220 pane_mark_attr(ci->focus, m, fd->attr));
221 home_call(ci->home->parent, ci->key, ci->focus,
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));
232 static int do_filter_line_prev(struct filter_data *fd safe,
234 struct pane *home safe,
235 struct pane *focus safe, int n,
236 const char **savestr)
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
254 ret = home_call(home, "doc:render-line-prev", focus, n, m);
256 /* Probably hit start-of-file */
258 if (doc_following(home, m) == WEOF)
259 /* End of file, no match possible */
262 /* we must be looking at a possible option for the previous line */
266 comm_call(&cb.c, "attr", focus,
268 pane_mark_attr(focus, m, fd->attr));
270 struct mark *m2 = mark_dup(m);
272 ret = home_call_comm(home, "doc:render-line", focus, &cb.c,
284 DEF_CMD(render_filter_prev)
286 struct filter_data *fd = ci->home->data;
287 struct mark *m = ci->mark;
293 check_settings(ci->focus, fd);
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);
303 /* That wasn't a matching line, try again */
304 ret = do_filter_line_prev(fd, m, ci->home->parent, ci->focus,
308 /* Only wanted start of line - found */
312 ret = do_filter_line_prev(fd, m, ci->home->parent, ci->focus,
321 DEF_CMD(filter_changed)
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.
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
332 struct filter_data *fd = ci->home->data;
333 struct mark *start, *end, *m;
334 struct command *comm = NULL;
335 bool found_one = False;
337 if (strcmp(ci->key, "Filter:set") == 0) {
340 call("view:changed", pane_focus(ci->home));
342 fd->explicit_set = True;
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);
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));
360 start = mark_new(ci->focus);
363 if (ci->mark && (!ci->mark2 || ci->mark2->seq > ci->mark->seq))
365 mark_to_mark(start, ci->mark);
368 mark_to_mark(start, ci->mark2);
369 else if (strcmp(ci->key, "Filter:set") == 0)
370 call("doc:file", ci->focus, -1, start);
374 while ((m2 = mark_prev(m)) != NULL)
376 mark_to_mark(start, m);
379 end = mark_new(ci->focus);
384 if (ci->mark && (!ci->mark2 || ci->mark2->seq < ci->mark->seq))
386 mark_to_mark(end, ci->mark);
389 mark_to_mark(end, ci->mark2);
390 else if (strcmp(ci->key, "Filter:set") == 0)
391 call("doc:file", ci->focus, 1, end);
395 while ((m2 = mark_next(m)) != NULL)
397 mark_to_mark(end, m);
400 if (call("doc:render-line", ci->focus, -1, end) > 0)
404 while (m->seq > start->seq || !found_one) {
406 const char *str = NULL;
407 struct mark *m2 = mark_dup(m);
409 ret = do_filter_line_prev(fd, m, ci->home->parent, ci->focus, 1,
412 /* m is a good line, m2 is like end */
414 if (!mark_same(m2, end))
415 call("Notify:clip", ci->focus, 0, m2, NULL,
417 mark_to_mark(end, m);
419 comm_call(comm, "", ci->focus, 0, m, str);
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);
433 /* filtered document is now empty - maybe someone cares */
434 home_call(ci->focus, "Notify:filter:empty", ci->home);
440 /* don't save anything */
446 struct filter_data *fd = ci->home->data;
447 int rpt = RPT_NUM(ci);
448 bool one_more = ci->num2 > 0;
451 check_settings(ci->focus, fd);
456 line = rpt + 1 - one_more;
458 line = rpt - 1 + one_more;
459 /* 'line' is which line to go to relative to here */
463 call("doc:EOL", ci->home->parent, -1, ci->mark);
466 call("doc:EOL", ci->home->parent, 1, ci->mark);
469 /* Must be at start of line for filtering to work */
470 call("doc:EOL", ci->home->parent, -1, ci->mark);
473 ret = do_filter_line_prev(fd, ci->mark,
474 ci->home->parent, ci->focus, 1, NULL);
481 struct call_return cr;
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)
489 if ((rpt < 0 && !one_more) || (rpt > 0 && one_more))
490 /* Target was start of a line, so we are there */
492 call("doc:EOL", ci->home->parent, 1, ci->mark);
496 DEF_CMD(filter_damaged)
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);
507 DEF_CMD(filter_attach);
508 DEF_CMD(filter_clone)
510 struct pane *parent = ci->focus;
512 filter_attach.func(ci);
513 pane_clone_children(ci->home, parent->focus);
517 static struct map *filter_map;
518 DEF_LOOKUP_CMD(filter_handle, filter_map);
520 static void filter_register_map(void)
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
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);
538 REDEF_CMD(filter_attach)
542 filter_register_map();
544 filter = pane_register(ci->focus, 0, &filter_handle.c);
547 pane_damaged(filter, DAMAGED_VIEW);
548 call("doc:request:doc:replaced", filter);
550 return comm_call(ci->comm2, "", filter);
553 void edlib_init(struct pane *ed safe)
555 call_comm("global-set-command", ed, &filter_attach,
556 0, NULL, "attach-linefilter");