Support Gecko >= 26 Downloads.jsm interface
[conkeror.git] / modules / isearch.js
blob18eb015dc8affd43dbecc101b7ea1fab97c5eca0
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-2010 John Foerch
6  *
7  * Use, modification, and distribution are subject to the terms specified in the
8  * COPYING file.
9 **/
11 define_variable("isearch_keep_selection", false,
12     "Set to `true' to make isearch leave the selection visible when a "+
13     "search is completed.");
15 define_variable("isearch_scroll_center_vertically", false,
16     "Set to `true' to vertically center the selection when scrolling during " +
17     "an isearch.  Only available with XULRunner >= 12.");
19 function initial_isearch_state (buffer, frame, forward) {
20     this.screenx = frame.scrollX;
21     this.screeny = frame.scrollY;
22     this.search_str = "";
23     this.wrapped = false;
24     let sel = frame.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
25     if (sel.rangeCount > 0) {
26         this.point = sel.getRangeAt(0);
27         if (caret_enabled(buffer))
28             this.caret = this.point.cloneRange();
29     } else {
30         this.point = null;
31     }
32     this.range = frame.document.createRange();
33     this.selection = null;
34     this.direction = forward;
37 function isearch_session (minibuffer, forward) {
38     minibuffer_input_state.call(this, minibuffer, isearch_keymap, "");
39     this.states = [];
40     this.buffer = this.minibuffer.window.buffers.current;
41     this.frame = this.buffer.focused_frame;
42     this.sel_ctrl = this.buffer.focused_selection_controller;
43     this.sel_ctrl.setDisplaySelection(Ci.nsISelectionController.SELECTION_ATTENTION);
44     this.sel_ctrl.repaintSelection(Ci.nsISelectionController.SELECTION_NORMAL);
45     this.states.push(new initial_isearch_state(this.buffer, this.frame, forward));
47 isearch_session.prototype = {
48     constructor: isearch_session,
49     __proto__: minibuffer_input_state.prototype,
51     get top () {
52         return this.states[this.states.length - 1];
53     },
54     _set_selection: function (range) {
55         const selctrlcomp = Ci.nsISelectionController;
56         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
57         sel.removeAllRanges();
58         sel.addRange(range.cloneRange());
59         var xulrunner_version = get_mozilla_version();
60         if (version_compare(xulrunner_version, "2.0") < 0) {
61             var flags = true;
62         } else {
63             flags = selctrlcomp.SCROLL_SYNCHRONOUS;
64             if (isearch_scroll_center_vertically &&
65                 version_compare(xulrunner_version, "12.0") >= 0)
66             {
67                 flags |= selctrlcomp.SCROLL_CENTER_VERTICALLY;
68             }
69         }
70         this.sel_ctrl.scrollSelectionIntoView(selctrlcomp.SELECTION_NORMAL,
71                                               selctrlcomp.SELECTION_FOCUS_REGION,
72                                               flags);
74     },
75     _clear_selection: function () {
76         const selctrlcomp = Ci.nsISelectionController;
77         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
78         sel.removeAllRanges();
79     },
80     restore_state: function () {
81         var m = this.minibuffer;
82         var s = this.top;
83         m.ignore_input_events = true;
84         m._input_text = s.search_str;
85         m.ignore_input_events = false;
86         if (s.selection)
87             this._set_selection(s.selection);
88         else
89             this._clear_selection();
90         this.frame.scrollTo(s.screenx, s.screeny);
91         m.prompt = ((s.wrapped ? "Wrapped ":"")
92                     + (s.range ? "" : "Failing ")
93                     + "I-Search" + (s.direction? "": " backward") + ":");
94     },
95     _highlight_find: function (str, wrapped, dir, pt) {
96         var doc = this.frame.document;
97         var finder = (Cc["@mozilla.org/embedcomp/rangefind;1"]
98                       .createInstance()
99                       .QueryInterface(Ci.nsIFind));
100         var searchRange;
101         var startPt;
102         var endPt;
103         var body = doc.documentElement;
105         finder.findBackwards = !dir;
106         finder.caseSensitive = (str != str.toLowerCase());
108         searchRange = doc.createRange();
109         startPt = doc.createRange();
110         endPt = doc.createRange();
112         var count = body.childNodes.length;
114         // Search range in the doc
115         searchRange.setStart(body,0);
116         searchRange.setEnd(body, count);
118         if (!dir) {
119             if (pt == null) {
120                 startPt.setStart(body, count);
121                 startPt.setEnd(body, count);
122             } else {
123                 startPt.setStart(pt.startContainer, pt.startOffset);
124                 startPt.setEnd(pt.startContainer, pt.startOffset);
125             }
126             endPt.setStart(body, 0);
127             endPt.setEnd(body, 0);
128         } else {
129             if (pt == null) {
130                 startPt.setStart(body, 0);
131                 startPt.setEnd(body, 0);
132             } else {
133                 startPt.setStart(pt.endContainer, pt.endOffset);
134                 startPt.setEnd(pt.endContainer, pt.endOffset);
135             }
136             endPt.setStart(body, count);
137             endPt.setEnd(body, count);
138         }
139         // search the doc
140         var retRange = null;
141         var selectionRange = null;
143         if (!wrapped) {
144             do {
145                 retRange = finder.Find(str, searchRange, startPt, endPt);
146                 var keepSearching = false;
147                 if (retRange) {
148                     var sc = retRange.startContainer;
149                     var ec = retRange.endContainer;
150                     var scp = sc.parentNode;
151                     var ecp = ec.parentNode;
152                     var sy1 = abs_point(scp).y;
153                     var ey2 = abs_point(ecp).y + ecp.offsetHeight;
155                     startPt = retRange.startContainer.ownerDocument.createRange();
156                     if (!dir) {
157                         startPt.setStart(retRange.startContainer, retRange.startOffset);
158                         startPt.setEnd(retRange.startContainer, retRange.startOffset);
159                     } else {
160                         startPt.setStart(retRange.endContainer, retRange.endOffset);
161                         startPt.setEnd(retRange.endContainer, retRange.endOffset);
162                     }
163                     // We want to find a match that is completely
164                     // visible, otherwise the view will scroll just a
165                     // bit to fit the selection in completely.
166                     keepSearching = (dir && sy1 < this.frame.scrollY)
167                         || (!dir && ey2 >= this.frame.scrollY + this.frame.innerHeight);
168                 }
169             } while (retRange && keepSearching);
170         } else {
171             retRange = finder.Find(str, searchRange, startPt, endPt);
172         }
174         if (retRange) {
175             this._set_selection(retRange);
176             selectionRange = retRange.cloneRange();
177         }
179         return selectionRange;
180     },
182     find: function (str, dir, pt) {
183         var s = this.top;
185         if (str == null || str.length == 0)
186             return;
188         // Should we wrap this time?
189         var wrapped = s.wrapped;
190         var point = pt;
191         if (s.wrapped == false && s.range == null
192             && s.search_str == str && s.direction == dir)
193         {
194             wrapped = true;
195             point = null;
196         }
198         var match_range = this._highlight_find(str, wrapped, dir, point);
200         var new_state = {
201             screenx: this.frame.scrollX,
202             screeny: this.frame.scrollY,
203             search_str: str,
204             wrapped: wrapped,
205             point: point,
206             range: match_range,
207             selection: match_range ? match_range : s.selection,
208             direction: dir
209         };
210         this.states.push(new_state);
211     },
213     focus_link: function () {
214         var sel = this.frame.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
215         if (!sel)
216             return;
217         var node = sel.focusNode;
218         if (node == null)
219             return;
220         do {
221             if (node.localName && node.localName.toLowerCase() == "a") {
222                 if (node.hasAttributes && node.attributes.getNamedItem("href")) {
223                     // if there is a selection, preserve it.  it is up
224                     // to the caller to decide whether or not to keep
225                     // the selection.
226                     var sel = this.frame.getSelection(
227                         Ci.nsISelectionController.SELECTION_NORMAL);
228                     if (sel.rangeCount > 0)
229                         var stored_selection = sel.getRangeAt(0).cloneRange();
230                     node.focus();
231                     if (stored_selection) {
232                         sel.removeAllRanges();
233                         sel.addRange(stored_selection);
234                     }
235                     return;
236                 }
237             }
238         } while ((node = node.parentNode));
239     },
241     collapse_selection: function() {
242         const selctrlcomp = Ci.nsISelectionController;
243         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
244         if (sel.rangeCount > 0)
245             sel.getRangeAt(0).collapse(true);
246     },
248     handle_input: function (m) {
249         m._set_selection();
250         this.find(m._input_text, this.top.direction, this.top.point);
251         this.restore_state();
252     },
254     done: false,
256     destroy: function () {
257         if (! this.done) {
258             this.frame.scrollTo(this.states[0].screenx, this.states[0].screeny);
259             if (caret_enabled(this.buffer) && this.states[0].caret)
260                 this._set_selection(this.states[0].caret);
261             else
262                 this._clear_selection();
263         }
264         minibuffer_input_state.prototype.destroy.call(this);
265     }
268 function isearch_continue_noninteractively (window, direction) {
269     var s = new isearch_session(window.minibuffer, direction);
270     if (window.isearch_last_search)
271         s.find(window.isearch_last_search, direction, s.top.point);
272     else
273         throw "No previous isearch";
274     window.minibuffer.push_state(s);
275     s.restore_state();
276     // if (direction && s.top.point !== null)
277     //    isearch_continue (window, direction);
278     isearch_done(window, true);
281 function isearch_continue (window, direction) {
282     var s = window.minibuffer.current_state;
283     // if the minibuffer is not open, this command operates in
284     // non-interactive mode.
285     if (s == null)
286         return isearch_continue_noninteractively(window, direction);
287     if (!(s instanceof isearch_session))
288         throw "Invalid minibuffer state";
289     if (s.states.length == 1 && window.isearch_last_search)
290         s.find(window.isearch_last_search, direction, s.top.point);
291     else
292         s.find(s.top.search_str, direction, s.top.range);
293     return s.restore_state();
296 interactive("isearch-continue",
297     "Continue the last isearch in the same direction.",
298     function (I) {
299         isearch_continue(I.window, I.window.isearch_last_direction || false);
300     });
302 interactive("isearch-continue-reverse",
303     "Continue the last isearch in the opposite direction.",
304     function (I) {
305         isearch_continue(I.window, !(I.window.isearch_last_direction || false));
306     });
308 interactive("isearch-continue-forward",
309     "Continue the last isearch, forward.",
310     function (I) {
311         I.window.isearch_last_direction = true;
312         isearch_continue(I.window, true);
313     });
315 interactive("isearch-continue-backward",
316     "Continue the last isearch, backward.",
317     function (I) {
318         I.window.isearch_last_direction = false;
319         isearch_continue(I.window, false);
320     });
322 function isearch_start (window, direction) {
323     var s = new isearch_session(window.minibuffer, direction);
324     window.isearch_last_direction = direction;
325     window.minibuffer.push_state(s);
326     s.restore_state();
329 interactive("isearch-forward",
330     "Start interactive text search, forward from point.",
331     function (I) { isearch_start(I.window, true); });
333 interactive("isearch-backward",
334     "Start interactive text search, backwards from point.",
335     function (I) { isearch_start(I.window, false); });
337 function isearch_backspace (window) {
338     var s = window.minibuffer.current_state;
339     if (!(s instanceof isearch_session))
340         throw "Invalid minibuffer state";
341     if (s.states.length > 1)
342         s.states.pop();
343     s.restore_state();
345 interactive("isearch-backspace",
346     "Undo last action in interactive search.",
347     function (I) { isearch_backspace(I.window); });
349 function isearch_done (window, keep_selection) {
350     var s = window.minibuffer.current_state;
351     if (!(s instanceof isearch_session))
352         throw "Invalid minibuffer state";
353     s.sel_ctrl.setDisplaySelection(Ci.nsISelectionController.SELECTION_NORMAL);
355     // Prevent focus from being reverted
356     s.buffer.clear_saved_focus();
358     s.done = true;
360     window.minibuffer.pop_state();
361     window.isearch_last_search = s.top.search_str;
362     s.focus_link();
363     if (! isearch_keep_selection && ! keep_selection)
364         s.collapse_selection();
366 interactive("isearch-done",
367     "Complete interactive search.",
368     function (I) { isearch_done(I.window); });
370 provide("isearch");