Case sensitive searching works, but only in UTF-8 mode
[elinks.git] / src / viewer / text / search.c
blobaa267d39193274b56e00b771576217972fc1b632
1 /* Searching in the HTML document */
3 #ifndef _GNU_SOURCE
4 #define _GNU_SOURCE /* XXX: we _WANT_ strcasestr() ! */
5 #endif
7 #ifdef HAVE_CONFIG_H
8 #include "config.h"
9 #endif
11 #include <ctype.h> /* tolower(), isprint() */
12 #include <sys/types.h> /* FreeBSD needs this before regex.h */
13 #ifdef HAVE_REGEX_H
14 #include <regex.h>
15 #endif
16 #include <stdlib.h>
17 #include <string.h>
19 #include "elinks.h"
21 #include "bfu/dialog.h"
22 #include "config/kbdbind.h"
23 #include "document/document.h"
24 #include "document/view.h"
25 #include "intl/charsets.h"
26 #include "intl/gettext/libintl.h"
27 #include "main/event.h"
28 #include "main/module.h"
29 #include "session/session.h"
30 #include "terminal/screen.h"
31 #include "terminal/terminal.h"
32 #include "util/color.h"
33 #include "util/error.h"
34 #include "util/memory.h"
35 #include "util/string.h"
36 #include "viewer/action.h"
37 #include "viewer/text/draw.h"
38 #include "viewer/text/link.h"
39 #include "viewer/text/search.h"
40 #include "viewer/text/view.h"
41 #include "viewer/text/vs.h"
44 #define SEARCH_HISTORY_FILENAME "searchhist"
46 static INIT_INPUT_HISTORY(search_history);
48 #undef UCHAR
49 #ifdef CONFIG_UTF_8
50 #define UCHAR unicode_val_T
51 #else
52 #define UCHAR unsigned char
53 #endif
55 static inline void
56 add_srch_chr(struct document *document, UCHAR c, int x, int y, int nn)
58 assert(document);
59 if_assert_failed return;
61 if (c == ' ' && !document->nsearch) return;
63 if (document->search) {
64 int n = document->nsearch;
66 if (c == ' ' && document->search[n - 1].c == ' ')
67 return;
69 document->search[n].c = c;
70 document->search[n].x = x;
71 document->search[n].y = y;
72 document->search[n].n = nn;
75 document->nsearch++;
78 static void
79 sort_srch(struct document *document)
81 int i;
82 int *min, *max;
84 assert(document);
85 if_assert_failed return;
87 document->slines1 = mem_calloc(document->height, sizeof(*document->slines1));
88 if (!document->slines1) return;
90 document->slines2 = mem_calloc(document->height, sizeof(*document->slines2));
91 if (!document->slines2) {
92 mem_free(document->slines1);
93 return;
96 min = mem_calloc(document->height, sizeof(*min));
97 if (!min) {
98 mem_free(document->slines1);
99 mem_free(document->slines2);
100 return;
103 max = mem_calloc(document->height, sizeof(*max));
104 if (!max) {
105 mem_free(document->slines1);
106 mem_free(document->slines2);
107 mem_free(min);
108 return;
111 for (i = 0; i < document->height; i++) {
112 min[i] = INT_MAX;
113 max[i] = 0;
116 for (i = 0; i < document->nsearch; i++) {
117 struct search *s = &document->search[i];
118 int sxn = s->x + s->n;
120 if (s->x < min[s->y]) {
121 min[s->y] = s->x;
122 document->slines1[s->y] = s;
124 if (sxn > max[s->y]) {
125 max[s->y] = sxn;
126 document->slines2[s->y] = s;
130 mem_free(min);
131 mem_free(max);
134 static int
135 get_srch(struct document *document)
137 struct node *node;
139 assert(document && document->nsearch == 0);
141 if_assert_failed return 0;
143 foreachback (node, document->nodes) {
144 int x, y;
145 int height = int_min(node->box.y + node->box.height, document->height);
147 for (y = node->box.y; y < height; y++) {
148 int width = int_min(node->box.x + node->box.width,
149 document->data[y].length);
151 for (x = node->box.x;
152 x < width && document->data[y].chars[x].data <= ' ';
153 x++);
155 for (; x < width; x++) {
156 UCHAR c = document->data[y].chars[x].data;
157 int count = 0;
158 int xx;
160 if (document->data[y].chars[x].attr & SCREEN_ATTR_UNSEARCHABLE)
161 continue;
163 if (c > ' ') {
164 add_srch_chr(document, c, x, y, 1);
165 continue;
168 for (xx = x + 1; xx < width; xx++) {
169 if (document->data[y].chars[xx].data < ' ')
170 continue;
171 count = xx - x;
172 break;
175 add_srch_chr(document, ' ', x, y, count);
176 x = xx - 1;
179 add_srch_chr(document, ' ', x, y, 0);
183 return document->nsearch;
186 static void
187 get_search_data(struct document *document)
189 int n;
191 assert(document);
192 if_assert_failed return;
194 if (document->search) return;
196 n = get_srch(document);
197 if (!n) return;
199 document->nsearch = 0;
201 document->search = mem_alloc(n * sizeof(*document->search));
202 if (!document->search) return;
204 get_srch(document);
205 while (document->nsearch
206 && document->search[--document->nsearch].c == ' ');
207 sort_srch(document);
210 /* Assign s1 and s2 the first search node and the last search node needed to
211 * form the region starting at line y and ending at the greater of y + height
212 * and the end of the document, with allowance at the start to allow for
213 * multi-line matches that would otherwise be partially outside of the region.
215 * Returns -1 on assertion failure, 1 if s1 and s2 are not found,
216 * and 0 if they are found. */
217 static int
218 get_range(struct document *document, int y, int height, int l,
219 struct search **s1, struct search **s2)
221 int i;
223 assert(document && s1 && s2);
224 if_assert_failed return -1;
226 *s1 = *s2 = NULL;
227 int_lower_bound(&y, 0);
229 /* Starting with line y, find the search node referencing the earliest
230 * point in the document text and the node referencing the last point,
231 * respectively s1 and s2.
233 for (i = y; i < y + height && i < document->height; i++) {
234 if (document->slines1[i] && (!*s1 || document->slines1[i] < *s1))
235 *s1 = document->slines1[i];
236 if (document->slines2[i] && (!*s2 || document->slines2[i] > *s2))
237 *s2 = document->slines2[i];
239 if (!*s1 || !*s2) return 1;
241 /* Skip back by l to facilitate multi-line matches where the match
242 * begins before the start of the search region but is still partly
243 * within. */
244 *s1 -= l;
246 if (*s1 < document->search)
247 *s1 = document->search;
248 if (*s2 > document->search + document->nsearch - l + 1)
249 *s2 = document->search + document->nsearch - l + 1;
250 if (*s1 > *s2)
251 *s1 = *s2 = NULL;
252 if (!*s1 || !*s2)
253 return 1;
255 return 0;
258 /* Returns a string |doc| that is a copy of the text in the search nodes
259 * from |s1| to |s1 + doclen - 1| with the space at the end of each line
260 * converted to a new-line character (LF). */
261 static UCHAR *
262 get_search_region_from_search_nodes(struct search *s1, struct search *s2,
263 int pattern_len, int *doclen)
265 UCHAR *doc;
266 int i;
268 *doclen = s2 - s1 + pattern_len;
269 if (!*doclen) return NULL;
271 doc = mem_alloc((*doclen + 1) * sizeof(UCHAR));
272 if (!doc) {
273 *doclen = -1;
274 return NULL;
277 for (i = 0; i < *doclen; i++) {
278 if (i > 0 && s1[i - 1].c == ' ' && s1[i - 1].y != s1[i].y) {
279 doc[i - 1] = '\n';
281 doc[i] = s1[i].c;
284 doc[*doclen] = 0;
286 return doc;
289 #ifdef HAVE_REGEX_H
290 struct regex_match_context {
291 struct search *s1;
292 struct search *s2;
293 int textlen;
294 int y1;
295 int y2;
296 int found;
297 unsigned char *pattern;
300 static int
301 init_regex(regex_t *regex, unsigned char *pattern)
303 int regex_flags = REG_NEWLINE;
304 int reg_err;
306 if (get_opt_int("document.browse.search.regex") == 2)
307 regex_flags |= REG_EXTENDED;
309 if (!get_opt_bool("document.browse.search.case"))
310 regex_flags |= REG_ICASE;
312 reg_err = regcomp(regex, pattern, regex_flags);
313 if (reg_err) {
314 regfree(regex);
315 return 0;
318 return 1;
321 static void
322 search_for_pattern(struct regex_match_context *common_ctx, void *data,
323 void (*match)(struct regex_match_context *, void *))
325 UCHAR *doc;
326 UCHAR *doctmp;
327 int doclen;
328 int regexec_flags = 0;
329 regex_t regex;
330 regmatch_t regmatch;
331 int pos = 0;
332 struct search *search_start = common_ctx->s1;
333 unsigned char save_c;
335 /* TODO: show error message */
336 /* XXX: This will probably require that reg_err be passed thru
337 * common_ctx to the caller. */
338 if (!init_regex(&regex, common_ctx->pattern)) {
339 #if 0
340 /* Where and how should we display the error dialog ? */
341 unsigned char regerror_string[MAX_STR_LEN];
343 regerror(reg_err, &regex, regerror_string, sizeof(regerror_string));
344 #endif
345 common_ctx->found = -2;
346 return;
349 doc = get_search_region_from_search_nodes(common_ctx->s1, common_ctx->s2, common_ctx->textlen, &doclen);
350 if (!doc) {
351 regfree(&regex);
352 common_ctx->found = doclen;
353 return;
356 doctmp = doc;
358 find_next:
359 while (pos < doclen) {
360 int y = search_start[pos].y;
362 if (y >= common_ctx->y1 && y <= common_ctx->y2) break;
363 pos++;
365 doctmp = &doc[pos];
366 common_ctx->s1 = &search_start[pos];
368 while (pos < doclen) {
369 int y = search_start[pos].y;
371 if (y < common_ctx->y1 || y > common_ctx->y2) break;
372 pos++;
374 save_c = doc[pos];
375 doc[pos] = 0;
377 while (*doctmp && !regexec(&regex, doctmp, 1, &regmatch, regexec_flags)) {
378 regexec_flags = REG_NOTBOL;
379 common_ctx->textlen = regmatch.rm_eo - regmatch.rm_so;
380 if (!common_ctx->textlen) { doc[pos] = save_c; common_ctx->found = 1; goto free_stuff; }
381 common_ctx->s1 += regmatch.rm_so;
382 doctmp += regmatch.rm_so;
384 match(common_ctx, data);
386 doctmp += int_max(common_ctx->textlen, 1);
387 common_ctx->s1 += int_max(common_ctx->textlen, 1);
390 doc[pos] = save_c;
391 if (pos < doclen)
392 goto find_next;
394 free_stuff:
395 regfree(&regex);
396 mem_free(doc);
399 struct is_in_range_regex_context {
400 int y;
401 int *min;
402 int *max;
405 static void
406 is_in_range_regex_match(struct regex_match_context *common_ctx, void *data)
408 struct is_in_range_regex_context *ctx = data;
409 int i;
411 if (common_ctx->s1[common_ctx->textlen].y < ctx->y || common_ctx->s1[common_ctx->textlen].y >= common_ctx->y2)
412 return;
414 common_ctx->found = 1;
416 for (i = 0; i < common_ctx->textlen; i++) {
417 if (!common_ctx->s1[i].n) continue;
419 int_upper_bound(ctx->min, common_ctx->s1[i].x);
420 int_lower_bound(ctx->max, common_ctx->s1[i].x + common_ctx->s1[i].n);
424 static int
425 is_in_range_regex(struct document *document, int y, int height,
426 unsigned char *text, int textlen,
427 int *min, int *max,
428 struct search *s1, struct search *s2)
430 struct regex_match_context common_ctx;
431 struct is_in_range_regex_context ctx;
433 ctx.y = y;
434 ctx.min = min;
435 ctx.max = max;
437 common_ctx.found = 0;
438 common_ctx.textlen = textlen;
439 common_ctx.y1 = y - 1;
440 common_ctx.y2 = y + height;
441 common_ctx.pattern = text;
442 common_ctx.s1 = s1;
443 common_ctx.s2 = s2;
445 search_for_pattern(&common_ctx, &ctx, is_in_range_regex_match);
447 return common_ctx.found;
449 #endif /* HAVE_REGEX_H */
451 static UCHAR *
452 memacpy_u(unsigned char *text, int textlen)
454 #ifdef CONFIG_UTF_8
455 UCHAR *mem = mem_alloc((textlen + 1) * sizeof(UCHAR));
456 int i;
458 if (!mem) return NULL;
459 for (i = 0; i < textlen; i++) mem[i] = utf_8_to_unicode(&text, text + 7);
460 mem[textlen] = 0;
461 return mem;
462 #else
463 return memacpy(text, textlen);
464 #endif
467 /* Returns an allocated string which is a lowered copy of passed one. */
468 static UCHAR *
469 lowered_string(UCHAR *text, int textlen)
471 UCHAR *ret;
473 if (textlen < 0) textlen = strlen_u(text);
475 ret = mem_calloc(1, (textlen + 1) * sizeof(UCHAR));
476 if (ret && textlen) {
477 do {
478 ret[textlen] = tolower(text[textlen]);
479 } while (textlen--);
482 return ret;
485 static int
486 is_in_range_plain(struct document *document, int y, int height,
487 unsigned *text, int textlen,
488 int *min, int *max,
489 struct search *s1, struct search *s2)
491 int yy = y + height;
492 UCHAR *txt;
493 int found = 0;
494 int case_sensitive = get_opt_bool("document.browse.search.case");
496 txt = case_sensitive ? memacpy_u(text, textlen) : lowered_string(text, textlen);
497 if (!txt) return -1;
499 /* TODO: This is a great candidate for nice optimizations. Fresh CS
500 * graduates can use their knowledge of ie. KMP (should be quite
501 * trivial, probably a starter; very fast as well) or Turbo-BM (or
502 * maybe some other Boyer-Moore variant, I don't feel that strong in
503 * this area), hmm? >:) --pasky */
505 #define maybe_tolower(c) (case_sensitive ? (c) : tolower(c))
507 for (; s1 <= s2; s1++) {
508 int i;
510 if (maybe_tolower(s1->c) != txt[0]) {
511 srch_failed:
512 continue;
515 for (i = 1; i < textlen; i++)
516 if (maybe_tolower(s1[i].c) != txt[i])
517 goto srch_failed;
519 if (s1[i].y < y || s1[i].y >= yy)
520 continue;
522 found = 1;
524 for (i = 0; i < textlen; i++) {
525 if (!s1[i].n) continue;
527 int_upper_bound(min, s1[i].x);
528 int_lower_bound(max, s1[i].x + s1[i].n);
532 #undef maybe_tolower
534 mem_free(txt);
536 return found;
539 static int
540 strlen_u(unsigned char *text)
542 #ifdef CONFIG_UTF_8
543 return strlen_utf8(&text);
544 #else
545 return strlen(text);
546 #endif
549 static int
550 is_in_range(struct document *document, int y, int height,
551 unsigned char *text, int *min, int *max)
553 struct search *s1, *s2;
554 int textlen;
556 assert(document && text && min && max);
557 if_assert_failed return -1;
559 *min = INT_MAX, *max = 0;
560 textlen = strlen_u(text);
562 if (get_range(document, y, height, textlen, &s1, &s2))
563 return 0;
565 #ifdef HAVE_REGEX_H
566 if (get_opt_int("document.browse.search.regex"))
567 return is_in_range_regex(document, y, height, text, textlen,
568 min, max, s1, s2);
569 #endif
570 return is_in_range_plain(document, y, height, text, textlen,
571 min, max, s1, s2);
574 #define realloc_points(pts, size) \
575 mem_align_alloc(pts, size, (size) + 1, 0xFF)
577 static void
578 get_searched_plain(struct document_view *doc_view, struct point **pt, int *pl,
579 int l, struct search *s1, struct search *s2)
581 UCHAR *txt;
582 struct point *points = NULL;
583 struct box *box;
584 int xoffset, yoffset;
585 int len = 0;
586 int case_sensitive = get_opt_bool("document.browse.search.case");
588 txt = case_sensitive ? memacpy_u(*doc_view->search_word, l)
589 : lowered_string(*doc_view->search_word, l);
590 if (!txt) return;
592 box = &doc_view->box;
593 xoffset = box->x - doc_view->vs->x;
594 yoffset = box->y - doc_view->vs->y;
596 #define maybe_tolower(c) (case_sensitive ? (c) : tolower(c))
598 for (; s1 <= s2; s1++) {
599 int i;
601 if (maybe_tolower(s1[0].c) != txt[0]) {
602 srch_failed:
603 continue;
606 for (i = 1; i < l; i++)
607 if (maybe_tolower(s1[i].c) != txt[i])
608 goto srch_failed;
610 for (i = 0; i < l; i++) {
611 int j;
612 int y = s1[i].y + yoffset;
614 if (!row_is_in_box(box, y))
615 continue;
617 for (j = 0; j < s1[i].n; j++) {
618 int sx = s1[i].x + j;
619 int x = sx + xoffset;
621 if (!col_is_in_box(box, x))
622 continue;
624 if (!realloc_points(&points, len))
625 continue;
627 points[len].x = sx;
628 points[len++].y = s1[i].y;
633 #undef maybe_tolower
635 mem_free(txt);
636 *pt = points;
637 *pl = len;
640 #ifdef HAVE_REGEX_H
641 struct get_searched_regex_context {
642 int xoffset;
643 int yoffset;
644 struct box *box;
645 struct point *points;
646 int len;
649 static void
650 get_searched_regex_match(struct regex_match_context *common_ctx, void *data)
652 struct get_searched_regex_context *ctx = data;
653 int i;
655 for (i = 0; i < common_ctx->textlen; i++) {
656 int j;
657 int y = common_ctx->s1[i].y + ctx->yoffset;
659 if (!row_is_in_box(ctx->box, y))
660 continue;
662 for (j = 0; j < common_ctx->s1[i].n; j++) {
663 int sx = common_ctx->s1[i].x + j;
664 int x = sx + ctx->xoffset;
666 if (!col_is_in_box(ctx->box, x))
667 continue;
669 if (!realloc_points(&ctx->points, ctx->len))
670 continue;
672 ctx->points[ctx->len].x = sx;
673 ctx->points[ctx->len++].y = common_ctx->s1[i].y;
678 static void
679 get_searched_regex(struct document_view *doc_view, struct point **pt, int *pl,
680 int textlen, struct search *s1, struct search *s2)
682 struct regex_match_context common_ctx;
683 struct get_searched_regex_context ctx;
685 ctx.points = NULL;
686 ctx.len = 0;
687 ctx.box = &doc_view->box;
688 ctx.xoffset = ctx.box->x - doc_view->vs->x;
689 ctx.yoffset = ctx.box->y - doc_view->vs->y;
691 common_ctx.found = 0;
692 common_ctx.textlen = textlen;
693 common_ctx.y1 = doc_view->vs->y - 1;
694 common_ctx.y2 = doc_view->vs->y + ctx.box->height;
695 common_ctx.pattern = *doc_view->search_word;
696 common_ctx.s1 = s1;
697 common_ctx.s2 = s2;
699 search_for_pattern(&common_ctx, &ctx, get_searched_regex_match);
701 *pt = ctx.points;
702 *pl = ctx.len;
704 #endif /* HAVE_REGEX_H */
706 static void
707 get_searched(struct document_view *doc_view, struct point **pt, int *pl)
709 struct search *s1, *s2;
710 int l;
712 assert(doc_view && doc_view->vs && pt && pl);
713 if_assert_failed return;
715 if (!has_search_word(doc_view))
716 return;
718 get_search_data(doc_view->document);
719 l = strlen_u(*doc_view->search_word);
720 if (get_range(doc_view->document, doc_view->vs->y,
721 doc_view->box.height, l, &s1, &s2)) {
722 *pt = NULL;
723 *pl = 0;
725 return;
728 #ifdef HAVE_REGEX_H
729 if (get_opt_int("document.browse.search.regex"))
730 get_searched_regex(doc_view, pt, pl, l, s1, s2);
731 else
732 #endif
733 get_searched_plain(doc_view, pt, pl, l, s1, s2);
736 /* Highlighting of searched strings. */
737 void
738 draw_searched(struct terminal *term, struct document_view *doc_view)
740 struct point *pt = NULL;
741 int len = 0;
743 assert(term && doc_view);
744 if_assert_failed return;
746 if (!has_search_word(doc_view))
747 return;
749 get_searched(doc_view, &pt, &len);
750 if (len) {
751 int i;
752 struct color_pair *color = get_bfu_color(term, "searched");
753 int xoffset = doc_view->box.x - doc_view->vs->x;
754 int yoffset = doc_view->box.y - doc_view->vs->y;
756 for (i = 0; i < len; i++) {
757 int x = pt[i].x + xoffset;
758 int y = pt[i].y + yoffset;
760 /* TODO: We should take in account original colors and
761 * combine them with defined color. */
762 #if 0
763 /* This piece of code shows the old way of handling
764 * colors and screen char attributes. */
765 unsigned co = get_char(term, x, y);
766 co = ((co >> 3) & 0x0700) | ((co << 3) & 0x3800);
767 #endif
769 draw_char_color(term, x, y, color);
773 mem_free_if(pt);
777 enum find_error {
778 FIND_ERROR_NONE,
779 FIND_ERROR_NO_PREVIOUS_SEARCH,
780 FIND_ERROR_HIT_TOP,
781 FIND_ERROR_HIT_BOTTOM,
782 FIND_ERROR_NOT_FOUND,
783 FIND_ERROR_MEMORY,
784 FIND_ERROR_REGEX,
787 static enum find_error find_next_do(struct session *ses,
788 struct document_view *doc_view,
789 int direction);
791 static void print_find_error(struct session *ses, enum find_error find_error);
793 static enum find_error
794 search_for_do(struct session *ses, unsigned char *str, int direction,
795 int report_errors)
797 struct document_view *doc_view;
798 enum find_error error;
800 assert(ses && str);
801 if_assert_failed return FIND_ERROR_NOT_FOUND;
803 doc_view = current_frame(ses);
805 assert(doc_view);
806 if_assert_failed return FIND_ERROR_NOT_FOUND;
808 mem_free_set(&ses->search_word, NULL);
809 mem_free_set(&ses->last_search_word, NULL);
811 if (!*str) return FIND_ERROR_NOT_FOUND;
813 /* We only set the last search word because we don.t want find_next()
814 * to try to find next link in search before the search data has been
815 * initialized. find_next() will set ses->search_word for us. */
816 ses->last_search_word = stracpy(str);
817 if (!ses->last_search_word) return FIND_ERROR_NOT_FOUND;
819 ses->search_direction = direction;
821 error = find_next_do(ses, doc_view, 1);
823 if (report_errors)
824 print_find_error(ses, error);
826 return error;
829 static void
830 search_for_back(struct session *ses, unsigned char *str)
832 assert(ses && str);
833 if_assert_failed return;
835 search_for_do(ses, str, -1, 1);
838 static void
839 search_for(struct session *ses, unsigned char *str)
841 assert(ses && str);
842 if_assert_failed return;
844 search_for_do(ses, str, 1, 1);
848 static inline int
849 point_intersect(struct point *p1, int l1, struct point *p2, int l2)
851 #define HASH_SIZE 4096
852 #define HASH(p) ((((p).y << 6) + (p).x) & (HASH_SIZE - 1))
854 int i;
855 static char hash[HASH_SIZE];
856 static int first_time = 1;
858 assert(p2);
859 if_assert_failed return 0;
861 if (first_time) memset(hash, 0, HASH_SIZE), first_time = 0;
863 for (i = 0; i < l1; i++) hash[HASH(p1[i])] = 1;
865 for (i = 0; i < l2; i++) {
866 int j;
868 if (!hash[HASH(p2[i])]) continue;
870 for (j = 0; j < l1; j++) {
871 if (p1[j].x != p2[i].x) continue;
872 if (p1[j].y != p2[i].y) continue;
874 for (i = 0; i < l1; i++)
875 hash[HASH(p1[i])] = 0;
877 return 1;
881 for (i = 0; i < l1; i++) hash[HASH(p1[i])] = 0;
883 return 0;
885 #undef HASH
886 #undef HASH_SIZE
889 static int
890 find_next_link_in_search(struct document_view *doc_view, int direction)
892 assert(doc_view && doc_view->vs);
893 if_assert_failed return 0;
895 if (direction == -2 || direction == 2) {
896 direction /= 2;
897 if (direction < 0)
898 find_link_page_up(doc_view);
899 else
900 find_link_page_down(doc_view);
902 if (doc_view->vs->current_link == -1) return 1;
903 goto nt;
906 while (doc_view->vs->current_link != -1
907 && next_link_in_view(doc_view, doc_view->vs->current_link + direction,
908 direction)) {
909 struct point *pt = NULL;
910 struct link *link;
911 int len;
914 link = &doc_view->document->links[doc_view->vs->current_link];
915 get_searched(doc_view, &pt, &len);
916 if (point_intersect(pt, len, link->points, link->npoints)) {
917 mem_free(pt);
918 return 0;
920 mem_free_if(pt);
923 if (direction < 0)
924 find_link_page_up(doc_view);
925 else
926 find_link_page_down(doc_view);
928 return 1;
931 static enum find_error
932 find_next_do(struct session *ses, struct document_view *doc_view, int direction)
934 int p, min, max, c = 0;
935 int step, hit_bottom = 0, hit_top = 0;
936 int height;
938 assert(ses && ses->tab && ses->tab->term && doc_view && doc_view->vs
939 && direction);
940 if_assert_failed return FIND_ERROR_NONE;
942 direction *= ses->search_direction;
943 p = doc_view->vs->y;
944 height = doc_view->box.height;
945 step = direction * height;
947 if (ses->search_word) {
948 if (!find_next_link_in_search(doc_view, direction))
949 return FIND_ERROR_NONE;
950 p += step;
953 if (!ses->search_word) {
954 if (!ses->last_search_word) {
955 return FIND_ERROR_NO_PREVIOUS_SEARCH;
957 ses->search_word = stracpy(ses->last_search_word);
958 if (!ses->search_word) return FIND_ERROR_NONE;
961 get_search_data(doc_view->document);
963 do {
964 int in_range = is_in_range(doc_view->document, p, height,
965 ses->search_word, &min, &max);
967 if (in_range == -1) return FIND_ERROR_MEMORY;
968 if (in_range == -2) return FIND_ERROR_REGEX;
969 if (in_range) {
970 doc_view->vs->y = p;
971 if (max >= min)
972 doc_view->vs->x = int_min(int_max(doc_view->vs->x,
973 max - doc_view->box.width),
974 min);
976 set_link(doc_view);
977 find_next_link_in_search(doc_view, direction * 2);
979 if (hit_top)
980 return FIND_ERROR_HIT_TOP;
982 if (hit_bottom)
983 return FIND_ERROR_HIT_BOTTOM;
985 return FIND_ERROR_NONE;
987 p += step;
988 if (p > doc_view->document->height) {
989 hit_bottom = 1;
990 p = 0;
992 if (p < 0) {
993 hit_top = 1;
994 p = 0;
995 while (p < doc_view->document->height) p += height;
996 p -= height;
998 c += height;
999 } while (c < doc_view->document->height + height);
1001 return FIND_ERROR_NOT_FOUND;
1004 static void
1005 print_find_error_not_found(struct session *ses, unsigned char *title,
1006 unsigned char *message, unsigned char *search_string)
1008 switch (get_opt_int("document.browse.search.show_not_found")) {
1009 case 2:
1010 info_box(ses->tab->term, MSGBOX_FREE_TEXT,
1011 title, ALIGN_CENTER,
1012 msg_text(ses->tab->term, message,
1013 search_string));
1014 break;
1016 case 1:
1017 beep_terminal(ses->tab->term);
1019 default:
1020 break;
1024 static void
1025 print_find_error(struct session *ses, enum find_error find_error)
1027 int hit_top = 0;
1028 unsigned char *message = NULL;
1030 switch (find_error) {
1031 case FIND_ERROR_HIT_TOP:
1032 hit_top = 1;
1033 case FIND_ERROR_HIT_BOTTOM:
1034 if (!get_opt_bool("document.browse.search"
1035 ".show_hit_top_bottom"))
1036 break;
1038 message = hit_top
1039 ? N_("Search hit top, continuing at bottom.")
1040 : N_("Search hit bottom, continuing at top.");
1041 break;
1042 case FIND_ERROR_NO_PREVIOUS_SEARCH:
1043 message = N_("No previous search");
1044 break;
1045 case FIND_ERROR_NOT_FOUND:
1046 print_find_error_not_found(ses, N_("Search"),
1047 N_("Search string"
1048 " '%s' not found"),
1049 ses->search_word);
1051 break;
1053 case FIND_ERROR_REGEX:
1054 print_find_error_not_found(ses, N_("Search"),
1055 N_("Could not compile"
1056 " regular expression"
1057 " '%s'"),
1058 ses->search_word);
1060 break;
1062 case FIND_ERROR_MEMORY:
1063 /* Why bother trying to create a msg_box?
1064 * We probably don't have the memory... */
1065 case FIND_ERROR_NONE:
1066 break;
1069 if (!message) return;
1070 info_box(ses->tab->term, 0, N_("Search"), ALIGN_CENTER, message);
1073 enum frame_event_status
1074 find_next(struct session *ses, struct document_view *doc_view, int direction)
1076 print_find_error(ses, find_next_do(ses, doc_view, direction));
1078 /* FIXME: Make this more fine-grained */
1079 return FRAME_EVENT_REFRESH;
1083 /* Link typeahead */
1085 enum typeahead_code {
1086 TYPEAHEAD_MATCHED,
1087 TYPEAHEAD_ERROR,
1088 TYPEAHEAD_ERROR_NO_FURTHER,
1089 TYPEAHEAD_CANCEL,
1092 static void
1093 typeahead_error(struct session *ses, unsigned char *typeahead, int no_further)
1095 unsigned char *message;
1097 if (no_further)
1098 message = N_("No further matches for '%s'.");
1099 else
1100 message = N_("Could not find a link with the text '%s'.");
1102 print_find_error_not_found(ses, N_("Typeahead"), message, typeahead);
1105 static inline unsigned char *
1106 get_link_typeahead_text(struct link *link)
1108 unsigned char *name = get_link_name(link);
1110 if (name) return name;
1111 if (link->where) return link->where;
1112 if (link->where_img) return link->where_img;
1114 return "";
1117 static int
1118 match_link_text(struct link *link, unsigned char *text, int textlen,
1119 int case_sensitive)
1121 unsigned char *match = get_link_typeahead_text(link);
1122 unsigned char *matchpos;
1124 if (link_is_form(link) || textlen > strlen(match))
1125 return -1;
1127 matchpos = case_sensitive ? strstr(match, text)
1128 : strcasestr(match, text);
1130 if (matchpos) {
1131 return matchpos - match;
1134 return -1;
1137 /* Searches the @document for a link with the given @text. takes the
1138 * current_link in the view, the link to start searching from @i and the
1139 * direction to search (1 is forward, -1 is back). */
1140 static inline int
1141 search_link_text(struct document *document, int current_link, int i,
1142 unsigned char *text, int direction, int *offset)
1144 int upper_link, lower_link;
1145 int case_sensitive = get_opt_bool("document.browse.search.case");
1146 int wraparound = get_opt_bool("document.browse.search.wraparound");
1147 int textlen = strlen(text);
1149 assert(textlen && direction && offset);
1151 /* The link interval in which we are currently searching */
1152 /* Set up the range of links that should be search in first attempt */
1153 if (direction > 0) {
1154 upper_link = document->nlinks;
1155 lower_link = i - 1;
1156 } else {
1157 upper_link = i + 1;
1158 lower_link = -1;
1161 for (; i > lower_link && i < upper_link; i += direction) {
1162 struct link *link = &document->links[i];
1163 int match_offset = match_link_text(link, text, textlen,
1164 case_sensitive);
1166 if (match_offset >= 0) {
1167 *offset = match_offset;
1168 return i;
1171 if (!wraparound) continue;
1173 /* Check if we are at the end of the first range.
1174 * Only wrap around one time. Initialize @i with
1175 * {+= direction} in mind. */
1176 if (direction > 0) {
1177 if (i == upper_link - 1) {
1178 upper_link = current_link + 1;
1179 lower_link = -1;
1180 i = lower_link;
1181 wraparound = 0;
1183 } else {
1184 if (i == lower_link + 1) {
1185 upper_link = document->nlinks;
1186 lower_link = current_link - 1;
1187 i = upper_link;
1188 wraparound = 0;
1193 return -1;
1196 /* The typeahead input line takes up one of the viewed lines so we
1197 * might have to scroll if the link is under the input line. */
1198 static inline void
1199 fixup_typeahead_match(struct session *ses, struct document_view *doc_view)
1201 int current_link = doc_view->vs->current_link;
1202 struct link *link = &doc_view->document->links[current_link];
1204 doc_view->box.height -= 1;
1205 set_pos_x(doc_view, link);
1206 set_pos_y(doc_view, link);
1207 doc_view->box.height += 1;
1210 static inline UCHAR
1211 get_document_char(struct document *document, int x, int y)
1213 return (document->height > y && document->data[y].length > x)
1214 ? document->data[y].chars[x].data : 0;
1217 static void
1218 draw_typeahead_match(struct terminal *term, struct document_view *doc_view,
1219 int chars, int offset)
1221 struct color_pair *color = get_bfu_color(term, "searched");
1222 int xoffset = doc_view->box.x - doc_view->vs->x;
1223 int yoffset = doc_view->box.y - doc_view->vs->y;
1224 struct link *link = get_current_link(doc_view);
1225 UCHAR *text = get_link_typeahead_text(link);
1226 int end = offset + chars;
1227 int i, j;
1229 for (i = 0, j = 0; text[j] && i < end; i++, j++) {
1230 int x = link->points[i].x;
1231 int y = link->points[i].y;
1232 UCHAR data = get_document_char(doc_view->document, x, y);
1234 /* Text wrapping might remove space chars from the link
1235 * position array so try to align the matched typeahead text
1236 * with what is actually on the screen by shifting the link
1237 * position variables if the canvas data do not match. */
1238 if (data != text[j]) {
1239 i--;
1240 end--;
1241 offset--;
1243 } else if (i >= offset) {
1244 /* TODO: We should take in account original colors and
1245 * combine them with defined color. */
1246 draw_char_color(term, xoffset + x, yoffset + y, color);
1251 static enum typeahead_code
1252 do_typeahead(struct session *ses, struct document_view *doc_view,
1253 unsigned char *text, int action_id, int *offset)
1255 int current = int_max(doc_view->vs->current_link, 0);
1256 int direction, match, i = current;
1257 struct document *document = doc_view->document;
1259 switch (action_id) {
1260 case ACT_EDIT_PREVIOUS_ITEM:
1261 case ACT_EDIT_UP:
1262 direction = -1;
1263 i--;
1264 if (i >= 0) break;
1265 if (!get_opt_bool("document.browse.search.wraparound")) {
1266 search_hit_boundary:
1267 if (match_link_text(&document->links[current],
1268 text, strlen(text),
1269 get_opt_bool("document"
1270 ".browse"
1271 ".search"
1272 ".case"))
1273 >= 0) {
1274 return TYPEAHEAD_ERROR_NO_FURTHER;
1277 return TYPEAHEAD_ERROR;
1280 i = doc_view->document->nlinks - 1;
1281 break;
1283 case ACT_EDIT_NEXT_ITEM:
1284 case ACT_EDIT_DOWN:
1285 direction = 1;
1286 i++;
1287 if (i < doc_view->document->nlinks) break;
1288 if (!get_opt_bool("document.browse.search.wraparound"))
1289 goto search_hit_boundary;
1291 i = 0;
1292 break;
1294 case ACT_EDIT_ENTER:
1295 goto_current_link(ses, doc_view, 0);
1296 return TYPEAHEAD_CANCEL;
1298 default:
1299 direction = 1;
1302 match = search_link_text(document, current, i, text, direction, offset);
1304 if (match == current && i != current)
1305 return TYPEAHEAD_ERROR_NO_FURTHER;
1307 if (match < 0) {
1308 if (i != current)
1309 return TYPEAHEAD_ERROR_NO_FURTHER;
1311 return TYPEAHEAD_ERROR;
1314 assert(match >= 0 && match < doc_view->document->nlinks);
1316 doc_view->vs->current_link = match;
1317 return TYPEAHEAD_MATCHED;
1321 /* Typeahead */
1323 static enum input_line_code
1324 text_typeahead_handler(struct input_line *line, int action_id)
1326 struct session *ses = line->ses;
1327 unsigned char *buffer = line->buffer;
1328 struct document_view *doc_view = current_frame(ses);
1329 int direction = ((unsigned char *) line->data)[0] == '/' ? 1 : -1;
1330 int report_errors = action_id == -1;
1331 enum find_error error;
1333 assertm(doc_view, "document not formatted");
1334 if_assert_failed return INPUT_LINE_CANCEL;
1336 switch (action_id) {
1337 case ACT_EDIT_REDRAW:
1338 return INPUT_LINE_PROCEED;
1340 case ACT_EDIT_ENTER:
1341 if (!*buffer) {
1342 /* This ensures that search-typeahead-text
1343 * followed immediately with enter
1344 * clears the last search. */
1345 search_for_do(ses, buffer, direction, 0);
1347 goto_current_link(ses, doc_view, 0);
1348 return INPUT_LINE_CANCEL;
1350 case ACT_EDIT_PREVIOUS_ITEM:
1351 find_next(ses, doc_view, -1);
1352 break;
1354 case ACT_EDIT_NEXT_ITEM:
1355 find_next(ses, doc_view, 1);
1356 break;
1358 case ACT_EDIT_SEARCH_TOGGLE_REGEX: {
1359 struct option *opt =
1360 get_opt_rec(config_options,
1361 "document.browse.search.regex");
1363 opt->value.number = (opt->value.number + 1)
1364 % (opt->max + 1);
1365 option_changed(ses, opt, opt);
1367 /* Fall thru */
1369 default:
1370 error = search_for_do(ses, buffer, direction, 0);
1372 if (error == FIND_ERROR_REGEX)
1373 break;
1375 if (report_errors)
1376 print_find_error(ses, error);
1378 /* We need to check |*buffer| here because
1379 * the input-line code will call this handler
1380 * even after it handles a back-space press. */
1381 if (error != FIND_ERROR_HIT_TOP
1382 && error != FIND_ERROR_HIT_BOTTOM
1383 && error != FIND_ERROR_NONE && *buffer)
1384 return INPUT_LINE_REWIND;
1387 draw_formatted(ses, 0);
1388 return INPUT_LINE_PROCEED;
1391 static enum input_line_code
1392 link_typeahead_handler(struct input_line *line, int action_id)
1394 struct session *ses = line->ses;
1395 unsigned char *buffer = line->buffer;
1396 struct document_view *doc_view = current_frame(ses);
1397 int offset = 0;
1399 assertm(doc_view, "document not formatted");
1400 if_assert_failed return INPUT_LINE_CANCEL;
1402 /* If there is nothing to match with don't start searching */
1403 if (!*buffer) {
1404 /* If something already were typed we need to redraw
1405 * in order to remove the coloring of the link text. */
1406 if (line->data) draw_formatted(ses, 0);
1407 return INPUT_LINE_PROCEED;
1410 if (action_id == ACT_EDIT_REDRAW) {
1411 int current = doc_view->vs->current_link;
1412 int offset, bufferlen;
1414 if (current < 0) return INPUT_LINE_PROCEED;
1416 bufferlen = strlen(buffer);
1417 offset = match_link_text(&doc_view->document->links[current],
1418 buffer, bufferlen,
1419 get_opt_bool("document.browse"
1420 ".search.case"));
1422 if (offset >= 0) {
1423 draw_typeahead_match(ses->tab->term, doc_view,
1424 bufferlen, offset);
1427 return INPUT_LINE_PROCEED;
1430 /* Hack time .. should we change mode? */
1431 if (!line->data) {
1432 enum main_action action_id = ACT_MAIN_NONE;
1434 switch (*buffer) {
1435 case '#':
1436 action_id = ACT_MAIN_SEARCH_TYPEAHEAD_LINK;
1437 break;
1439 case '?':
1440 action_id = ACT_MAIN_SEARCH_TYPEAHEAD_TEXT_BACK;
1441 break;
1443 case '/':
1444 action_id = ACT_MAIN_SEARCH_TYPEAHEAD_TEXT;
1445 break;
1447 default:
1448 break;
1451 /* Should we reboot the input line .. (inefficient but easy) */
1452 if (action_id != ACT_MAIN_NONE) {
1453 search_typeahead(ses, doc_view, action_id);
1454 return INPUT_LINE_CANCEL;
1457 line->data = "#";
1460 switch (do_typeahead(ses, doc_view, buffer, action_id, &offset)) {
1461 case TYPEAHEAD_MATCHED:
1462 fixup_typeahead_match(ses, doc_view);
1463 draw_formatted(ses, 0);
1464 draw_typeahead_match(ses->tab->term, doc_view, strlen(buffer), offset);
1465 return INPUT_LINE_PROCEED;
1467 case TYPEAHEAD_ERROR_NO_FURTHER:
1468 typeahead_error(ses, buffer, 1);
1469 draw_typeahead_match(ses->tab->term, doc_view, strlen(buffer), offset);
1470 return INPUT_LINE_PROCEED;
1472 case TYPEAHEAD_ERROR:
1473 typeahead_error(ses, buffer, 0);
1474 return INPUT_LINE_REWIND;
1476 case TYPEAHEAD_CANCEL:
1477 default:
1478 return INPUT_LINE_CANCEL;
1482 enum frame_event_status
1483 search_typeahead(struct session *ses, struct document_view *doc_view,
1484 action_id_T action_id)
1486 unsigned char *prompt = "#";
1487 unsigned char *data = NULL;
1488 input_line_handler_T handler = text_typeahead_handler;
1489 struct input_history *history = &search_history;
1491 switch (action_id) {
1492 case ACT_MAIN_SEARCH_TYPEAHEAD_TEXT:
1493 prompt = data = "/";
1494 break;
1496 case ACT_MAIN_SEARCH_TYPEAHEAD_TEXT_BACK:
1497 prompt = data = "?";
1498 break;
1500 case ACT_MAIN_SEARCH_TYPEAHEAD_LINK:
1501 data = "#";
1502 /* Falling forward .. good punk rock */
1503 case ACT_MAIN_SEARCH_TYPEAHEAD:
1504 default:
1505 if (doc_view->document->nlinks) {
1506 handler = link_typeahead_handler;
1507 break;
1510 info_box(ses->tab->term, MSGBOX_FREE_TEXT,
1511 N_("Typeahead"), ALIGN_CENTER,
1512 msg_text(ses->tab->term,
1513 N_("No links in current document")));
1515 return FRAME_EVENT_OK;
1518 input_field_line(ses, prompt, data, history, handler);
1519 return FRAME_EVENT_OK;
1523 /* The dialog functions are clones of input_field() ones. Gross code
1524 * duplication. */
1525 /* TODO: This is just hacked input_field(), containing a lot of generic crap
1526 * etc. The useless cruft should be blasted out. And it's quite ugly anyway,
1527 * a nice cleanup target ;-). --pasky */
1529 enum search_option {
1530 SEARCH_OPT_REGEX,
1531 SEARCH_OPT_CASE,
1533 SEARCH_OPTIONS,
1536 static struct option_resolver resolvers[] = {
1537 { SEARCH_OPT_REGEX, "regex" },
1538 { SEARCH_OPT_CASE, "case" },
1541 struct search_dlg_hop {
1542 void *data;
1543 union option_value values[SEARCH_OPTIONS];
1546 static widget_handler_status_T
1547 search_dlg_cancel(struct dialog_data *dlg_data, struct widget_data *widget_data)
1549 void (*fn)(void *) = widget_data->widget->data;
1550 struct search_dlg_hop *hop = dlg_data->dlg->udata2;
1551 void *data = hop->data;
1553 if (fn) fn(data);
1554 return cancel_dialog(dlg_data, widget_data);
1557 static widget_handler_status_T
1558 search_dlg_ok(struct dialog_data *dlg_data, struct widget_data *widget_data)
1560 void (*fn)(void *, unsigned char *) = widget_data->widget->data;
1561 struct search_dlg_hop *hop = dlg_data->dlg->udata2;
1562 void *data = hop->data;
1563 unsigned char *text = dlg_data->widgets_data->cdata;
1565 update_dialog_data(dlg_data);
1567 commit_option_values(resolvers, get_opt_rec(config_options,
1568 "document.browse.search"),
1569 hop->values, SEARCH_OPTIONS);
1571 if (check_dialog(dlg_data)) return EVENT_NOT_PROCESSED;
1573 add_to_input_history(dlg_data->dlg->widgets->info.field.history, text, 1);
1575 if (fn) fn(data, text);
1577 return cancel_dialog(dlg_data, widget_data);
1580 /* XXX: @data is ignored. */
1581 static void
1582 search_dlg_do(struct terminal *term, struct memory_list *ml,
1583 unsigned char *title, void *data,
1584 struct input_history *history,
1585 void (*fn)(void *, unsigned char *))
1587 /* [gettext_accelerator_context(.search_dlg_do)] */
1588 struct dialog *dlg;
1589 unsigned char *field;
1590 struct search_dlg_hop *hop;
1591 unsigned char *text = _("Search for text", term);
1593 hop = mem_calloc(1, sizeof(*hop));
1594 if (!hop) return;
1596 checkout_option_values(resolvers, get_opt_rec(config_options,
1597 "document.browse.search"),
1598 hop->values, SEARCH_OPTIONS);
1599 hop->data = data;
1601 #define SEARCH_WIDGETS_COUNT 8
1602 dlg = calloc_dialog(SEARCH_WIDGETS_COUNT, MAX_STR_LEN);
1603 if (!dlg) {
1604 mem_free(hop);
1605 return;
1608 dlg->title = _(title, term);
1609 dlg->layouter = generic_dialog_layouter;
1610 dlg->layout.fit_datalen = 1;
1611 dlg->layout.float_groups = 1;
1612 dlg->udata = text;
1613 dlg->udata2 = hop;
1615 add_to_ml(&ml, hop, NULL);
1617 /* @field is automatically cleared by calloc() */
1618 field = get_dialog_offset(dlg, SEARCH_WIDGETS_COUNT);
1619 add_dlg_field(dlg, text, 0, 0, NULL, MAX_STR_LEN, field, history);
1621 add_dlg_radio(dlg, _("Normal search", term), 1, 0, &hop->values[SEARCH_OPT_REGEX].number);
1622 add_dlg_radio(dlg, _("Regexp search", term), 1, 1, &hop->values[SEARCH_OPT_REGEX].number);
1623 add_dlg_radio(dlg, _("Extended regexp search", term), 1, 2, &hop->values[SEARCH_OPT_REGEX].number);
1624 add_dlg_radio(dlg, _("Case sensitive", term), 2, 1, &hop->values[SEARCH_OPT_CASE].number);
1625 add_dlg_radio(dlg, _("Case insensitive", term), 2, 0, &hop->values[SEARCH_OPT_CASE].number);
1627 add_dlg_button(dlg, _("~OK", term), B_ENTER, search_dlg_ok, fn);
1628 add_dlg_button(dlg, _("~Cancel", term), B_ESC, search_dlg_cancel, NULL);
1630 add_dlg_end(dlg, SEARCH_WIDGETS_COUNT);
1632 add_to_ml(&ml, dlg, NULL);
1633 do_dialog(term, dlg, ml);
1636 enum frame_event_status
1637 search_dlg(struct session *ses, struct document_view *doc_view, int direction)
1639 unsigned char *title;
1640 void *search_function;
1642 assert(direction);
1643 if_assert_failed return FRAME_EVENT_OK;
1645 if (direction > 0) {
1646 title = N_("Search");
1647 search_function = search_for;
1648 } else {
1649 title = N_("Search backward");
1650 search_function = search_for_back;
1653 search_dlg_do(ses->tab->term, NULL,
1654 title, ses,
1655 &search_history,
1656 search_function);
1658 return FRAME_EVENT_OK;
1661 static enum evhook_status
1662 search_history_write_hook(va_list ap, void *data)
1664 save_input_history(&search_history, SEARCH_HISTORY_FILENAME);
1665 return EVENT_HOOK_STATUS_NEXT;
1668 static struct event_hook_info search_history_hooks[] = {
1669 { "periodic-saving", 0, search_history_write_hook, NULL },
1671 NULL_EVENT_HOOK_INFO,
1674 static void
1675 init_search_history(struct module *module)
1677 load_input_history(&search_history, SEARCH_HISTORY_FILENAME);
1680 static void
1681 done_search_history(struct module *module)
1683 save_input_history(&search_history, SEARCH_HISTORY_FILENAME);
1684 free_list(search_history.entries);
1687 struct module search_history_module = struct_module(
1688 /* name: */ N_("Search History"),
1689 /* options: */ NULL,
1690 /* hooks: */ search_history_hooks,
1691 /* submodules: */ NULL,
1692 /* data: */ NULL,
1693 /* init: */ init_search_history,
1694 /* done: */ done_search_history