Debian package: Declare compliance with Debian Policy 4.3.0
[conkeror.git] / modules / hints.js
blobe8cb1e9bb3ca7d61f78219f83abc201c7888d08f
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009-2010 John J. Foerch
4  *
5  * Portions of this file are derived from Vimperator,
6  * (C) Copyright 2006-2007 Martin Stubenschrott.
7  *
8  * Use, modification, and distribution are subject to the terms specified in the
9  * COPYING file.
10 **/
12 define_variable("active_img_hint_background_color", "#88FF00",
13     "Color for the active image hint background.");
15 define_variable("img_hint_background_color", "yellow",
16     "Color for inactive image hint backgrounds.");
18 define_variable("active_hint_background_color", "#88FF00",
19     "Color for the active hint background.");
21 define_variable("hint_background_color", "yellow",
22     "Color for the inactive hint.");
25 define_variable("hint_digits", null,
26     "Null or a string of the digits to use as the counting base "+
27     "for hint numbers, starting with the digit that represents zero "+
28     "and ascending.  If null, base 10 will be used with the normal "+
29     "hindu-arabic numerals.");
31 define_variable("hints_display_alt", true,
32     "Display alt text in hints.");
35 /**
36  * hints_enumerate is a generator of natural numbers in the base defined
37  * by hint_digits.
38  */
39 function hints_enumerate () {
40     var base = hint_digits.length;
41     var n = [1];
42     var p = 1;
43     while (true) {
44         yield n.map(function (x) hint_digits[x]).join("");
45         var i = p-1;
46         n[i]++;
47         while (n[i] >= base && i > 0) {
48             n[i] = 0;
49             n[--i]++;
50         }
51         if (n[0] >= base) {
52             n[0] = 0;
53             n.unshift(1);
54             p++;
55         }
56     }
59 /**
60  * hints_parse converts a string that represents a natural number to an
61  * int.  When hint_digits is non-null, it defines the base for conversion.
62  */
63 function hints_parse (str) {
64     if (hint_digits) {
65         var base = hint_digits.length;
66         var n = 0;
67         for (var i = 0, p = str.length - 1; p >= 0; i++, p--) {
68             n += hint_digits.indexOf(str[i]) * Math.pow(base, p);
69         }
70         return n;
71     } else
72         return parseInt(str);
75 /**
76  * Register hints style sheet
77  */
78 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
79 register_user_stylesheet(hints_stylesheet);
82 function hints_simple_text_match (text, pattern) {
83     var pos = text.indexOf(pattern);
84     if (pos == -1)
85         return false;
86     return [pos, pos + pattern.length];
89 define_variable('hints_text_match', hints_simple_text_match,
90     "A function which takes a string and a pattern (another string) "+
91     "and returns an array of [start, end] indices if the pattern was "+
92     "found in the string, or false if it was not.");
95 /**
96  *   In the hints interaction, a node can be selected either by typing
97  * the number of its associated hint, or by typing substrings of the
98  * text content of the node.  In the case of selecting by text
99  * content, multiple substrings can be given by separating them with
100  * spaces.
101  */
102 function hint_manager (window, xpath_expr, focused_frame, focused_element) {
103     this.window = window;
104     this.hints = [];
105     this.valid_hints = [];
106     this.xpath_expr = xpath_expr;
107     this.focused_frame = focused_frame;
108     this.focused_element = focused_element;
109     this.last_selected_hint = null;
111     // Generate
112     this.generate_hints();
114 hint_manager.prototype = {
115     constructor: hint_manager,
116     current_hint_string: "",
117     current_hint_number: -1,
119     /**
120      * Create an initially hidden hint span element absolutely
121      * positioned over each element that matches
122      * hint_xpath_expression.  This is done recursively for all frames
123      * and iframes.  Information about the resulting hints are also
124      * stored in the hints array.
125      */
126     generate_hints: function () {
127         var topwin = this.window;
128         var top_height = topwin.innerHeight;
129         var top_width = topwin.innerWidth;
130         var hints = this.hints;
131         var xpath_expr = this.xpath_expr;
132         var focused_frame_hint = null, focused_element_hint = null;
133         var focused_frame = this.focused_frame;
134         var focused_element = this.focused_element;
136         function helper (window, offsetX, offsetY) {
137             var win_height = window.height;
138             var win_width = window.width;
140             // Bounds
141             var minX = offsetX < 0 ? -offsetX : 0;
142             var minY = offsetY < 0 ? -offsetY : 0;
143             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
144             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
146             var scrollX = window.scrollX;
147             var scrollY = window.scrollY;
149             var doc = window.document;
150             if (! doc.documentElement)
151                 return;
152             var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
153                                    Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
154                                    null /* existing results */);
156             var base_node = doc.createElementNS(XHTML_NS, "span");
157             base_node.className = "__conkeror_hint";
159             var fragment = doc.createDocumentFragment();
160             var rect, elem, text, node, show_text;
161             for (var j = 0; j < res.snapshotLength; j++) {
162                 elem = res.snapshotItem(j);
163                 rect = elem.getBoundingClientRect();
164                 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
165                     rect = { top: rect.top,
166                              left: rect.left,
167                              bottom: rect.bottom,
168                              right: rect.right };
169                     try {
170                         var coords = elem.getAttribute("coords")
171                             .match(/^\D*(-?\d+)\D+(-?\d+)/);
172                         if (coords.length == 3) {
173                             rect.left += parseInt(coords[1]);
174                             rect.top += parseInt(coords[2]);
175                         }
176                     } catch (e) {}
177                 }
178                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
179                     continue;
180                 let style = topwin.getComputedStyle(elem, "");
181                 if (style.display == "none" || style.visibility == "hidden")
182                     continue;
183                 if (! (elem instanceof Ci.nsIDOMHTMLAreaElement))
184                     rect = elem.getClientRects()[0];
185                 if (!rect)
186                     continue;
187                 var nchildren = elem.childNodes.length;
188                 if (elem instanceof Ci.nsIDOMHTMLAnchorElement &&
189                     rect.width == 0 && rect.height == 0)
190                 {
191                     for (var c = 0; c < nchildren; ++c) {
192                         var cc = elem.childNodes.item(c);
193                         if (cc.getBoundingClientRect) {
194                             rect = cc.getBoundingClientRect();
195                             break;
196                         }
197                     }
198                 }
199                 show_text = false;
200                 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
201                     text = elem.value;
202                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
203                     if (elem.selectedIndex >= 0)
204                         text = elem.item(elem.selectedIndex).text;
205                     else
206                         text = "";
207                 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
208                     text = elem.name ? elem.name : "";
209                 } else if (/^\s*$/.test(elem.textContent) &&
210                            nchildren == 1 &&
211                            elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
212                     text = elem.childNodes.item(0).alt;
213                     show_text = hints_display_alt;
214                 } else
215                     text = elem.textContent;
217                 node = base_node.cloneNode(true);
218                 node.style.left = (rect.left + scrollX) + "px";
219                 node.style.top = (rect.top + scrollY) + "px";
220                 fragment.appendChild(node);
222                 let hint = { text: text,
223                              ltext: text.toLowerCase(),
224                              elem: elem,
225                              hint: node,
226                              img_hint: null,
227                              visible: false,
228                              show_text: show_text };
229                 if (elem.style) {
230                     hint.saved_color = elem.style.color;
231                     hint.saved_bgcolor = elem.style.backgroundColor;
232                 }
233                 hints.push(hint);
235                 if (elem == focused_element)
236                     focused_element_hint = hint;
237                 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
238                           elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
239                          elem.contentWindow == focused_frame)
240                     focused_frame_hint = hint;
241             }
242             doc.documentElement.appendChild(fragment);
244             /* Recurse into any IFRAME or FRAME elements */
245             var frametag = "frame";
246             while (true) {
247                 var frames = doc.getElementsByTagName(frametag);
248                 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
249                     elem = frames[i];
250                     rect = elem.getBoundingClientRect();
251                     if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
252                         continue;
253                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
254                 }
255                 if (frametag == "frame") frametag = "iframe"; else break;
256             }
257         }
258         helper(topwin, 0, 0);
259         this.last_selected_hint = focused_element_hint || focused_frame_hint;
260     },
262     /* Updates valid_hints and also re-numbers and re-displays all hints. */
263     update_valid_hints: function () {
264         this.valid_hints = [];
265         var cur_number = 1;
266         if (hint_digits)
267             var number_generator = hints_enumerate();
268         var active_number = this.current_hint_number;
269         var tokens = this.current_hint_string.split(" ");
270         var case_sensitive = (this.current_hint_string !=
271                               this.current_hint_string.toLowerCase());
272         var rect, text, img_hint, doc, scrollX, scrollY;
273     outer:
274         for (var i = 0, h; (h = this.hints[i]); ++i) {
275             if (case_sensitive)
276                 text = h.text;
277             else
278                 text = h.ltext;
279             for (var j = 0, ntokens = tokens.length; j < ntokens; ++j) {
280                 if (! hints_text_match(text, tokens[j])) {
281                     if (h.visible) {
282                         h.visible = false;
283                         h.hint.style.display = "none";
284                         if (h.img_hint)
285                             h.img_hint.style.display = "none";
286                         if (h.saved_color != null) {
287                             h.elem.style.backgroundColor = h.saved_bgcolor;
288                             h.elem.style.color = h.saved_color;
289                         }
290                     }
291                     continue outer;
292                 }
293             }
295             h.visible = true;
297             if (h == this.last_selected_hint && active_number == -1)
298                 this.current_hint_number = active_number = cur_number;
300             var img_elem = null;
302             if (text == "" && h.elem.firstChild &&
303                 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
304                 img_elem = h.elem.firstChild;
305             else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
306                 img_elem = h.elem;
308             if (img_elem) {
309                 if (!h.img_hint) {
310                     rect = img_elem.getBoundingClientRect();
311                     if (rect) {
312                         doc = h.elem.ownerDocument;
313                         scrollX = doc.defaultView.scrollX;
314                         scrollY = doc.defaultView.scrollY;
315                         img_hint = doc.createElementNS(XHTML_NS, "span");
316                         img_hint.className = "__conkeror_img_hint";
317                         img_hint.style.left = (rect.left + scrollX) + "px";
318                         img_hint.style.top = (rect.top + scrollY) + "px";
319                         img_hint.style.width = (rect.right - rect.left) + "px";
320                         img_hint.style.height = (rect.bottom - rect.top) + "px";
321                         h.img_hint = img_hint;
322                         doc.documentElement.appendChild(img_hint);
323                     } else
324                         img_elem = null;
325                 }
326                 if (img_elem) {
327                     var bgcolor = (active_number == cur_number) ?
328                         active_img_hint_background_color : img_hint_background_color;
329                     h.img_hint.style.backgroundColor = bgcolor;
330                     h.img_hint.style.display = "inline";
331                 }
332             }
334             if (!h.img_hint && h.elem.style)
335                 h.elem.style.backgroundColor = (active_number == cur_number) ?
336                     active_hint_background_color : hint_background_color;
338             if (h.elem.style)
339                 h.elem.style.color = "black";
341             var label = "";
342             if (hint_digits)
343                 label = number_generator.next();
344             else
345                 label += cur_number;
346             if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
347                 label +=  " " + text;
348             } else if (h.show_text && !/^\s*$/.test(text)) {
349                 let substrs = [[0,4]];
350                 for (j = 0; j < ntokens; ++j) {
351                     let m = hints_text_match(text, tokens[j]);
352                     if (m == false) continue;
353                     splice_range(substrs, m[0], m[1] + 2);
354                 }
355                 label += " " + substrs.map(function (x) {
356                     return text.substring(x[0],Math.min(x[1], text.length));
357                 }).join("..") + "..";
358             }
359             h.hint.textContent = label;
360             h.hint.style.display = "inline";
361             this.valid_hints.push(h);
362             cur_number++;
363         }
365         if (active_number == -1)
366             this.select_hint(1);
367     },
369     select_hint: function (index) {
370         var old_index = this.current_hint_number;
371         if (index == old_index)
372             return;
373         var vh = this.valid_hints;
374         var vl = this.valid_hints.length;
375         if (old_index >= 1 && old_index <= vl) {
376             var h = vh[old_index - 1];
377             if (h.img_hint)
378                 h.img_hint.style.backgroundColor = img_hint_background_color;
379             if (h.elem.style)
380                 h.elem.style.backgroundColor = hint_background_color;
381         }
382         this.current_hint_number = index;
383         this.last_selected_hint = null;
384         if (index >= 1 && index <= vl) {
385             h = vh[index - 1];
386             if (h.img_hint)
387                 h.img_hint.style.backgroundColor = active_img_hint_background_color;
388             if (h.elem.style)
389                 h.elem.style.backgroundColor = active_hint_background_color;
390             this.last_selected_hint = h;
391         }
392     },
394     hide_hints: function () {
395         for (var i = 0, h; h = this.hints[i]; ++i) {
396             if (h.visible) {
397                 h.visible = false;
398                 if (h.saved_color != null) {
399                     try {
400                         h.elem.style.color = h.saved_color;
401                         h.elem.style.backgroundColor = h.saved_bgcolor;
402                     } catch (e) { /* element may be dead */ }
403                 }
404                 if (h.img_hint) {
405                     try {
406                         h.img_hint.style.display = "none";
407                     } catch (e) { /* element may be dead */ }
408                 }
409                 try {
410                     h.hint.style.display = "none";
411                 } catch (e) { /* element may be dead */ }
412             }
413         }
414     },
416     remove: function () {
417         for (var i = 0, h; h = this.hints[i]; ++i) {
418             if (h.visible && h.saved_color != null) {
419                 try {
420                     h.elem.style.color = h.saved_color;
421                     h.elem.style.backgroundColor = h.saved_bgcolor;
422                 } catch (e) { /* element may be dead */ }
423             }
424             if (h.img_hint) {
425                 try {
426                     h.img_hint.parentNode.removeChild(h.img_hint);
427                 } catch (e) { /* element may be dead */ }
428             }
429             try {
430                 h.hint.parentNode.removeChild(h.hint);
431             } catch (e) { /* element may be dead */ }
432         }
433         this.hints = [];
434         this.valid_hints = [];
435     }
439  * Display the URL and other information for the currently selected node.
440  */
441 function hints_minibuffer_annotation (hints, window) {
442     this.hints = hints;
443     this.input = window.minibuffer.input_element;
445 hints_minibuffer_annotation.prototype = {
446     constructor: hints_minibuffer_annotation,
448     update: function () {
449         var s = [];
450         if (this.hints.manager && this.hints.manager.last_selected_hint) {
451             var elem = this.hints.manager.last_selected_hint.elem;
452             if (elem.hasAttribute("onmousedown") ||
453                 elem.hasAttribute("onclick"))
454             {
455                 s.push("[script]");
456             }
457             var tag = elem.localName.toLowerCase();
458             if ((tag == "input" || tag == "button") &&
459                 elem.type == "submit" && elem.form && elem.form.action)
460             {
461                 s.push((elem.form.method || "GET").toUpperCase() + ":" +
462                        elem.form.action);
463             } else {
464                 try {
465                     var spec = load_spec(elem);
466                     var uri = load_spec_uri_string(spec);
467                     if (uri)
468                         s.push(uri);
469                 } catch (e) {}
470             }
471         }
472         this.input.annotation = s.join(" ");
473     },
475     load: function () {
476         this.input.annotate = true;
477         this.update();
478     },
480     unload: function () {
481         this.input.annotate = false;
482     }
485 define_global_mode("hints_minibuffer_annotation_mode",
486     function enable () {
487         minibuffer_annotation_mode.register(hints_minibuffer_annotation_mode);
488     },
489     function disable () {
490         minibuffer_annotation_mode.unregister(hints_minibuffer_annotation_mode);
491     },
492     $doc = "Display the URL associated with the currently selected hint in "+
493            "a minibuffer annotation.\nThis mode is most useful when "+
494            "hints_auto_exit_delay is long or disabled.");
496 hints_minibuffer_annotation_mode(true);
499  * keyword arguments:
501  * $prompt
502  * $callback
503  * $abort_callback
504  */
505 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
506 function hints_minibuffer_state (minibuffer, buffer) {
507     keywords(arguments, $keymap = hint_keymap, $auto);
508     basic_minibuffer_state.call(this, minibuffer, $prompt = arguments.$prompt,
509                                 $keymap = arguments.$keymap);
510     if (hints_minibuffer_annotation_mode_enabled)
511         this.hints_minibuffer_annotation = new hints_minibuffer_annotation(this, buffer.window);
512     this.original_prompt = arguments.$prompt;
514     let deferred = Promise.defer();
515     this.deferred = deferred;
516     this.promise = make_simple_cancelable(deferred);
518     this.auto_exit = arguments.$auto ? true : false;
519     this.xpath_expr = arguments.$hint_xpath_expression;
520     this.auto_exit_timer_ID = null;
521     this.multiple = arguments.$multiple;
522     this.focused_element = buffer.focused_element;
523     this.focused_frame = buffer.focused_frame;
525 hints_minibuffer_state.prototype = {
526     constructor: hints_minibuffer_state,
527     __proto__: basic_minibuffer_state.prototype,
528     manager: null,
529     typed_string: "",
530     typed_number: "",
531     load: function () {
532         basic_minibuffer_state.prototype.load.call(this);
533         if (!this.manager) {
534             var buf = this.minibuffer.window.buffers.current;
535             this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
536                                             this.focused_frame, this.focused_element);
537         }
538         this.manager.update_valid_hints();
539         if (this.hints_minibuffer_annotation)
540             this.hints_minibuffer_annotation.load();
541     },
542     clear_auto_exit_timer: function () {
543         var window = this.minibuffer.window;
544         if (this.auto_exit_timer_ID != null) {
545             window.clearTimeout(this.auto_exit_timer_ID);
546             this.auto_exit_timer_ID = null;
547         }
548     },
549     unload: function () {
550         this.clear_auto_exit_timer();
551         this.manager.hide_hints();
552         if (this.hints_minibuffer_annotation)
553             this.hints_minibuffer_annotation.unload();
554         basic_minibuffer_state.prototype.unload.call(this);
555     },
556     destroy: function () {
557         this.promise.cancel();
558         this.clear_auto_exit_timer();
559         this.manager.remove();
560         if (this.hints_minibuffer_annotation)
561             this.hints_minibuffer_annotation.unload();
562         basic_minibuffer_state.prototype.destroy.call(this);
563     },
564     update_minibuffer: function (m) {
565         if (this.typed_number.length > 0)
566             m.prompt = this.original_prompt + " #" + this.typed_number;
567         else
568             m.prompt = this.original_prompt;
569         if (this.hints_minibuffer_annotation)
570             this.hints_minibuffer_annotation.update();
571     },
573     handle_auto_exit: function (ambiguous) {
574         var window = this.minibuffer.window;
575         var num = this.manager.current_hint_number;
576         if (!this.auto_exit)
577             return;
578         let s = this;
579         let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
580         if (delay > 0)
581             this.auto_exit_timer_ID = window.setTimeout(function () { hints_exit(window, s); },
582                                                         delay);
583     },
585     handle_input: function (m) {
586         this.clear_auto_exit_timer();
587         this.typed_number = "";
588         this.typed_string = m._input_text;
589         this.manager.current_hint_string = this.typed_string;
590         this.manager.current_hint_number = -1;
591         this.manager.update_valid_hints();
592         if (this.manager.valid_hints.length == 1)
593             this.handle_auto_exit(false /* unambiguous */);
594         else if (this.manager.valid_hints.length > 1)
595         this.handle_auto_exit(true /* ambiguous */);
596         this.update_minibuffer(m);
597     }
600 define_variable("hints_auto_exit_delay", 0,
601     "Delay (in milliseconds) after the most recent key stroke before a "+
602     "sole matching element is automatically selected.  When zero, "+
603     "automatic selection is disabled.  A value of 500 is a good "+
604     "starting point for an average-speed typist.");
606 define_variable("hints_ambiguous_auto_exit_delay", 0,
607     "Delay (in milliseconds) after the most recent key stroke before the "+
608     "first of an ambiguous match is automatically selected.  If this is "+
609     "set to 0, automatic selection in ambiguous matches is disabled.");
612 define_key_match_predicate("match_hint_digit", "hint digit",
613     function (e) {
614         if (e.type != "keypress")
615             return false;
616         if (e.charCode == 48) //0 is special
617             return true;
618         if (hint_digits) {
619             if (hint_digits.indexOf(String.fromCharCode(e.charCode)) > -1)
620                 return true;
621         } else if (e.charCode >= 49 && e.charCode <= 57)
622             return true;
623         return false;
624     });
626 interactive("hints-handle-number",
627     "This is the handler for numeric keys in hinting mode.  Normally, "+
628     "that means '1' through '9' and '0', but the numeric base (and digits) "+
629     "can be configured via the user variable 'hint_digits'.  No matter "+
630     "what numeric base is in effect, the character '0' is special, and "+
631     "will always be treated as a number 0, translated into the current "+
632     "base if necessary.",
633     function (I) {
634         let s = I.minibuffer.check_state(hints_minibuffer_state);
635         s.clear_auto_exit_timer();
636         var ch = String.fromCharCode(I.event.charCode);
637         if (hint_digits && ch == "0")
638             ch = hint_digits[0];
639         var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
640         s.typed_number += ch;
641         s.manager.select_hint(hints_parse(s.typed_number));
642         var num = s.manager.current_hint_number;
643         if (num > 0 && num <= s.manager.valid_hints.length)
644             auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
645         else if (num == 0) {
646             if (!s.multiple) {
647                 hints_exit(I.window, s);
648                 return;
649             }
650             auto_exit_ambiguous = false;
651         }
652         if (auto_exit_ambiguous !== null)
653             s.handle_auto_exit(auto_exit_ambiguous);
654         s.update_minibuffer(I.minibuffer);
655     });
657 function hints_backspace (window, s) {
658     let m = window.minibuffer;
659     s.clear_auto_exit_timer();
660     var l = s.typed_number.length;
661     if (l > 0) {
662         s.typed_number = s.typed_number.substring(0, --l);
663         var num = l > 0 ? hints_parse(s.typed_number) : 1;
664         s.manager.select_hint(num);
665     } else if (s.typed_string.length > 0) {
666         call_builtin_command(window, 'cmd_deleteCharBackward');
667         s.typed_string = m._input_text;
668         //m._set_selection();
669         s.manager.current_hint_string = s.typed_string;
670         s.manager.current_hint_number = -1;
671         s.manager.update_valid_hints();
672     }
673     s.update_minibuffer(m);
675 interactive("hints-backspace", null,
676     function (I) {
677         hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
678     });
680 function hints_next (window, s, count) {
681     s.clear_auto_exit_timer();
682     s.typed_number = "";
683     var cur = s.manager.current_hint_number - 1;
684     var vh = s.manager.valid_hints;
685     var vl = s.manager.valid_hints.length;
686     if (vl > 0) {
687         cur = (cur + count) % vl;
688         if (cur < 0)
689             cur += vl;
690         s.manager.select_hint(cur + 1);
691     }
692     s.update_minibuffer(window.minibuffer);
694 interactive("hints-next", null,
695     function (I) {
696         hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
697     });
699 interactive("hints-previous", null,
700     function (I) {
701         hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
702     });
704 function hints_exit (window, s) {
705     var cur = s.manager.current_hint_number;
706     var elem = null;
707     if (cur > 0 && cur <= s.manager.valid_hints.length)
708         elem = s.manager.valid_hints[cur - 1].elem;
709     else if (cur == 0)
710         elem = window.buffers.current.top_frame;
711     if (elem !== null) {
712         s.deferred.resolve(elem);
713         window.minibuffer.pop_state();
714     }
717 interactive("hints-exit", null,
718     function (I) {
719         hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
720     });
722 interactive("hints-quote-next", null,
723     function (I) {
724         I.overlay_keymap = hint_quote_next_keymap;
725     },
726     $prefix);
729 define_keywords("$buffer");
730 minibuffer.prototype.read_hinted_element = function () {
731     keywords(arguments);
732     var buf = arguments.$buffer;
733     var s = new hints_minibuffer_state(this, buf, forward_keywords(arguments));
734     this.push_state(s);
735     yield co_return(yield s.promise);
738 provide("hints");