1 /* Searching in the HTML document */
4 #define _GNU_SOURCE /* XXX: we _WANT_ strcasestr() ! */
11 #include <ctype.h> /* tolower(), isprint() */
12 #include <sys/types.h> /* FreeBSD needs this before regex.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
);
50 #define UCHAR unicode_val_T
52 #define UCHAR unsigned char
56 add_srch_chr(struct document
*document
, UCHAR c
, int x
, int y
, int nn
)
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
== ' ')
69 document
->search
[n
].c
= c
;
70 document
->search
[n
].x
= x
;
71 document
->search
[n
].y
= y
;
72 document
->search
[n
].n
= nn
;
79 sort_srch(struct document
*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
);
96 min
= mem_calloc(document
->height
, sizeof(*min
));
98 mem_free(document
->slines1
);
99 mem_free(document
->slines2
);
103 max
= mem_calloc(document
->height
, sizeof(*max
));
105 mem_free(document
->slines1
);
106 mem_free(document
->slines2
);
111 for (i
= 0; i
< document
->height
; i
++) {
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
]) {
122 document
->slines1
[s
->y
] = s
;
124 if (sxn
> max
[s
->y
]) {
126 document
->slines2
[s
->y
] = s
;
135 get_srch(struct document
*document
)
139 assert(document
&& document
->nsearch
== 0);
141 if_assert_failed
return 0;
143 foreachback (node
, document
->nodes
) {
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
<= ' ';
155 for (; x
< width
; x
++) {
156 UCHAR c
= document
->data
[y
].chars
[x
].data
;
160 if (document
->data
[y
].chars
[x
].attr
& SCREEN_ATTR_UNSEARCHABLE
)
164 add_srch_chr(document
, c
, x
, y
, 1);
168 for (xx
= x
+ 1; xx
< width
; xx
++) {
169 if (document
->data
[y
].chars
[xx
].data
< ' ')
175 add_srch_chr(document
, ' ', x
, y
, count
);
179 add_srch_chr(document
, ' ', x
, y
, 0);
183 return document
->nsearch
;
187 get_search_data(struct document
*document
)
192 if_assert_failed
return;
194 if (document
->search
) return;
196 n
= get_srch(document
);
199 document
->nsearch
= 0;
201 document
->search
= mem_alloc(n
* sizeof(*document
->search
));
202 if (!document
->search
) return;
205 while (document
->nsearch
206 && document
->search
[--document
->nsearch
].c
== ' ');
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. */
218 get_range(struct document
*document
, int y
, int height
, int l
,
219 struct search
**s1
, struct search
**s2
)
223 assert(document
&& s1
&& s2
);
224 if_assert_failed
return -1;
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
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;
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). */
262 get_search_region_from_search_nodes(struct search
*s1
, struct search
*s2
,
263 int pattern_len
, int *doclen
)
268 *doclen
= s2
- s1
+ pattern_len
;
269 if (!*doclen
) return NULL
;
271 doc
= mem_alloc((*doclen
+ 1) * sizeof(UCHAR
));
277 for (i
= 0; i
< *doclen
; i
++) {
278 if (i
> 0 && s1
[i
- 1].c
== ' ' && s1
[i
- 1].y
!= s1
[i
].y
) {
290 struct regex_match_context
{
297 unsigned char *pattern
;
301 init_regex(regex_t
*regex
, unsigned char *pattern
)
303 int regex_flags
= REG_NEWLINE
;
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
);
322 search_for_pattern(struct regex_match_context
*common_ctx
, void *data
,
323 void (*match
)(struct regex_match_context
*, void *))
328 int regexec_flags
= 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(®ex
, common_ctx
->pattern
)) {
340 /* Where and how should we display the error dialog ? */
341 unsigned char regerror_string
[MAX_STR_LEN
];
343 regerror(reg_err
, ®ex
, regerror_string
, sizeof(regerror_string
));
345 common_ctx
->found
= -2;
349 doc
= get_search_region_from_search_nodes(common_ctx
->s1
, common_ctx
->s2
, common_ctx
->textlen
, &doclen
);
352 common_ctx
->found
= doclen
;
359 while (pos
< doclen
) {
360 int y
= search_start
[pos
].y
;
362 if (y
>= common_ctx
->y1
&& y
<= common_ctx
->y2
) break;
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;
377 while (*doctmp
&& !regexec(®ex
, doctmp
, 1, ®match
, 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);
399 struct is_in_range_regex_context
{
406 is_in_range_regex_match(struct regex_match_context
*common_ctx
, void *data
)
408 struct is_in_range_regex_context
*ctx
= data
;
411 if (common_ctx
->s1
[common_ctx
->textlen
].y
< ctx
->y
|| common_ctx
->s1
[common_ctx
->textlen
].y
>= common_ctx
->y2
)
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
);
425 is_in_range_regex(struct document
*document
, int y
, int height
,
426 unsigned char *text
, int textlen
,
428 struct search
*s1
, struct search
*s2
)
430 struct regex_match_context common_ctx
;
431 struct is_in_range_regex_context ctx
;
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
;
445 search_for_pattern(&common_ctx
, &ctx
, is_in_range_regex_match
);
447 return common_ctx
.found
;
449 #endif /* HAVE_REGEX_H */
452 memacpy_u(unsigned char *text
, int textlen
)
455 UCHAR
*mem
= mem_alloc((textlen
+ 1) * sizeof(UCHAR
));
458 if (!mem
) return NULL
;
459 for (i
= 0; i
< textlen
; i
++) mem
[i
] = utf_8_to_unicode(&text
, text
+ 7);
463 return memacpy(text
, textlen
);
467 /* Returns an allocated string which is a lowered copy of passed one. */
469 lowered_string(UCHAR
*text
, int textlen
)
473 if (textlen
< 0) textlen
= strlen_u(text
);
475 ret
= mem_calloc(1, (textlen
+ 1) * sizeof(UCHAR
));
476 if (ret
&& textlen
) {
478 ret
[textlen
] = tolower(text
[textlen
]);
486 is_in_range_plain(struct document
*document
, int y
, int height
,
487 unsigned *text
, int textlen
,
489 struct search
*s1
, struct search
*s2
)
494 int case_sensitive
= get_opt_bool("document.browse.search.case");
496 txt
= case_sensitive
? memacpy_u(text
, textlen
) : lowered_string(text
, textlen
);
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
++) {
510 if (maybe_tolower(s1
->c
) != txt
[0]) {
515 for (i
= 1; i
< textlen
; i
++)
516 if (maybe_tolower(s1
[i
].c
) != txt
[i
])
519 if (s1
[i
].y
< y
|| s1
[i
].y
>= yy
)
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
);
540 strlen_u(unsigned char *text
)
543 return strlen_utf8(&text
);
550 is_in_range(struct document
*document
, int y
, int height
,
551 unsigned char *text
, int *min
, int *max
)
553 struct search
*s1
, *s2
;
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
))
566 if (get_opt_int("document.browse.search.regex"))
567 return is_in_range_regex(document
, y
, height
, text
, textlen
,
570 return is_in_range_plain(document
, y
, height
, text
, textlen
,
574 #define realloc_points(pts, size) \
575 mem_align_alloc(pts, size, (size) + 1, 0xFF)
578 get_searched_plain(struct document_view
*doc_view
, struct point
**pt
, int *pl
,
579 int l
, struct search
*s1
, struct search
*s2
)
582 struct point
*points
= NULL
;
584 int xoffset
, yoffset
;
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
);
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
++) {
601 if (maybe_tolower(s1
[0].c
) != txt
[0]) {
606 for (i
= 1; i
< l
; i
++)
607 if (maybe_tolower(s1
[i
].c
) != txt
[i
])
610 for (i
= 0; i
< l
; i
++) {
612 int y
= s1
[i
].y
+ yoffset
;
614 if (!row_is_in_box(box
, y
))
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
))
624 if (!realloc_points(&points
, len
))
628 points
[len
++].y
= s1
[i
].y
;
641 struct get_searched_regex_context
{
645 struct point
*points
;
650 get_searched_regex_match(struct regex_match_context
*common_ctx
, void *data
)
652 struct get_searched_regex_context
*ctx
= data
;
655 for (i
= 0; i
< common_ctx
->textlen
; i
++) {
657 int y
= common_ctx
->s1
[i
].y
+ ctx
->yoffset
;
659 if (!row_is_in_box(ctx
->box
, y
))
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
))
669 if (!realloc_points(&ctx
->points
, ctx
->len
))
672 ctx
->points
[ctx
->len
].x
= sx
;
673 ctx
->points
[ctx
->len
++].y
= common_ctx
->s1
[i
].y
;
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
;
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
;
699 search_for_pattern(&common_ctx
, &ctx
, get_searched_regex_match
);
704 #endif /* HAVE_REGEX_H */
707 get_searched(struct document_view
*doc_view
, struct point
**pt
, int *pl
)
709 struct search
*s1
, *s2
;
712 assert(doc_view
&& doc_view
->vs
&& pt
&& pl
);
713 if_assert_failed
return;
715 if (!has_search_word(doc_view
))
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
)) {
729 if (get_opt_int("document.browse.search.regex"))
730 get_searched_regex(doc_view
, pt
, pl
, l
, s1
, s2
);
733 get_searched_plain(doc_view
, pt
, pl
, l
, s1
, s2
);
736 /* Highlighting of searched strings. */
738 draw_searched(struct terminal
*term
, struct document_view
*doc_view
)
740 struct point
*pt
= NULL
;
743 assert(term
&& doc_view
);
744 if_assert_failed
return;
746 if (!has_search_word(doc_view
))
749 get_searched(doc_view
, &pt
, &len
);
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. */
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);
769 draw_char_color(term
, x
, y
, color
);
779 FIND_ERROR_NO_PREVIOUS_SEARCH
,
781 FIND_ERROR_HIT_BOTTOM
,
782 FIND_ERROR_NOT_FOUND
,
787 static enum find_error
find_next_do(struct session
*ses
,
788 struct document_view
*doc_view
,
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
,
797 struct document_view
*doc_view
;
798 enum find_error error
;
801 if_assert_failed
return FIND_ERROR_NOT_FOUND
;
803 doc_view
= current_frame(ses
);
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);
824 print_find_error(ses
, error
);
830 search_for_back(struct session
*ses
, unsigned char *str
)
833 if_assert_failed
return;
835 search_for_do(ses
, str
, -1, 1);
839 search_for(struct session
*ses
, unsigned char *str
)
842 if_assert_failed
return;
844 search_for_do(ses
, str
, 1, 1);
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))
855 static char hash
[HASH_SIZE
];
856 static int first_time
= 1;
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
++) {
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;
881 for (i
= 0; i
< l1
; i
++) hash
[HASH(p1
[i
])] = 0;
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) {
898 find_link_page_up(doc_view
);
900 find_link_page_down(doc_view
);
902 if (doc_view
->vs
->current_link
== -1) return 1;
906 while (doc_view
->vs
->current_link
!= -1
907 && next_link_in_view(doc_view
, doc_view
->vs
->current_link
+ direction
,
909 struct point
*pt
= NULL
;
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
)) {
924 find_link_page_up(doc_view
);
926 find_link_page_down(doc_view
);
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;
938 assert(ses
&& ses
->tab
&& ses
->tab
->term
&& doc_view
&& doc_view
->vs
940 if_assert_failed
return FIND_ERROR_NONE
;
942 direction
*= ses
->search_direction
;
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
;
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
);
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
;
972 doc_view
->vs
->x
= int_min(int_max(doc_view
->vs
->x
,
973 max
- doc_view
->box
.width
),
977 find_next_link_in_search(doc_view
, direction
* 2);
980 return FIND_ERROR_HIT_TOP
;
983 return FIND_ERROR_HIT_BOTTOM
;
985 return FIND_ERROR_NONE
;
988 if (p
> doc_view
->document
->height
) {
995 while (p
< doc_view
->document
->height
) p
+= height
;
999 } while (c
< doc_view
->document
->height
+ height
);
1001 return FIND_ERROR_NOT_FOUND
;
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")) {
1010 info_box(ses
->tab
->term
, MSGBOX_FREE_TEXT
,
1011 title
, ALIGN_CENTER
,
1012 msg_text(ses
->tab
->term
, message
,
1017 beep_terminal(ses
->tab
->term
);
1025 print_find_error(struct session
*ses
, enum find_error find_error
)
1028 unsigned char *message
= NULL
;
1030 switch (find_error
) {
1031 case FIND_ERROR_HIT_TOP
:
1033 case FIND_ERROR_HIT_BOTTOM
:
1034 if (!get_opt_bool("document.browse.search"
1035 ".show_hit_top_bottom"))
1039 ? N_("Search hit top, continuing at bottom.")
1040 : N_("Search hit bottom, continuing at top.");
1042 case FIND_ERROR_NO_PREVIOUS_SEARCH
:
1043 message
= N_("No previous search");
1045 case FIND_ERROR_NOT_FOUND
:
1046 print_find_error_not_found(ses
, N_("Search"),
1053 case FIND_ERROR_REGEX
:
1054 print_find_error_not_found(ses
, N_("Search"),
1055 N_("Could not compile"
1056 " regular expression"
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
:
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
{
1088 TYPEAHEAD_ERROR_NO_FURTHER
,
1093 typeahead_error(struct session
*ses
, unsigned char *typeahead
, int no_further
)
1095 unsigned char *message
;
1098 message
= N_("No further matches for '%s'.");
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
;
1118 match_link_text(struct link
*link
, unsigned char *text
, int textlen
,
1121 unsigned char *match
= get_link_typeahead_text(link
);
1122 unsigned char *matchpos
;
1124 if (link_is_form(link
) || textlen
> strlen(match
))
1127 matchpos
= case_sensitive
? strstr(match
, text
)
1128 : strcasestr(match
, text
);
1131 return matchpos
- match
;
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). */
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
;
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
,
1166 if (match_offset
>= 0) {
1167 *offset
= match_offset
;
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;
1184 if (i
== lower_link
+ 1) {
1185 upper_link
= document
->nlinks
;
1186 lower_link
= current_link
- 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. */
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;
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;
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
;
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
]) {
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
:
1265 if (!get_opt_bool("document.browse.search.wraparound")) {
1266 search_hit_boundary
:
1267 if (match_link_text(&document
->links
[current
],
1269 get_opt_bool("document"
1274 return TYPEAHEAD_ERROR_NO_FURTHER
;
1277 return TYPEAHEAD_ERROR
;
1280 i
= doc_view
->document
->nlinks
- 1;
1283 case ACT_EDIT_NEXT_ITEM
:
1287 if (i
< doc_view
->document
->nlinks
) break;
1288 if (!get_opt_bool("document.browse.search.wraparound"))
1289 goto search_hit_boundary
;
1294 case ACT_EDIT_ENTER
:
1295 goto_current_link(ses
, doc_view
, 0);
1296 return TYPEAHEAD_CANCEL
;
1302 match
= search_link_text(document
, current
, i
, text
, direction
, offset
);
1304 if (match
== current
&& i
!= current
)
1305 return TYPEAHEAD_ERROR_NO_FURTHER
;
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
;
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
:
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);
1354 case ACT_EDIT_NEXT_ITEM
:
1355 find_next(ses
, doc_view
, 1);
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)
1365 option_changed(ses
, opt
, opt
);
1370 error
= search_for_do(ses
, buffer
, direction
, 0);
1372 if (error
== FIND_ERROR_REGEX
)
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
);
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 */
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
],
1419 get_opt_bool("document.browse"
1423 draw_typeahead_match(ses
->tab
->term
, doc_view
,
1427 return INPUT_LINE_PROCEED
;
1430 /* Hack time .. should we change mode? */
1432 enum main_action action_id
= ACT_MAIN_NONE
;
1436 action_id
= ACT_MAIN_SEARCH_TYPEAHEAD_LINK
;
1440 action_id
= ACT_MAIN_SEARCH_TYPEAHEAD_TEXT_BACK
;
1444 action_id
= ACT_MAIN_SEARCH_TYPEAHEAD_TEXT
;
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
;
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
:
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
= "/";
1496 case ACT_MAIN_SEARCH_TYPEAHEAD_TEXT_BACK
:
1497 prompt
= data
= "?";
1500 case ACT_MAIN_SEARCH_TYPEAHEAD_LINK
:
1502 /* Falling forward .. good punk rock */
1503 case ACT_MAIN_SEARCH_TYPEAHEAD
:
1505 if (doc_view
->document
->nlinks
) {
1506 handler
= link_typeahead_handler
;
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
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
{
1536 static struct option_resolver resolvers
[] = {
1537 { SEARCH_OPT_REGEX
, "regex" },
1538 { SEARCH_OPT_CASE
, "case" },
1541 struct search_dlg_hop
{
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
;
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. */
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)] */
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
));
1596 checkout_option_values(resolvers
, get_opt_rec(config_options
,
1597 "document.browse.search"),
1598 hop
->values
, SEARCH_OPTIONS
);
1601 #define SEARCH_WIDGETS_COUNT 8
1602 dlg
= calloc_dialog(SEARCH_WIDGETS_COUNT
, MAX_STR_LEN
);
1608 dlg
->title
= _(title
, term
);
1609 dlg
->layouter
= generic_dialog_layouter
;
1610 dlg
->layout
.fit_datalen
= 1;
1611 dlg
->layout
.float_groups
= 1;
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
;
1643 if_assert_failed
return FRAME_EVENT_OK
;
1645 if (direction
> 0) {
1646 title
= N_("Search");
1647 search_function
= search_for
;
1649 title
= N_("Search backward");
1650 search_function
= search_for_back
;
1653 search_dlg_do(ses
->tab
->term
, NULL
,
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
,
1675 init_search_history(struct module
*module
)
1677 load_input_history(&search_history
, SEARCH_HISTORY_FILENAME
);
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
,
1693 /* init: */ init_search_history
,
1694 /* done: */ done_search_history