1 // Copyright (c) 2008-2012 Kris Maglione <maglione.k@gmail.com>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 defineModule("finder", {
8 exports: ["RangeFind", "RangeFinder", "rangefinder"],
9 require: ["prefs", "util"]
12 lazyRequire("buffer", ["Buffer"]);
13 lazyRequire("overlay", ["overlay"]);
15 function id(w) w.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
17 function equals(a, b) id(a) == id(b);
19 /** @instance rangefinder */
20 var RangeFinder = Module("rangefinder", {
21 Local: function (dactyl, modules, window) ({
24 this.modules = modules;
26 this.lastFindPattern = "";
30 let { window } = this.modes.getStack(0).params;
31 return window || this.window.content;
35 let find = overlay.getData(this.content.document,
38 if (!isinstance(find, RangeFind) || find.stale)
39 return this.rangeFind = null;
42 set rangeFind(val) overlay.setData(this.content.document,
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);
52 cleanup: function cleanup() {
53 for (let doc in util.iterDocuments()) {
54 let find = overlay.getData(doc, "range-find", null);
58 overlay.setData(doc, "range-find", null);
62 get commandline() this.modules.commandline,
63 get modes() this.modules.modes,
64 get options() this.modules.options,
66 openPrompt: function openPrompt(mode) {
67 this.modules.marks.push();
69 this.CommandMode(mode, this.content).open();
71 Buffer(this.content).resetCaret();
73 if (this.rangeFind && equals(this.rangeFind.window.get(), this.window))
74 this.rangeFind.reset();
75 this.find("", mode == this.modes.FIND_BACKWARD);
78 bootstrap: function bootstrap(str, backward) {
79 if (arguments.length < 2 && this.rangeFind)
80 backward = this.rangeFind.reverse;
82 let highlighted = this.rangeFind && this.rangeFind.highlighted;
83 let selections = this.rangeFind && this.rangeFind.selections;
84 let linksOnly = false;
86 let matchCase = this.options["findcase"] === "smart" ? /[A-Z]/.test(str) :
87 this.options["findcase"] === "ignore" ? false : true;
89 function replacer(m, n1) {
107 this.options["findflags"].forEach(function (f) replacer(f, f));
109 let pattern = str.replace(/\\(.|$)/g, replacer);
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.
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) {
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,
128 this.rangeFind.highlighted = highlighted;
129 this.rangeFind.selections = selections;
131 this.rangeFind.pattern = str;
135 find: function find(pattern, backwards) {
136 this.modules.marks.push();
137 let str = this.bootstrap(pattern, backwards);
138 this.backward = this.rangeFind.reverse;
140 if (!this.rangeFind.find(str))
141 this.dactyl.echoerr(_("finder.notFound", pattern),
142 this.commandline.FORCE_SINGLELINE);
144 return this.rangeFind.found;
147 findAgain: function findAgain(reverse) {
148 this.modules.marks.push();
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);
161 this.commandline.echo((this.rangeFind.backward ? "?" : "/") + this.rangeFind.pattern,
162 "Normal", this.commandline.FORCE_SINGLELINE);
164 if (this.options["hlfind"])
166 this.rangeFind.focus();
169 onCancel: function onCancel() {
171 this.rangeFind.cancel();
174 onChange: function onChange(command) {
175 if (this.options["incfind"]) {
176 command = this.bootstrap(command);
177 this.rangeFind.find(command);
181 onHistory: function onHistory() {
182 this.rangeFind.found = false;
185 onSubmit: function onSubmit(command) {
186 if (!command && this.lastFindPattern) {
187 this.find(this.lastFindPattern, this.backward);
192 if (!this.options["incfind"] || !this.rangeFind || !this.rangeFind.found) {
194 this.find(command || this.lastFindPattern, this.backward);
197 if (this.options["hlfind"])
199 this.rangeFind.focus();
203 * Highlights all occurrences of the last sought for string in the
206 highlight: function highlight() {
208 this.rangeFind.highlight();
212 * Clears all find highlighting.
214 clear: function clear() {
216 this.rangeFind.highlight(true);
220 modes: function initModes(dactyl, modules, window) {
221 initModes.require("commandline");
223 const { modes } = modules;
225 modes.addMode("FIND", {
226 description: "Find mode, active when typing search input",
227 bases: [modes.COMMAND_LINE]
229 modes.addMode("FIND_FORWARD", {
230 description: "Forward Find mode, active when typing search input",
233 modes.addMode("FIND_BACKWARD", {
234 description: "Backward Find mode, active when typing search input",
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(); },
245 commandline: function initCommandline(dactyl, modules, window) {
246 const { rangefinder } = modules;
247 rangefinder.CommandMode = Class("CommandFindMode", modules.CommandMode, {
248 init: function init(mode, window) {
250 this.window = window;
251 init.supercall(this);
256 get prompt() this.mode === modules.modes.FIND_BACKWARD ? "?" : "/",
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
264 mappings: function initMappings(dactyl, modules, window) {
265 const { Buffer, buffer, config, mappings, modes, rangefinder } = modules;
266 var myModes = config.browserModes.concat([modes.CARET]);
268 mappings.add(myModes,
269 ["/", "<find-forward>"], "Find a pattern starting at the current caret position",
270 function () { rangefinder.openPrompt(modes.FIND_FORWARD); });
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); });
276 mappings.add(myModes,
277 ["n", "<find-next>"], "Find next",
278 function () { rangefinder.findAgain(false); });
280 mappings.add(myModes,
281 ["N", "<find-previous>"], "Find previous",
282 function () { rangefinder.findAgain(true); });
284 mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["*", "<find-word-forward>"],
285 "Find word under cursor",
287 rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), false);
288 rangefinder.findAgain();
291 mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["#", "<find-word-backward>"],
292 "Find word under cursor backwards",
294 rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), true);
295 rangefinder.findAgain();
299 options: function initOptions(dactyl, modules, window) {
300 const { options, rangefinder } = modules;
302 options.add(["hlfind", "hlf"],
303 "Highlight all /find pattern matches on the current page after submission",
305 setter: function (value) {
306 rangefinder[value ? "highlight" : "clear"]();
311 options.add(["findcase", "fc"],
312 "Find case matching mode",
316 "smart": "Case is significant when capital letters are typed",
317 "match": "Case is always significant",
318 "ignore": "Case is never significant"
322 options.add(["findflags", "ff"],
323 "Default flags for find invocations",
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"
336 options.add(["incfind", "if"],
337 "Find a pattern incrementally as it is typed rather than awaiting c_<Return>",
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.
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).
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;
369 this.baseDocument = util.weakReference(this.content.document);
370 this.elementPath = elementPath || null;
371 this.reverse = Boolean(backward);
373 this.finder = services.Find();
374 this.matchCase = Boolean(matchCase);
375 this.regexp = Boolean(regexp);
379 this.highlighted = null;
380 this.selections = [];
381 this.lastString = "";
384 get store() overlay.getData(this.content.document, "buffer", Object),
386 get backward() this.finder.findBackwards,
387 set backward(val) this.finder.findBackwards = val,
389 get matchCase() this.finder.caseSensitive,
390 set matchCase(val) this.finder.caseSensitive = Boolean(val),
392 get findString() this.lastString,
394 get flags() this.matchCase ? "" : "i",
396 get selectedRange() {
397 let win = this.store.focusedFrame && this.store.focusedFrame.get() || this.content;
399 let selection = win.getSelection();
400 return (selection.rangeCount ? selection.getRangeAt(0) : this.ranges[0].range).cloneRange();
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);
408 this.store.focusedFrame = util.weakReference(range.startContainer.ownerDocument.defaultView);
411 cancel: function cancel() {
412 this.purgeListeners();
414 this.range.deselect();
415 this.range.descroll();
419 compareRanges: function compareRanges(r1, r2) {
421 return this.backward ? r1.compareBoundaryPoints(r1.END_TO_START, r2)
422 : -r1.compareBoundaryPoints(r1.START_TO_END, r2);
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));
437 return ranges[ranges.length - 1];
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))
450 focus: function focus() {
452 var node = DOM.XPath(RangeFind.selectNodePath,
453 this.lastRange.commonAncestorContainer).snapshotItem(0);
456 // Re-highlight collapsed selection
457 this.selectedRange = this.lastRange;
461 highlight: function highlight(clear) {
462 if (!clear && (!this.lastString || this.lastString == this.highlighted))
464 if (clear && !this.highlighted)
467 if (!clear && this.highlighted)
468 this.highlight(true);
471 this.selections.forEach(function (selection) {
472 selection.removeAllRanges();
474 this.selections = [];
475 this.highlighted = null;
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;
488 let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
490 if (this.selections.indexOf(sel) < 0)
491 this.selections.push(sel);
493 this.highlighted = this.lastString;
495 this.selectedRange = this.lastRange;
500 indexIter: function indexIter(private_) {
501 let idx = this.range.index;
503 var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)];
505 var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)];
507 for (let i in groups[0])
512 this.lastRange = null;
513 for (let i in groups[1])
518 iter: function iter(word) {
519 let saved = ["lastRange", "lastString", "range", "regexp"].map(function (s) [s, this[s]], this);
522 let regexp = this.regexp && word != util.regexp.escape(word);
523 this.lastRange = null;
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))
533 this.lastRange = lastRange;
538 this.range = this.ranges[0];
539 this.lastString = word;
540 while (res = this.find(null, this.reverse, true))
545 saved.forEach(function ([k, v]) this[k] = v, this);
549 makeFrameList: function makeFrameList(win) {
555 function pushRange(start, end) {
557 if (r = RangeFind.Range(r, frames.length))
561 let doc = start.startContainer.ownerDocument;
563 let range = doc.createRange();
564 range.setStart(start.startContainer, start.startOffset);
565 range.setEnd(end.startContainer, end.startOffset);
567 if (!self.elementPath)
570 for (let r in self.findSubRanges(range))
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);
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);
589 pushRange(pageStart, pageEnd);
591 let anonNodes = doc.getAnonymousNodes(doc.documentElement);
593 for (let [, elem] in iter(anonNodes)) {
594 let range = RangeFind.nodeContents(elem);
595 pushRange(RangeFind.endpoint(range, true), RangeFind.endpoint(range, false));
600 if (frames.length == 0)
601 frames[0] = RangeFind.Range(RangeFind.endpoint(backup, true), 0);
605 reset: function reset() {
606 this.ranges = this.makeFrameList(this.content);
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());
619 find: function find(pattern, reverse, private_) {
620 if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange))
623 this.wrapped = false;
624 this.backward = reverse ? !this.reverse : this.reverse;
625 let again = pattern == null;
627 pattern = this.lastString;
629 pattern = pattern.toLowerCase();
631 if (!again && (pattern === "" || pattern.indexOf(this.lastString) !== 0 || this.backward)) {
633 this.range.deselect();
635 this.range.descroll();
636 this.lastRange = this.startRange;
637 this.range = this.ranges.first;
641 let regexp = this.regexp && word != util.regexp.escape(word);
652 var range = this.startRange;
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();
659 this.range = this.ranges[i];
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);
665 if (this.backward && !again)
666 start = RangeFind.endpoint(this.startRange, false);
669 let range = this.range.range.cloneRange();
670 range[this.backward ? "setEnd" : "setStart"](
671 start.startContainer, start.startOffset);
672 range = DOM.stringify(range);
675 var match = RegExp(pattern, "m" + this.flags).exec(range);
677 match = RegExp("[^]*(?:" + pattern + ")", "m" + this.flags).exec(range);
679 match = RegExp(pattern + "$", this.flags).exec(match[0]);
681 if (!(match && match[0]))
686 var range = this.finder.Find(word, this.range.range, start, this.range.range);
687 if (range && DOM(range.commonAncestorContainer).isVisible)
692 this.lastRange = range.cloneRange();
694 this.lastString = pattern;
702 if (range && (!private_ || private_ < 0))
703 this.selectedRange = range;
707 get stale() this._stale || this.baseDocument.get() != this.content.document,
708 set stale(val) this._stale = val,
710 addListeners: function addListeners() {
711 for (let range in array.iterValues(this.ranges))
712 range.window.addEventListener("unload", this.closure.onUnload, true);
714 purgeListeners: function purgeListeners() {
715 for (let range in array.iterValues(this.ranges))
717 range.window.removeEventListener("unload", this.closure.onUnload, true);
719 catch (e if e.result === Cr.NS_ERROR_FAILURE) {}
721 onUnload: function onUnload(event) {
722 this.purgeListeners();
723 if (this.highlighted)
724 this.highlight(true);
728 Range: Class("RangeFind.Range", {
729 init: function init(range, index) {
733 this.document = range.startContainer.ownerDocument;
734 this.window = this.document.defaultView;
736 if (this.selection == null)
742 docShell: Class.Memoize(function () util.docShell(this.window)),
744 intersects: function (range) RangeFind.intersects(this.range, range),
746 save: function save() {
747 this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset);
749 this.initialSelection = null;
750 if (this.selection.rangeCount)
751 this.initialSelection = this.selection.getRangeAt(0);
754 descroll: function descroll() {
755 this.window.scrollTo(this.scroll.x, this.scroll.y);
758 deselect: function deselect() {
759 if (this.selection) {
760 this.selection.removeAllRanges();
761 if (this.initialSelection)
762 this.selection.addRange(this.initialSelection);
766 get selectionController() this.docShell
767 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
768 .QueryInterface(Ci.nsISelectionController),
771 return this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
778 contains: function contains(range, r, quiet) {
780 return range.compareBoundaryPoints(range.START_TO_END, r) >= 0 &&
781 range.compareBoundaryPoints(range.END_TO_START, r) <= 0;
784 if (e.result != Cr.NS_ERROR_DOM_WRONG_DOCUMENT_ERR && !quiet)
785 util.reportError(e, true);
789 containsNode: function containsNode(range, n, quiet) n.ownerDocument && this.contains(range, RangeFind.nodeRange(n), quiet),
790 intersects: function intersects(range, r) {
792 return r.compareBoundaryPoints(range.START_TO_END, range) >= 0 &&
793 r.compareBoundaryPoints(range.END_TO_START, range) <= 0;
796 util.reportError(e, true);
800 endpoint: function endpoint(range, before) {
801 range = range.cloneRange();
802 range.collapse(before);
805 equal: function equal(r1, r2) {
807 return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2);
813 nodeContents: function nodeContents(node) {
814 let range = node.ownerDocument.createRange();
816 range.selectNodeContents(node);
821 nodeRange: function nodeRange(node) {
822 let range = node.ownerDocument.createRange();
824 range.selectNode(node);
829 sameDocument: function sameDocument(r1, r2) {
830 if (!(r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument))
833 r1.compareBoundaryPoints(r1.START_TO_START, r2);
835 catch (e if e.result == 0x80530004 /* NS_ERROR_DOM_WRONG_DOCUMENT_ERR */) {
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);
850 // catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
854 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: