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