keyboard.js: properly assign parent keymap for auto-generated prefix keymaps
[conkeror.git] / modules / find.js
blob7f97875ff0f46a40fb126afb0c8425351ef31814
1 /**
2  * (C) Copyright 2004-2005 Shawn Betts
3  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4  * (C) Copyright 2008 Nelson Elhage
5  * (C) Copyright 2008 John Foerch
6  *
7  * Use, modification, and distribution are subject to the terms specified in the
8  * COPYING file.
9 **/
11 const CARET_PREF = 'accessibility.browsewithcaret';
12 const CARET_ATTRIBUTE = 'showcaret';
14 define_variable("isearch_keep_selection", false,
15                 "Set to `true' to make isearch leave the selection "+
16                 "visible when a search is completed.");
18 function caret_enabled(buffer)
20     return buffer.browser.getAttribute(CARET_ATTRIBUTE);
23 // turn on the selection in all frames
24 function getFocusedSelCtrl(buffer)
26     var ds = buffer.doc_shell;
27     var dsEnum = ds.getDocShellEnumerator(Components.interfaces.nsIDocShellTreeItem.typeContent,
28                                           Components.interfaces.nsIDocShell.ENUMERATE_FORWARDS);
29     while (dsEnum.hasMoreElements()) {
30         ds = dsEnum.getNext().QueryInterface(Components.interfaces.nsIDocShell);
31         if (ds.hasFocus)
32         {
33             var display = ds.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
34                 .getInterface(Components.interfaces.nsISelectionDisplay);
35             if (!display)
36                 return null;
37             return display.QueryInterface(Components.interfaces.nsISelectionController);
38         }
39     }
41   // One last try
42   return ds
43       .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
44       .getInterface(Components.interfaces.nsISelectionDisplay)
45       .QueryInterface(Components.interfaces.nsISelectionController);
48 function clear_selection(buffer) {
49     let sel_ctrl = getFocusedSelCtrl(buffer);
50     if (sel_ctrl) {
51         let sel = sel_ctrl.getSelection(sel_ctrl.SELECTION_NORMAL);
52         if(caret_enabled(buffer)) {
53             if(sel.anchorNode) {
54                 sel.collapseToStart();
55             }
56         } else {
57             sel.removeAllRanges();
58         }
59     }
63 function initial_isearch_state(buffer, frame, forward)
65     this.screenx = frame.scrollX;
66     this.screeny = frame.scrollY;
67     this.search_str = "";
68     this.wrapped = false;
69     let sel = frame.getSelection(Components.interfaces.nsISelectionController.SELECTION_NORMAL);
70     if(sel.rangeCount > 0) {
71         this.point = sel.getRangeAt(0);
72         if (caret_enabled(buffer))
73             this.caret = this.point.cloneRange();
74     } else {
75         this.point = null;
76     }
77     this.range = frame.document.createRange();
78     this.selection = null;
79     this.direction = forward;
82 function isearch_session(window, forward)
84     this.states = [];
85     this.buffer = window.buffers.current;
86     this.frame = this.buffer.focused_frame;
87     this.sel_ctrl = getFocusedSelCtrl(this.buffer);
88     this.sel_ctrl.setDisplaySelection(Components.interfaces.nsISelectionController.SELECTION_ATTENTION);
89     this.sel_ctrl.repaintSelection(Components.interfaces.nsISelectionController.SELECTION_NORMAL);
90     this.states.push(new initial_isearch_state(this.buffer, this.frame, forward));
91     this.window = window;
93     minibuffer_input_state.call(this, isearch_keymap, "");
95 isearch_session.prototype = {
96     constructor : isearch_session,
97     __proto__ : minibuffer_input_state.prototype, // inherit from minibuffer_state
99     get top () {
100         return this.states[this.states.length - 1];
101     },
102     _set_selection : function (range)
103     {
104         try {
105             const selctrlcomp = Components.interfaces.nsISelectionController;
106             var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
107             sel.removeAllRanges();
108             sel.addRange(range.cloneRange());
109             this.sel_ctrl.scrollSelectionIntoView(selctrlcomp.SELECTION_NORMAL,
110                                                   selctrlcomp.SELECTION_FOCUS_REGION,
111                                                   true);
112         } catch(e) {/*FIXME:figure out if/why this is needed*/ dumpln("setSelection: " + e);}
113     },
114     _clear_selection : function ()
115     {
116         const selctrlcomp = Components.interfaces.nsISelectionController;
117         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
118         sel.removeAllRanges();
119     },
120     restore_state: function () {
121         var m = this.window.minibuffer;
122         var s = this.top;
123         m._input_text = s.search_str;
124         if (s.selection)
125             this._set_selection(s.selection);
126         else
127             this._clear_selection();
128         this.frame.scrollTo(s.screenx, s.screeny);
129         m.prompt = ((s.wrapped ? "Wrapped ":"")
130                     + (s.range ? "" : "Failing ")
131                     + "I-Search" + (s.direction? "": " backward") + ":");
132     },
133     _highlight_find : function (str, wrapped, dir, pt) {
134         try {
135             var doc = this.frame.document;
136             var finder = (Components.classes["@mozilla.org/embedcomp/rangefind;1"].createInstance()
137                           .QueryInterface(Components.interfaces.nsIFind));
138             var searchRange;
139             var startPt;
140             var endPt;
141             var body = doc.documentElement;
143             finder.findBackwards = !dir;
145             searchRange = doc.createRange();
146             startPt = doc.createRange();
147             endPt = doc.createRange();
149             var count = body.childNodes.length;
151             // Search range in the doc
152             searchRange.setStart(body,0);
153             searchRange.setEnd(body, count);
155             if (!dir) {
156                 if (pt == null) {
157                     startPt.setStart(body, count);
158                     startPt.setEnd(body, count);
159                 } else {
160                     startPt.setStart(pt.startContainer, pt.startOffset);
161                     startPt.setEnd(pt.startContainer, pt.startOffset);
162                 }
163                 endPt.setStart(body, 0);
164                 endPt.setEnd(body, 0);
165             } else {
166                 if (pt == null) {
167                     startPt.setStart(body, 0);
168                     startPt.setEnd(body, 0);
169                 } else {
170                     startPt.setStart(pt.endContainer, pt.endOffset);
171                     startPt.setEnd(pt.endContainer, pt.endOffset);
172                 }
173                 endPt.setStart(body, count);
174                 endPt.setEnd(body, count);
175             }
176             // search the doc
177             var retRange = null;
178             var selectionRange = null;
181             if (!wrapped) {
182                 do {
183                     retRange = finder.Find(str, searchRange, startPt, endPt);
184                     var keepSearching = false;
185                     if (retRange) {
186                         var sc = retRange.startContainer;
187                         var ec = retRange.endContainer;
188                         var scp = sc.parentNode;
189                         var ecp = ec.parentNode;
190                         var sy1 = abs_point(scp).y;
191                         var ey2 = abs_point(ecp).y + ecp.offsetHeight;
193                         startPt = retRange.startContainer.ownerDocument.createRange();
194                         if (!dir) {
195                             startPt.setStart(retRange.startContainer, retRange.startOffset);
196                             startPt.setEnd(retRange.startContainer, retRange.startOffset);
197                         } else {
198                             startPt.setStart(retRange.endContainer, retRange.endOffset);
199                             startPt.setEnd(retRange.endContainer, retRange.endOffset);
200                         }
201                         // We want to find a match that is completely
202                         // visible, otherwise the view will scroll just a
203                         // bit to fit the selection in completely.
204                         keepSearching = (dir && sy1 < this.frame.scrollY)
205                             || (!dir && ey2 >= this.frame.scrollY + this.frame.innerHeight);
206                     }
207                 } while (retRange && keepSearching);
208             } else {
209                 retRange = finder.Find(str, searchRange, startPt, endPt);
210             }
212             if (retRange) {
213                 this._set_selection(retRange);
214                 selectionRange = retRange.cloneRange();
215             } else {
217             }
219             return selectionRange;
220         } catch(e) { /* FIXME: figure out why this is needed*/ this.window.alert(e); }
221         return null;
222     },
224     find : function (str, dir, pt) {
225         var s = this.top;
227         if (str == null || str.length == 0)
228             return;
230         // Should we wrap this time?
231         var wrapped = s.wrapped;
232         var point = pt;
233         if (s.wrapped == false && s.range == null && s.search_str == str && s.direction == dir) {
234             wrapped = true;
235             point = null;
236         }
238         var match_range = this._highlight_find(str, wrapped, dir, point);
240         var new_state = {
241             screenx: this.frame.scrollX, screeny: this.frame.scrollY,
242             search_str: str, wrapped: wrapped, point: point,
243             range: match_range,
244             selection: match_range ? match_range : s.selection,
245             direction: dir
246         };
247         this.states.push(new_state);
248     },
250     focus_link : function ()
251     {
252         var sel = this.frame.getSelection(Components.interfaces.nsISelectionController.SELECTION_NORMAL);
253         if (!sel)
254             return;
255         var node = sel.focusNode;
256         if (node == null)
257             return;
259         do {
260             if (node.localName == "A") {
261                 if (node.hasAttributes && node.attributes.getNamedItem("href")) {
262                     // if there is a selection, preserve it.  it is up
263                     // to the caller to decide whether or not to keep
264                     // the selection.
265                     var sel = this.frame.getSelection(
266                         Components.interfaces.nsISelectionController.SELECTION_NORMAL);
267                     if(sel.rangeCount > 0) {
268                         var stored_selection = sel.getRangeAt(0).cloneRange();
269                     }
270                     node.focus();
271                     if (stored_selection) {
272                         sel.removeAllRanges();
273                         sel.addRange(stored_selection);
274                     }
275                     return;
276                 }
277             }
278         } while ((node = node.parentNode));
279     },
281     collapse_selection : function() {
282         const selctrlcomp = Components.interfaces.nsISelectionController;
283         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
284         if(sel.rangeCount > 0) {
285             sel.getRangeAt(0).collapse(true);
286         }
287     },
289     handle_input : function (m) {
290         m._set_selection();
291         this.find(m._input_text, this.top.direction, this.top.point);
292         this.restore_state();
293     },
295     done : false,
297     destroy : function () {
298         if (!this.done)  {
299             this.frame.scrollTo(this.states[0].screenx, this.states[0].screeny);
300             if (caret_enabled(this.buffer) && this.states[0].caret) {
301                 this._set_selection(this.states[0].caret);
302             } else {
303                 this._clear_selection();
304             }
305         }
306     }
309 function isearch_continue_noninteractively(window, direction) {
310     var s = new isearch_session(window, direction);
311     if (window.isearch_last_search)
312         s.find(window.isearch_last_search, direction, s.top.point);
313     else
314         throw "No previous isearch";
315     window.minibuffer.push_state(s);
316     s.restore_state();
317     // if (direction && s.top.point !== null)
318     //    isearch_continue (window, direction);
319     isearch_done (window, true);
322 function isearch_continue(window, direction) {
323     var s = window.minibuffer.current_state;
324     // if the minibuffer is not open, this command operates in
325     // non-interactive mode.
326     if (s == null)
327         return isearch_continue_noninteractively(window, direction);
328     if (!(s instanceof isearch_session))
329         throw "Invalid minibuffer state";
330     if (s.states.length == 1 && window.isearch_last_search)
331         s.find(window.isearch_last_search, direction, s.top.point);
332     else
333         s.find(s.top.search_str, direction, s.top.range);
334     return s.restore_state();
336 interactive("isearch-continue-forward", null, function (I) {isearch_continue(I.window, true);});
337 interactive("isearch-continue-backward", null, function (I) {isearch_continue(I.window, false);});
339 function isearch_start (window, direction)
341     var s = new isearch_session(window, direction);
342     window.minibuffer.push_state(s);
343     s.restore_state();
345 interactive("isearch-forward", null, function (I) {isearch_start(I.window, true);});
346 interactive("isearch-backward", null, function (I) {isearch_start(I.window, false);});
348 function isearch_backspace (window)
350     var s = window.minibuffer.current_state;
351     if (!(s instanceof isearch_session))
352         throw "Invalid minibuffer state";
353     if (s.states.length > 1)
354         s.states.pop();
355     s.restore_state();
357 interactive("isearch-backspace", null, function (I) {isearch_backspace(I.window);});
359 function isearch_done (window, keep_selection)
361     var s = window.minibuffer.current_state;
362     if (!(s instanceof isearch_session))
363         throw "Invalid minibuffer state";
364     s.sel_ctrl.setDisplaySelection(Components.interfaces.nsISelectionController.SELECTION_NORMAL);
366     // Prevent focus from being reverted
367     window.minibuffer.saved_focused_element = null;
368     window.minibuffer.saved_focused_window = null;
370     s.done = true;
372     window.minibuffer.pop_state();
373     window.isearch_last_search = s.top.search_str;
374     s.focus_link();
375     if (! isearch_keep_selection && ! keep_selection)
376         s.collapse_selection();
378 interactive("isearch-done", null, function (I) {isearch_done(I.window);});