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