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