]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/finder.jsm
Import 1.0 supporting Firefox up to 14.*
[dactyl.git] / common / modules / finder.jsm
1 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 /* use strict */
6
7 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("finder", {
9     exports: ["RangeFind", "RangeFinder", "rangefinder"],
10     require: ["prefs"]
11 }, this);
12
13 this.lazyRequire("buffer", ["Buffer"]);
14 this.lazyRequire("overlay", ["overlay"]);
15
16 function equals(a, b) XPCNativeWrapper(a) == XPCNativeWrapper(b);
17
18 /** @instance rangefinder */
19 var RangeFinder = Module("rangefinder", {
20     Local: function (dactyl, modules, window) ({
21         init: function () {
22             this.dactyl = dactyl;
23             this.modules = modules;
24             this.window = window;
25             this.lastFindPattern = "";
26         },
27
28         get content() {
29             let { window } = this.modes.getStack(0).params;
30             return window || this.window.content;
31         },
32
33         get rangeFind() {
34             let find = overlay.getData(this.content.document,
35                                        "range-find", null);
36
37             if (!isinstance(find, RangeFind) || find.stale)
38                 return this.rangeFind = null;
39             return find;
40         },
41         set rangeFind(val) overlay.setData(this.content.document,
42                                            "range-find", val)
43     }),
44
45     init: function init() {
46         prefs.safeSet("accessibility.typeaheadfind.autostart", false);
47         // The above should be sufficient, but: http://bugzil.la/348187
48         prefs.safeSet("accessibility.typeaheadfind", false);
49     },
50
51     cleanup: function cleanup() {
52         for (let doc in util.iterDocuments()) {
53             let find = overlay.getData(doc, "range-find", null);
54             if (find)
55                 find.highlight(true);
56
57             overlay.setData(doc, "range-find", null);
58         }
59     },
60
61     get commandline() this.modules.commandline,
62     get modes() this.modules.modes,
63     get options() this.modules.options,
64
65     openPrompt: function openPrompt(mode) {
66         this.modules.marks.push();
67         this.commandline;
68         this.CommandMode(mode, this.content).open();
69
70         Buffer(this.content).resetCaret();
71
72         if (this.rangeFind && equals(this.rangeFind.window.get(), this.window))
73             this.rangeFind.reset();
74         this.find("", mode == this.modes.FIND_BACKWARD);
75     },
76
77     bootstrap: function bootstrap(str, backward) {
78         if (arguments.length < 2 && this.rangeFind)
79             backward = this.rangeFind.reverse;
80
81         let highlighted = this.rangeFind && this.rangeFind.highlighted;
82         let selections = this.rangeFind && this.rangeFind.selections;
83         let linksOnly = false;
84         let regexp = false;
85         let matchCase = this.options["findcase"] === "smart"  ? /[A-Z]/.test(str) :
86                         this.options["findcase"] === "ignore" ? false : true;
87
88         function replacer(m, n1) {
89             if (n1 == "c")
90                 matchCase = false;
91             else if (n1 == "C")
92                 matchCase = true;
93             else if (n1 == "l")
94                 linksOnly = true;
95             else if (n1 == "L")
96                 linksOnly = false;
97             else if (n1 == "r")
98                 regexp = true;
99             else if (n1 == "R")
100                 regexp = false;
101             else
102                 return m;
103             return "";
104         }
105
106         this.options["findflags"].forEach(function (f) replacer(f, f));
107
108         let pattern = str.replace(/\\(.|$)/g, replacer);
109
110         if (str)
111             this.lastFindPattern = str;
112         // It's possible, with :tabdetach for instance, for the rangeFind to
113         // actually move from one window to another, which breaks things.
114         if (!this.rangeFind
115             || !equals(this.rangeFind.window.get(), this.window)
116             || linksOnly  != !!this.rangeFind.elementPath
117             || regexp     != this.rangeFind.regexp
118             || matchCase  != this.rangeFind.matchCase
119             || !!backward != this.rangeFind.reverse) {
120
121             if (this.rangeFind)
122                 this.rangeFind.cancel();
123             this.rangeFind = null;
124             this.rangeFind = RangeFind(this.window, this.content, matchCase, backward,
125                                        linksOnly && this.options.get("hinttags").matcher,
126                                        regexp);
127             this.rangeFind.highlighted = highlighted;
128             this.rangeFind.selections = selections;
129         }
130         this.rangeFind.pattern = str;
131         return pattern;
132     },
133
134     find: function find(pattern, backwards) {
135         this.modules.marks.push();
136         let str = this.bootstrap(pattern, backwards);
137         this.backward = this.rangeFind.reverse;
138
139         if (!this.rangeFind.find(str))
140             this.dactyl.echoerr(_("finder.notFound", pattern),
141                                 this.commandline.FORCE_SINGLELINE);
142
143         return this.rangeFind.found;
144     },
145
146     findAgain: function findAgain(reverse) {
147         this.modules.marks.push();
148         if (!this.rangeFind)
149             this.find(this.lastFindPattern);
150         else if (!this.rangeFind.find(null, reverse))
151             this.dactyl.echoerr(_("finder.notFound", this.lastFindPattern),
152                                 this.commandline.FORCE_SINGLELINE);
153         else if (this.rangeFind.wrapped) {
154             let msg = this.rangeFind.backward ? _("finder.atTop")
155                                               : _("finder.atBottom");
156             this.commandline.echo(msg, "WarningMsg", this.commandline.APPEND_TO_MESSAGES
157                                                    | this.commandline.FORCE_SINGLELINE);
158         }
159         else
160             this.commandline.echo((this.rangeFind.backward ? "?" : "/") + this.rangeFind.pattern,
161                                   "Normal", this.commandline.FORCE_SINGLELINE);
162
163         if (this.options["hlfind"])
164             this.highlight();
165         this.rangeFind.focus();
166     },
167
168     onCancel: function onCancel() {
169         if (this.rangeFind)
170             this.rangeFind.cancel();
171     },
172
173     onChange: function onChange(command) {
174         if (this.options["incfind"]) {
175             command = this.bootstrap(command);
176             this.rangeFind.find(command);
177         }
178     },
179
180     onHistory: function onHistory() {
181         this.rangeFind.found = false;
182     },
183
184     onSubmit: function onSubmit(command) {
185         if (!command && this.lastFindPattern) {
186             this.find(this.lastFindPattern, this.backward);
187             this.findAgain();
188             return;
189         }
190
191         if (!this.options["incfind"] || !this.rangeFind || !this.rangeFind.found) {
192             this.clear();
193             this.find(command || this.lastFindPattern, this.backward);
194         }
195
196         if (this.options["hlfind"])
197             this.highlight();
198         this.rangeFind.focus();
199     },
200
201     /**
202      * Highlights all occurrences of the last sought for string in the
203      * current buffer.
204      */
205     highlight: function highlight() {
206         if (this.rangeFind)
207             this.rangeFind.highlight();
208     },
209
210     /**
211      * Clears all find highlighting.
212      */
213     clear: function clear() {
214         if (this.rangeFind)
215             this.rangeFind.highlight(true);
216     }
217 }, {
218 }, {
219     modes: function initModes(dactyl, modules, window) {
220         initModes.require("commandline");
221
222         const { modes } = modules;
223
224         modes.addMode("FIND", {
225             description: "Find mode, active when typing search input",
226             bases: [modes.COMMAND_LINE]
227         });
228         modes.addMode("FIND_FORWARD", {
229             description: "Forward Find mode, active when typing search input",
230             bases: [modes.FIND]
231         });
232         modes.addMode("FIND_BACKWARD", {
233             description: "Backward Find mode, active when typing search input",
234             bases: [modes.FIND]
235         });
236     },
237     commands: function initCommands(dactyl, modules, window) {
238         const { commands, rangefinder } = modules;
239         commands.add(["noh[lfind]"],
240             "Remove the find highlighting",
241             function () { rangefinder.clear(); },
242             { argCount: "0" });
243     },
244     commandline: function initCommandline(dactyl, modules, window) {
245         const { rangefinder } = modules;
246         rangefinder.CommandMode = Class("CommandFindMode", modules.CommandMode, {
247             init: function init(mode, window) {
248                 this.mode = mode;
249                 this.window = window;
250                 init.supercall(this);
251             },
252
253             historyKey: "find",
254
255             get prompt() this.mode === modules.modes.FIND_BACKWARD ? "?" : "/",
256
257             get onCancel() modules.rangefinder.closure.onCancel,
258             get onChange() modules.rangefinder.closure.onChange,
259             get onHistory() modules.rangefinder.closure.onHistory,
260             get onSubmit() modules.rangefinder.closure.onSubmit
261         });
262     },
263     mappings: function (dactyl, modules, window) {
264         const { Buffer, buffer, config, mappings, modes, rangefinder } = modules;
265         var myModes = config.browserModes.concat([modes.CARET]);
266
267         mappings.add(myModes,
268             ["/", "<find-forward>"], "Find a pattern starting at the current caret position",
269             function () { rangefinder.openPrompt(modes.FIND_FORWARD); });
270
271         mappings.add(myModes,
272             ["?", "<find-backward>", "<S-Slash>"], "Find a pattern backward of the current caret position",
273             function () { rangefinder.openPrompt(modes.FIND_BACKWARD); });
274
275         mappings.add(myModes,
276             ["n", "<find-next>"], "Find next",
277             function () { rangefinder.findAgain(false); });
278
279         mappings.add(myModes,
280             ["N", "<find-previous>"], "Find previous",
281             function () { rangefinder.findAgain(true); });
282
283         mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["*", "<find-word-forward>"],
284             "Find word under cursor",
285             function () {
286                 rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), false);
287                 rangefinder.findAgain();
288             });
289
290         mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["#", "<find-word-backward>"],
291             "Find word under cursor backwards",
292             function () {
293                 rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), true);
294                 rangefinder.findAgain();
295             });
296
297     },
298     options: function (dactyl, modules, window) {
299         const { options, rangefinder } = modules;
300
301         options.add(["hlfind", "hlf"],
302             "Highlight all /find pattern matches on the current page after submission",
303             "boolean", false, {
304                 setter: function (value) {
305                     rangefinder[value ? "highlight" : "clear"]();
306                     return value;
307                 }
308             });
309
310         options.add(["findcase", "fc"],
311             "Find case matching mode",
312             "string", "smart",
313             {
314                 values: {
315                     "smart": "Case is significant when capital letters are typed",
316                     "match": "Case is always significant",
317                     "ignore": "Case is never significant"
318                 }
319             });
320
321         options.add(["findflags", "ff"],
322             "Default flags for find invocations",
323             "charlist", "",
324             {
325                 values: {
326                     "c": "Ignore case",
327                     "C": "Match case",
328                     "r": "Perform a regular expression search",
329                     "R": "Perform a plain string search",
330                     "l": "Search only in links",
331                     "L": "Search all text"
332                 }
333             });
334
335         options.add(["incfind", "if"],
336             "Find a pattern incrementally as it is typed rather than awaiting c_<Return>",
337             "boolean", true);
338     }
339 });
340
341 /**
342  * @class RangeFind
343  *
344  * A fairly sophisticated typeahead-find replacement. It supports
345  * incremental find very much as the builtin component.
346  * Additionally, it supports several features impossible to
347  * implement using the standard component. Incremental finding
348  * works both forwards and backwards. Erasing characters during an
349  * incremental find moves the selection back to the first
350  * available match for the shorter term. The selection and viewport
351  * are restored when the find is canceled.
352  *
353  * Also, in addition to full support for frames and iframes, this
354  * implementation will begin finding from the position of the
355  * caret in the last active frame. This is contrary to the behavior
356  * of the builtin component, which always starts a find from the
357  * beginning of the first frame in the case of frameset documents,
358  * and cycles through all frames from beginning to end. This makes it
359  * impossible to choose the starting point of a find for such
360  * documents, and represents a major detriment to productivity where
361  * large amounts of data are concerned (e.g., for API documents).
362  */
363 var RangeFind = Class("RangeFind", {
364     init: function init(window, content, matchCase, backward, elementPath, regexp) {
365         this.window = util.weakReference(window);
366         this.content = content;
367
368         this.baseDocument = util.weakReference(this.content.document);
369         this.elementPath = elementPath || null;
370         this.reverse = Boolean(backward);
371
372         this.finder = services.Find();
373         this.matchCase = Boolean(matchCase);
374         this.regexp = Boolean(regexp);
375
376         this.reset();
377
378         this.highlighted = null;
379         this.selections = [];
380         this.lastString = "";
381     },
382
383     get store() overlay.getData(this.content.document, "buffer", Object),
384
385     get backward() this.finder.findBackwards,
386     set backward(val) this.finder.findBackwards = val,
387
388     get matchCase() this.finder.caseSensitive,
389     set matchCase(val) this.finder.caseSensitive = Boolean(val),
390
391     get findString() this.lastString,
392
393     get flags() this.matchCase ? "" : "i",
394
395     get selectedRange() {
396         let win = this.store.focusedFrame && this.store.focusedFrame.get() || this.content;
397
398         let selection = win.getSelection();
399         return (selection.rangeCount ? selection.getRangeAt(0) : this.ranges[0].range).cloneRange();
400     },
401     set selectedRange(range) {
402         this.range.selection.removeAllRanges();
403         this.range.selection.addRange(range);
404         this.range.selectionController.scrollSelectionIntoView(
405             this.range.selectionController.SELECTION_NORMAL, 0, false);
406
407         this.store.focusedFrame = util.weakReference(range.startContainer.ownerDocument.defaultView);
408     },
409
410     cancel: function cancel() {
411         this.purgeListeners();
412         if (this.range) {
413             this.range.deselect();
414             this.range.descroll();
415         }
416     },
417
418     compareRanges: function compareRanges(r1, r2) {
419         try {
420             return this.backward ?  r1.compareBoundaryPoints(r1.END_TO_START, r2)
421                                  : -r1.compareBoundaryPoints(r1.START_TO_END, r2);
422         }
423         catch (e) {
424             util.reportError(e);
425             return 0;
426         }
427     },
428
429     findRange: function findRange(range) {
430         let doc = range.startContainer.ownerDocument;
431         let win = doc.defaultView;
432         let ranges = this.ranges.filter(function (r)
433             r.window === win && RangeFind.sameDocument(r.range, range) && RangeFind.contains(r.range, range));
434
435         if (this.backward)
436             return ranges[ranges.length - 1];
437         return ranges[0];
438     },
439
440     findSubRanges: function findSubRanges(range) {
441         let doc = range.startContainer.ownerDocument;
442         for (let elem in this.elementPath(doc)) {
443             let r = RangeFind.nodeRange(elem);
444             if (RangeFind.contains(range, r))
445                 yield r;
446         }
447     },
448
449     focus: function focus() {
450         if (this.lastRange)
451             var node = DOM.XPath(RangeFind.selectNodePath,
452                                  this.lastRange.commonAncestorContainer).snapshotItem(0);
453         if (node) {
454             node.focus();
455             // Re-highlight collapsed selection
456             this.selectedRange = this.lastRange;
457         }
458     },
459
460     highlight: function highlight(clear) {
461         if (!clear && (!this.lastString || this.lastString == this.highlighted))
462             return;
463         if (clear && !this.highlighted)
464             return;
465
466         if (!clear && this.highlighted)
467             this.highlight(true);
468
469         if (clear) {
470             this.selections.forEach(function (selection) {
471                 selection.removeAllRanges();
472             });
473             this.selections = [];
474             this.highlighted = null;
475         }
476         else {
477             this.selections = [];
478             let string = this.lastString;
479             for (let r in this.iter(string)) {
480                 let controller = this.range.selectionController;
481                 for (let node = r.startContainer; node; node = node.parentNode)
482                     if (node instanceof Ci.nsIDOMNSEditableElement) {
483                         controller = node.editor.selectionController;
484                         break;
485                     }
486
487                 let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
488                 sel.addRange(r);
489                 if (this.selections.indexOf(sel) < 0)
490                     this.selections.push(sel);
491             }
492             this.highlighted = this.lastString;
493             if (this.lastRange)
494                 this.selectedRange = this.lastRange;
495             this.addListeners();
496         }
497     },
498
499     indexIter: function indexIter(private_) {
500         let idx = this.range.index;
501         if (this.backward)
502             var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)];
503         else
504             var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)];
505
506         for (let i in groups[0])
507             yield i;
508
509         if (!private_) {
510             this.wrapped = true;
511             this.lastRange = null;
512             for (let i in groups[1])
513                 yield i;
514         }
515     },
516
517     iter: function iter(word) {
518         let saved = ["lastRange", "lastString", "range", "regexp"].map(function (s) [s, this[s]], this);
519         let res;
520         try {
521             let regexp = this.regexp && word != util.regexp.escape(word);
522             this.lastRange = null;
523             this.regexp = false;
524             if (regexp) {
525                 let re = RegExp(word, "gm" + this.flags);
526                 for (this.range in array.iterValues(this.ranges)) {
527                     for (let match in util.regexp.iterate(re, DOM.stringify(this.range.range, true))) {
528                         let lastRange = this.lastRange;
529                         if (res = this.find(null, this.reverse, true))
530                             yield res;
531                         else
532                             this.lastRange = lastRange;
533                     }
534                 }
535             }
536             else {
537                 this.range = this.ranges[0];
538                 this.lastString = word;
539                 while (res = this.find(null, this.reverse, true))
540                     yield res;
541             }
542         }
543         finally {
544             saved.forEach(function ([k, v]) this[k] = v, this);
545         }
546     },
547
548     makeFrameList: function makeFrameList(win) {
549         const self = this;
550         win = win.top;
551         let frames = [];
552         let backup = null;
553
554         function pushRange(start, end) {
555             function push(r) {
556                 if (r = RangeFind.Range(r, frames.length))
557                     frames.push(r);
558             }
559
560             let doc = start.startContainer.ownerDocument;
561
562             let range = doc.createRange();
563             range.setStart(start.startContainer, start.startOffset);
564             range.setEnd(end.startContainer, end.startOffset);
565
566             if (!self.elementPath)
567                 push(range);
568             else
569                 for (let r in self.findSubRanges(range))
570                     push(r);
571         }
572         function rec(win) {
573             let doc = win.document;
574             let pageRange = RangeFind[doc.body ? "nodeRange" : "nodeContents"](doc.body || doc.documentElement);
575             backup = backup || pageRange;
576             let pageStart = RangeFind.endpoint(pageRange, true);
577             let pageEnd = RangeFind.endpoint(pageRange, false);
578
579             for (let frame in array.iterValues(win.frames)) {
580                 let range = doc.createRange();
581                 if (DOM(frame.frameElement).style.visibility == "visible") {
582                     range.selectNode(frame.frameElement);
583                     pushRange(pageStart, RangeFind.endpoint(range, true));
584                     pageStart = RangeFind.endpoint(range, false);
585                     rec(frame);
586                 }
587             }
588             pushRange(pageStart, pageEnd);
589
590             let anonNodes = doc.getAnonymousNodes(doc.documentElement);
591             if (anonNodes) {
592                 for (let [, elem] in iter(anonNodes)) {
593                     let range = RangeFind.nodeContents(elem);
594                     pushRange(RangeFind.endpoint(range, true), RangeFind.endpoint(range, false));
595                 }
596             }
597         }
598         rec(win);
599         if (frames.length == 0)
600             frames[0] = RangeFind.Range(RangeFind.endpoint(backup, true), 0);
601         return frames;
602     },
603
604     reset: function reset() {
605         this.ranges = this.makeFrameList(this.content);
606
607         this.startRange = this.selectedRange;
608         this.startRange.collapse(!this.reverse);
609         this.lastRange = this.selectedRange;
610         this.range = this.findRange(this.startRange) || this.ranges[0];
611         util.assert(this.range, "Null range", false);
612         this.ranges.first = this.range;
613         this.ranges.forEach(function (range) range.save());
614         this.forward = null;
615         this.found = false;
616     },
617
618     find: function find(pattern, reverse, private_) {
619         if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange))
620             this.reset();
621
622         this.wrapped = false;
623         this.backward = reverse ? !this.reverse : this.reverse;
624         let again = pattern == null;
625         if (again)
626             pattern = this.lastString;
627         if (!this.matchCase)
628             pattern = pattern.toLowerCase();
629
630         if (!again && (pattern === "" || pattern.indexOf(this.lastString) !== 0 || this.backward)) {
631             if (!private_)
632                 this.range.deselect();
633             if (pattern === "")
634                 this.range.descroll();
635             this.lastRange = this.startRange;
636             this.range = this.ranges.first;
637         }
638
639         let word = pattern;
640         let regexp = this.regexp && word != util.regexp.escape(word);
641
642         if (regexp)
643             try {
644                 RegExp(pattern);
645             }
646             catch (e) {
647                 pattern = "";
648             }
649
650         if (pattern == "")
651             var range = this.startRange;
652         else
653             for (let i in this.indexIter(private_)) {
654                 if (!private_ && this.range.window != this.ranges[i].window && this.range.window != this.ranges[i].window.parent) {
655                     this.range.descroll();
656                     this.range.deselect();
657                 }
658                 this.range = this.ranges[i];
659
660                 let start = RangeFind.sameDocument(this.lastRange, this.range.range) && this.range.intersects(this.lastRange) ?
661                                 RangeFind.endpoint(this.lastRange, !(again ^ this.backward)) :
662                                 RangeFind.endpoint(this.range.range, !this.backward);
663
664                 if (this.backward && !again)
665                     start = RangeFind.endpoint(this.startRange, false);
666
667                 if (regexp) {
668                     let range = this.range.range.cloneRange();
669                     range[this.backward ? "setEnd" : "setStart"](
670                         start.startContainer, start.startOffset);
671                     range = DOM.stringify(range);
672
673                     if (!this.backward)
674                         var match = RegExp(pattern, "m" + this.flags).exec(range);
675                     else {
676                         match = RegExp("[^]*(?:" + pattern + ")", "m" + this.flags).exec(range);
677                         if (match)
678                             match = RegExp(pattern + "$", this.flags).exec(match[0]);
679                     }
680                     if (!(match && match[0]))
681                         continue;
682                     word = match[0];
683                 }
684
685                 var range = this.finder.Find(word, this.range.range, start, this.range.range);
686                 if (range && DOM(range.commonAncestorContainer).isVisible)
687                     break;
688             }
689
690         if (range)
691             this.lastRange = range.cloneRange();
692         if (!private_) {
693             this.lastString = pattern;
694             if (range == null) {
695                 this.cancel();
696                 this.found = false;
697                 return null;
698             }
699             this.found = true;
700         }
701         if (range && (!private_ || private_ < 0))
702             this.selectedRange = range;
703         return range;
704     },
705
706     get stale() this._stale || this.baseDocument.get() != this.content.document,
707     set stale(val) this._stale = val,
708
709     addListeners: function addListeners() {
710         for (let range in array.iterValues(this.ranges))
711             range.window.addEventListener("unload", this.closure.onUnload, true);
712     },
713     purgeListeners: function purgeListeners() {
714         for (let range in array.iterValues(this.ranges))
715             try {
716                 range.window.removeEventListener("unload", this.closure.onUnload, true);
717             }
718             catch (e if e.result === Cr.NS_ERROR_FAILURE) {}
719     },
720     onUnload: function onUnload(event) {
721         this.purgeListeners();
722         if (this.highlighted)
723             this.highlight(true);
724         this.stale = true;
725     }
726 }, {
727     Range: Class("RangeFind.Range", {
728         init: function init(range, index) {
729             this.index = index;
730
731             this.range = range;
732             this.document = range.startContainer.ownerDocument;
733             this.window = this.document.defaultView;
734
735             if (this.selection == null)
736                 return false;
737
738             this.save();
739         },
740
741         docShell: Class.Memoize(function () util.docShell(this.window)),
742
743         intersects: function (range) RangeFind.intersects(this.range, range),
744
745         save: function save() {
746             this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset);
747
748             this.initialSelection = null;
749             if (this.selection.rangeCount)
750                 this.initialSelection = this.selection.getRangeAt(0);
751         },
752
753         descroll: function descroll() {
754             this.window.scrollTo(this.scroll.x, this.scroll.y);
755         },
756
757         deselect: function deselect() {
758             if (this.selection) {
759                 this.selection.removeAllRanges();
760                 if (this.initialSelection)
761                     this.selection.addRange(this.initialSelection);
762             }
763         },
764
765         get selectionController() this.docShell
766                     .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
767                     .QueryInterface(Ci.nsISelectionController),
768         get selection() {
769             try {
770                 return this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
771             }
772             catch (e) {
773                 return null;
774             }
775         }
776     }),
777     contains: function contains(range, r, quiet) {
778         try {
779             return range.compareBoundaryPoints(range.START_TO_END, r) >= 0 &&
780                    range.compareBoundaryPoints(range.END_TO_START, r) <= 0;
781         }
782         catch (e) {
783             if (e.result != Cr.NS_ERROR_DOM_WRONG_DOCUMENT_ERR && !quiet)
784                 util.reportError(e, true);
785             return false;
786         }
787     },
788     containsNode: function containsNode(range, n, quiet) n.ownerDocument && this.contains(range, RangeFind.nodeRange(n), quiet),
789     intersects: function intersects(range, r) {
790         try {
791             return r.compareBoundaryPoints(range.START_TO_END, range) >= 0 &&
792                    r.compareBoundaryPoints(range.END_TO_START, range) <= 0;
793         }
794         catch (e) {
795             util.reportError(e, true);
796             return false;
797         }
798     },
799     endpoint: function endpoint(range, before) {
800         range = range.cloneRange();
801         range.collapse(before);
802         return range;
803     },
804     equal: function equal(r1, r2) {
805         try {
806             return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2);
807         }
808         catch (e) {
809             return false;
810         }
811     },
812     nodeContents: function nodeContents(node) {
813         let range = node.ownerDocument.createRange();
814         try {
815             range.selectNodeContents(node);
816         }
817         catch (e) {}
818         return range;
819     },
820     nodeRange: function nodeRange(node) {
821         let range = node.ownerDocument.createRange();
822         try {
823             range.selectNode(node);
824         }
825         catch (e) {}
826         return range;
827     },
828     sameDocument: function sameDocument(r1, r2) {
829         if (!(r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument))
830             return false;
831         try {
832             r1.compareBoundaryPoints(r1.START_TO_START, r2);
833         }
834         catch (e if e.result == 0x80530004 /* NS_ERROR_DOM_WRONG_DOCUMENT_ERR */) {
835             return false;
836         }
837         return true;
838     },
839     selectNodePath: ["a", "xhtml:a", "*[@onclick]"].map(function (p) "ancestor-or-self::" + p).join(" | "),
840     union: function union(a, b) {
841         let start = a.compareBoundaryPoints(a.START_TO_START, b) < 0 ? a : b;
842         let end   = a.compareBoundaryPoints(a.END_TO_END, b) > 0 ? a : b;
843         let res   = start.cloneRange();
844         res.setEnd(end.endContainer, end.endOffset);
845         return res;
846     }
847 });
848
849 // catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
850
851 endModule();
852
853 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: