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