whitespace
[conkeror.git] / modules / isearch.js
blobd50a0bbcbcd68f68792d0a8f477529ae742c0a5e
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 in_module(null);
13 define_variable("isearch_keep_selection", false,
14     "Set to `true' to make isearch leave the selection visible when a "+
15     "search is completed.");
18 function initial_isearch_state (buffer, frame, forward) {
19     this.screenx = frame.scrollX;
20     this.screeny = frame.scrollY;
21     this.search_str = "";
22     this.wrapped = false;
23     let sel = frame.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
24     if (sel.rangeCount > 0) {
25         this.point = sel.getRangeAt(0);
26         if (caret_enabled(buffer))
27             this.caret = this.point.cloneRange();
28     } else {
29         this.point = null;
30     }
31     this.range = frame.document.createRange();
32     this.selection = null;
33     this.direction = forward;
36 function isearch_session (minibuffer, forward) {
37     minibuffer_input_state.call(this, minibuffer, isearch_keymap, "");
38     this.states = [];
39     this.buffer = this.minibuffer.window.buffers.current;
40     this.frame = this.buffer.focused_frame;
41     this.sel_ctrl = this.buffer.focused_selection_controller;
42     this.sel_ctrl.setDisplaySelection(Ci.nsISelectionController.SELECTION_ATTENTION);
43     this.sel_ctrl.repaintSelection(Ci.nsISelectionController.SELECTION_NORMAL);
44     this.states.push(new initial_isearch_state(this.buffer, this.frame, forward));
46 isearch_session.prototype = {
47     constructor: isearch_session,
48     __proto__: minibuffer_input_state.prototype,
50     get top () {
51         return this.states[this.states.length - 1];
52     },
53     _set_selection: function (range) {
54         const selctrlcomp = Ci.nsISelectionController;
55         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
56         sel.removeAllRanges();
57         sel.addRange(range.cloneRange());
58         this.sel_ctrl.scrollSelectionIntoView(selctrlcomp.SELECTION_NORMAL,
59                                               selctrlcomp.SELECTION_FOCUS_REGION,
60                                               true);
61     },
62     _clear_selection: function () {
63         const selctrlcomp = Ci.nsISelectionController;
64         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
65         sel.removeAllRanges();
66     },
67     restore_state: function () {
68         var m = this.minibuffer;
69         var s = this.top;
70         m.ignore_input_events = true;
71         m._input_text = s.search_str;
72         m.ignore_input_events = false;
73         if (s.selection)
74             this._set_selection(s.selection);
75         else
76             this._clear_selection();
77         this.frame.scrollTo(s.screenx, s.screeny);
78         m.prompt = ((s.wrapped ? "Wrapped ":"")
79                     + (s.range ? "" : "Failing ")
80                     + "I-Search" + (s.direction? "": " backward") + ":");
81     },
82     _highlight_find: function (str, wrapped, dir, pt) {
83         var doc = this.frame.document;
84         var finder = (Cc["@mozilla.org/embedcomp/rangefind;1"]
85                       .createInstance()
86                       .QueryInterface(Ci.nsIFind));
87         var searchRange;
88         var startPt;
89         var endPt;
90         var body = doc.documentElement;
92         finder.findBackwards = !dir;
93         finder.caseSensitive = (str != str.toLowerCase());
95         searchRange = doc.createRange();
96         startPt = doc.createRange();
97         endPt = doc.createRange();
99         var count = body.childNodes.length;
101         // Search range in the doc
102         searchRange.setStart(body,0);
103         searchRange.setEnd(body, count);
105         if (!dir) {
106             if (pt == null) {
107                 startPt.setStart(body, count);
108                 startPt.setEnd(body, count);
109             } else {
110                 startPt.setStart(pt.startContainer, pt.startOffset);
111                 startPt.setEnd(pt.startContainer, pt.startOffset);
112             }
113             endPt.setStart(body, 0);
114             endPt.setEnd(body, 0);
115         } else {
116             if (pt == null) {
117                 startPt.setStart(body, 0);
118                 startPt.setEnd(body, 0);
119             } else {
120                 startPt.setStart(pt.endContainer, pt.endOffset);
121                 startPt.setEnd(pt.endContainer, pt.endOffset);
122             }
123             endPt.setStart(body, count);
124             endPt.setEnd(body, count);
125         }
126         // search the doc
127         var retRange = null;
128         var selectionRange = null;
130         if (!wrapped) {
131             do {
132                 retRange = finder.Find(str, searchRange, startPt, endPt);
133                 var keepSearching = false;
134                 if (retRange) {
135                     var sc = retRange.startContainer;
136                     var ec = retRange.endContainer;
137                     var scp = sc.parentNode;
138                     var ecp = ec.parentNode;
139                     var sy1 = abs_point(scp).y;
140                     var ey2 = abs_point(ecp).y + ecp.offsetHeight;
142                     startPt = retRange.startContainer.ownerDocument.createRange();
143                     if (!dir) {
144                         startPt.setStart(retRange.startContainer, retRange.startOffset);
145                         startPt.setEnd(retRange.startContainer, retRange.startOffset);
146                     } else {
147                         startPt.setStart(retRange.endContainer, retRange.endOffset);
148                         startPt.setEnd(retRange.endContainer, retRange.endOffset);
149                     }
150                     // We want to find a match that is completely
151                     // visible, otherwise the view will scroll just a
152                     // bit to fit the selection in completely.
153                     keepSearching = (dir && sy1 < this.frame.scrollY)
154                         || (!dir && ey2 >= this.frame.scrollY + this.frame.innerHeight);
155                 }
156             } while (retRange && keepSearching);
157         } else {
158             retRange = finder.Find(str, searchRange, startPt, endPt);
159         }
161         if (retRange) {
162             this._set_selection(retRange);
163             selectionRange = retRange.cloneRange();
164         }
166         return selectionRange;
167     },
169     find: function (str, dir, pt) {
170         var s = this.top;
172         if (str == null || str.length == 0)
173             return;
175         // Should we wrap this time?
176         var wrapped = s.wrapped;
177         var point = pt;
178         if (s.wrapped == false && s.range == null
179             && s.search_str == str && s.direction == dir)
180         {
181             wrapped = true;
182             point = null;
183         }
185         var match_range = this._highlight_find(str, wrapped, dir, point);
187         var new_state = {
188             screenx: this.frame.scrollX,
189             screeny: this.frame.scrollY,
190             search_str: str,
191             wrapped: wrapped,
192             point: point,
193             range: match_range,
194             selection: match_range ? match_range : s.selection,
195             direction: dir
196         };
197         this.states.push(new_state);
198     },
200     focus_link: function () {
201         var sel = this.frame.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
202         if (!sel)
203             return;
204         var node = sel.focusNode;
205         if (node == null)
206             return;
207         do {
208             if (node.localName && node.localName.toLowerCase() == "a") {
209                 if (node.hasAttributes && node.attributes.getNamedItem("href")) {
210                     // if there is a selection, preserve it.  it is up
211                     // to the caller to decide whether or not to keep
212                     // the selection.
213                     var sel = this.frame.getSelection(
214                         Ci.nsISelectionController.SELECTION_NORMAL);
215                     if (sel.rangeCount > 0)
216                         var stored_selection = sel.getRangeAt(0).cloneRange();
217                     node.focus();
218                     if (stored_selection) {
219                         sel.removeAllRanges();
220                         sel.addRange(stored_selection);
221                     }
222                     return;
223                 }
224             }
225         } while ((node = node.parentNode));
226     },
228     collapse_selection: function() {
229         const selctrlcomp = Ci.nsISelectionController;
230         var sel = this.sel_ctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
231         if (sel.rangeCount > 0)
232             sel.getRangeAt(0).collapse(true);
233     },
235     handle_input: function (m) {
236         m._set_selection();
237         this.find(m._input_text, this.top.direction, this.top.point);
238         this.restore_state();
239     },
241     done: false,
243     destroy: function () {
244         if (! this.done) {
245             this.frame.scrollTo(this.states[0].screenx, this.states[0].screeny);
246             if (caret_enabled(this.buffer) && this.states[0].caret)
247                 this._set_selection(this.states[0].caret);
248             else
249                 this._clear_selection();
250         }
251         minibuffer_input_state.prototype.destroy.call(this);
252     }
255 function isearch_continue_noninteractively (window, direction) {
256     var s = new isearch_session(window.minibuffer, direction);
257     if (window.isearch_last_search)
258         s.find(window.isearch_last_search, direction, s.top.point);
259     else
260         throw "No previous isearch";
261     window.minibuffer.push_state(s);
262     s.restore_state();
263     // if (direction && s.top.point !== null)
264     //    isearch_continue (window, direction);
265     isearch_done(window, true);
268 function isearch_continue (window, direction) {
269     var s = window.minibuffer.current_state;
270     // if the minibuffer is not open, this command operates in
271     // non-interactive mode.
272     if (s == null)
273         return isearch_continue_noninteractively(window, direction);
274     if (!(s instanceof isearch_session))
275         throw "Invalid minibuffer state";
276     if (s.states.length == 1 && window.isearch_last_search)
277         s.find(window.isearch_last_search, direction, s.top.point);
278     else
279         s.find(s.top.search_str, direction, s.top.range);
280     return s.restore_state();
282 interactive("isearch-continue-forward", null,
283             function (I) { isearch_continue(I.window, true); });
284 interactive("isearch-continue-backward", null,
285             function (I) { isearch_continue(I.window, false); });
287 function isearch_start (window, direction) {
288     var s = new isearch_session(window.minibuffer, direction);
289     window.minibuffer.push_state(s);
290     s.restore_state();
292 interactive("isearch-forward", null,
293             function (I) { isearch_start(I.window, true); });
294 interactive("isearch-backward", null,
295             function (I) { isearch_start(I.window, false); });
297 function isearch_backspace (window) {
298     var s = window.minibuffer.current_state;
299     if (!(s instanceof isearch_session))
300         throw "Invalid minibuffer state";
301     if (s.states.length > 1)
302         s.states.pop();
303     s.restore_state();
305 interactive("isearch-backspace", null,
306             function (I) { isearch_backspace(I.window); });
308 function isearch_done (window, keep_selection) {
309     var s = window.minibuffer.current_state;
310     if (!(s instanceof isearch_session))
311         throw "Invalid minibuffer state";
312     s.sel_ctrl.setDisplaySelection(Ci.nsISelectionController.SELECTION_NORMAL);
314     // Prevent focus from being reverted
315     window.minibuffer.saved_focused_element = null;
316     window.minibuffer.saved_focused_window = null;
318     s.done = true;
320     window.minibuffer.pop_state();
321     window.isearch_last_search = s.top.search_str;
322     s.focus_link();
323     if (! isearch_keep_selection && ! keep_selection)
324         s.collapse_selection();
326 interactive("isearch-done", null,
327             function (I) { isearch_done(I.window); });
329 provide("isearch");