1 // Copyright (c) 2008-2011 by 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 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("finder", {
9 exports: ["RangeFind", "RangeFinder", "rangefinder"],
10 use: ["messages", "services", "util"]
13 function equals(a, b) XPCNativeWrapper(a) == XPCNativeWrapper(b);
15 /** @instance rangefinder */
16 var RangeFinder = Module("rangefinder", {
17 Local: function (dactyl, modules, window) ({
20 this.modules = modules;
22 this.lastFindPattern = "";
26 let find = modules.buffer.localStore.rangeFind;
27 if (find && find.stale || !isinstance(find, RangeFind))
28 return this.rangeFind = null;
31 set rangeFind(val) modules.buffer.localStore.rangeFind = val
34 get commandline() this.modules.commandline,
35 get modes() this.modules.modes,
36 get options() this.modules.options,
38 openPrompt: function (mode) {
40 this.CommandMode(mode).open();
42 if (this.rangeFind && equals(this.rangeFind.window.get(), this.window))
43 this.rangeFind.reset();
44 this.find("", mode === this.modes.FIND_BACKWARD);
47 bootstrap: function (str, backward) {
48 let highlighted = this.rangeFind && this.rangeFind.highlighted;
49 let selections = this.rangeFind && this.rangeFind.selections;
50 let linksOnly = false;
52 let matchCase = this.options["findcase"] === "smart" ? /[A-Z]/.test(str) :
53 this.options["findcase"] === "ignore" ? false : true;
55 str = str.replace(/\\(.|$)/g, function (m, n1) {
73 // It's possible, with :tabdetach for instance, for the rangeFind to
74 // actually move from one window to another, which breaks things.
76 || !equals(this.rangeFind.window.get(), this.window)
77 || linksOnly != !!this.rangeFind.elementPath
78 || regexp != this.rangeFind.regexp
79 || matchCase != this.rangeFind.matchCase
80 || !!backward != this.rangeFind.reverse) {
83 this.rangeFind.cancel();
84 this.rangeFind = RangeFind(this.window, matchCase, backward,
85 linksOnly && this.options.get("hinttags").matcher,
87 this.rangeFind.highlighted = highlighted;
88 this.rangeFind.selections = selections;
90 return this.lastFindPattern = str;
93 find: function (pattern, backwards) {
94 let str = this.bootstrap(pattern, backwards);
95 if (!this.rangeFind.find(str))
96 this.dactyl.echoerr(_("finder.notFound", pattern),
97 this.commandline.FORCE_SINGLELINE);
99 return this.rangeFind.found;
102 findAgain: function (reverse) {
104 this.find(this.lastFindPattern);
105 else if (!this.rangeFind.find(null, reverse))
106 this.dactyl.echoerr(_("finder.notFound", this.lastFindPattern),
107 this.commandline.FORCE_SINGLELINE);
108 else if (this.rangeFind.wrapped) {
109 let msg = this.rangeFind.backward ? _("finder.atTop")
110 : _("finder.atBottom");
111 this.commandline.echo(msg, "WarningMsg", this.commandline.APPEND_TO_MESSAGES
112 | this.commandline.FORCE_SINGLELINE);
115 this.commandline.echo((this.rangeFind.backward ? "?" : "/") + this.lastFindPattern,
116 "Normal", this.commandline.FORCE_SINGLELINE);
118 if (this.options["hlfind"])
120 this.rangeFind.focus();
123 onCancel: function () {
125 this.rangeFind.cancel();
128 onChange: function (command) {
129 if (this.options["incfind"]) {
130 command = this.bootstrap(command);
131 this.rangeFind.find(command);
135 onSubmit: function (command) {
136 if (!this.options["incfind"] || !this.rangeFind || !this.rangeFind.found) {
138 this.find(command || this.lastFindPattern, this.modes.extended & this.modes.FIND_BACKWARD);
141 if (this.options["hlfind"])
143 this.rangeFind.focus();
147 * Highlights all occurrences of the last sought for string in the
150 highlight: function () {
152 this.rangeFind.highlight();
156 * Clears all find highlighting.
160 this.rangeFind.highlight(true);
164 modes: function initModes(dactyl, modules, window) {
165 initModes.require("commandline");
167 const { modes } = modules;
169 modes.addMode("FIND", {
170 description: "Find mode, active when typing search input",
171 bases: [modes.COMMAND_LINE],
173 modes.addMode("FIND_FORWARD", {
174 description: "Forward Find mode, active when typing search input",
177 modes.addMode("FIND_BACKWARD", {
178 description: "Backward Find mode, active when typing search input",
182 commands: function initCommands(dactyl, modules, window) {
183 const { commands, rangefinder } = modules;
184 commands.add(["noh[lfind]"],
185 "Remove the find highlighting",
186 function () { rangefinder.clear(); },
189 commandline: function initCommandline(dactyl, modules, window) {
190 const { rangefinder } = modules;
191 rangefinder.CommandMode = Class("CommandFindMode", modules.CommandMode, {
192 init: function init(mode) {
194 init.supercall(this);
199 get prompt() this.mode === modules.modes.FIND_BACKWARD ? "?" : "/",
201 get onCancel() modules.rangefinder.closure.onCancel,
202 get onChange() modules.rangefinder.closure.onChange,
203 get onSubmit() modules.rangefinder.closure.onSubmit
206 mappings: function (dactyl, modules, window) {
207 const { buffer, config, mappings, modes, rangefinder } = modules;
208 var myModes = config.browserModes.concat([modes.CARET]);
210 mappings.add(myModes,
211 ["/"], "Find a pattern starting at the current caret position",
212 function () { rangefinder.openPrompt(modes.FIND_FORWARD); });
214 mappings.add(myModes,
215 ["?"], "Find a pattern backward of the current caret position",
216 function () { rangefinder.openPrompt(modes.FIND_BACKWARD); });
218 mappings.add(myModes,
220 function () { rangefinder.findAgain(false); });
222 mappings.add(myModes,
223 ["N"], "Find previous",
224 function () { rangefinder.findAgain(true); });
226 mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["*"],
227 "Find word under cursor",
229 rangefinder.find(buffer.getCurrentWord(), false);
230 rangefinder.findAgain();
233 mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["#"],
234 "Find word under cursor backwards",
236 rangefinder.find(buffer.getCurrentWord(), true);
237 rangefinder.findAgain();
241 options: function (dactyl, modules, window) {
242 const { options, rangefinder } = modules;
243 const { prefs } = require("prefs");
245 // prefs.safeSet("accessibility.typeaheadfind.autostart", false);
246 // The above should be sufficient, but: https://bugzilla.mozilla.org/show_bug.cgi?id=348187
247 prefs.safeSet("accessibility.typeaheadfind", false);
249 options.add(["hlfind", "hlf"],
250 "Highlight all /find pattern matches on the current page after submission",
252 setter: function (value) {
253 rangefinder[value ? "highlight" : "clear"]();
258 options.add(["findcase", "fc"],
259 "Find case matching mode",
263 "smart": "Case is significant when capital letters are typed",
264 "match": "Case is always significant",
265 "ignore": "Case is never significant"
269 options.add(["incfind", "if"],
270 "Find a pattern incrementally as it is typed rather than awaiting <Return>",
278 * A fairly sophisticated typeahead-find replacement. It supports
279 * incremental find very much as the builtin component.
280 * Additionally, it supports several features impossible to
281 * implement using the standard component. Incremental finding
282 * works both forwards and backwards. Erasing characters during an
283 * incremental find moves the selection back to the first
284 * available match for the shorter term. The selection and viewport
285 * are restored when the find is canceled.
287 * Also, in addition to full support for frames and iframes, this
288 * implementation will begin finding from the position of the
289 * caret in the last active frame. This is contrary to the behavior
290 * of the builtin component, which always starts a find from the
291 * beginning of the first frame in the case of frameset documents,
292 * and cycles through all frames from beginning to end. This makes it
293 * impossible to choose the starting point of a find for such
294 * documents, and represents a major detriment to productivity where
295 * large amounts of data are concerned (e.g., for API documents).
297 var RangeFind = Class("RangeFind", {
298 init: function init(window, matchCase, backward, elementPath, regexp) {
299 this.window = Cu.getWeakReference(window);
300 this.content = window.content;
302 this.baseDocument = Cu.getWeakReference(this.content.document);
303 this.elementPath = elementPath || null;
304 this.reverse = Boolean(backward);
306 this.finder = services.Find();
307 this.matchCase = Boolean(matchCase);
308 this.regexp = Boolean(regexp);
312 this.highlighted = null;
313 this.selections = [];
314 this.lastString = "";
317 get store() this.content.document.dactylStore = this.content.document.dactylStore || {},
319 get backward() this.finder.findBackwards,
321 get matchCase() this.finder.caseSensitive,
322 set matchCase(val) this.finder.caseSensitive = Boolean(val),
324 get regexp() this.finder.regularExpression || false,
327 return this.finder.regularExpression = Boolean(val);
334 get findString() this.lastString,
336 get selectedRange() {
337 let win = this.store.focusedFrame && this.store.focusedFrame.get() || this.content;
339 let selection = win.getSelection();
340 return (selection.rangeCount ? selection.getRangeAt(0) : this.ranges[0].range).cloneRange();
342 set selectedRange(range) {
343 this.range.selection.removeAllRanges();
344 this.range.selection.addRange(range);
345 this.range.selectionController.scrollSelectionIntoView(
346 this.range.selectionController.SELECTION_NORMAL, 0, false);
348 this.store.focusedFrame = Cu.getWeakReference(range.startContainer.ownerDocument.defaultView);
351 cancel: function cancel() {
352 this.purgeListeners();
353 this.range.deselect();
354 this.range.descroll();
357 compareRanges: function compareRanges(r1, r2) {
359 return this.backward ? r1.compareBoundaryPoints(r1.END_TO_START, r2)
360 : -r1.compareBoundaryPoints(r1.START_TO_END, r2);
368 findRange: function findRange(range) {
369 let doc = range.startContainer.ownerDocument;
370 let win = doc.defaultView;
371 let ranges = this.ranges.filter(function (r)
372 r.window === win && RangeFind.contains(r.range, range));
375 return ranges[ranges.length - 1];
379 findSubRanges: function findSubRanges(range) {
380 let doc = range.startContainer.ownerDocument;
381 for (let elem in this.elementPath(doc)) {
382 let r = RangeFind.nodeRange(elem);
383 if (RangeFind.contains(range, r))
388 focus: function focus() {
390 var node = util.evaluateXPath(RangeFind.selectNodePath,
391 this.lastRange.commonAncestorContainer).snapshotItem(0);
394 // Re-highlight collapsed selection
395 this.selectedRange = this.lastRange;
399 highlight: function highlight(clear) {
400 if (!clear && (!this.lastString || this.lastString == this.highlighted))
402 if (clear && !this.highlighted)
405 if (!clear && this.highlighted)
406 this.highlight(true);
409 this.selections.forEach(function (selection) {
410 selection.removeAllRanges();
412 this.selections = [];
413 this.highlighted = null;
416 this.selections = [];
417 let string = this.lastString;
418 for (let r in this.iter(string)) {
419 let controller = this.range.selectionController;
420 for (let node = r.startContainer; node; node = node.parentNode)
421 if (node instanceof Ci.nsIDOMNSEditableElement) {
422 controller = node.editor.selectionController;
426 let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
428 if (this.selections.indexOf(sel) < 0)
429 this.selections.push(sel);
431 this.highlighted = this.lastString;
433 this.selectedRange = this.lastRange;
438 indexIter: function (private_) {
439 let idx = this.range.index;
441 var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)];
443 var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)];
445 for (let i in groups[0])
450 this.lastRange = null;
451 for (let i in groups[1])
456 iter: function (word) {
457 let saved = ["lastRange", "lastString", "range"].map(function (s) [s, this[s]], this);
459 this.range = this.ranges[0];
460 this.lastRange = null;
461 this.lastString = word;
463 while (res = this.find(null, this.reverse, true))
467 saved.forEach(function ([k, v]) this[k] = v, this);
471 makeFrameList: function (win) {
477 function pushRange(start, end) {
479 if (r = RangeFind.Range(r, frames.length))
483 let range = start.startContainer.ownerDocument.createRange();
484 range.setStart(start.startContainer, start.startOffset);
485 range.setEnd(end.startContainer, end.startOffset);
487 if (!self.elementPath)
490 for (let r in self.findSubRanges(range))
494 let doc = win.document;
495 let pageRange = RangeFind.nodeRange(doc.body || doc.documentElement.lastChild);
496 backup = backup || pageRange;
497 let pageStart = RangeFind.endpoint(pageRange, true);
498 let pageEnd = RangeFind.endpoint(pageRange, false);
500 for (let frame in array.iterValues(win.frames)) {
501 let range = doc.createRange();
502 if (util.computedStyle(frame.frameElement).visibility == "visible") {
503 range.selectNode(frame.frameElement);
504 pushRange(pageStart, RangeFind.endpoint(range, true));
505 pageStart = RangeFind.endpoint(range, false);
509 pushRange(pageStart, pageEnd);
512 if (frames.length == 0)
513 frames[0] = RangeFind.Range(RangeFind.endpoint(backup, true), 0);
518 this.ranges = this.makeFrameList(this.content);
520 this.startRange = this.selectedRange;
521 this.startRange.collapse(!this.reverse);
522 this.lastRange = this.selectedRange;
523 this.range = this.findRange(this.startRange);
524 this.ranges.first = this.range;
525 this.ranges.forEach(function (range) range.save());
530 // This doesn't work yet.
531 resetCaret: function () {
532 let equal = RangeFind.equal;
533 let selection = this.win.getSelection();
534 if (selection.rangeCount == 0)
535 selection.addRange(this.pageStart);
536 function getLines() {
537 let orig = selection.getRangeAt(0);
538 function getRanges(forward) {
539 selection.removeAllRanges();
540 selection.addRange(orig);
544 this.sel.lineMove(forward, false);
545 cur = selection.getRangeAt(0);
546 if (equal(cur, last))
552 for (let range in getRanges(true))
554 for (let range in getRanges(false))
557 for (let range in getLines()) {
558 if (this.sel.checkVisibility(range.startContainer, range.startOffset, range.startOffset))
564 find: function (word, reverse, private_) {
565 if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange))
568 this.wrapped = false;
569 this.finder.findBackwards = reverse ? !this.reverse : this.reverse;
570 let again = word == null;
572 word = this.lastString;
574 word = word.toLowerCase();
576 if (!again && (word === "" || word.indexOf(this.lastString) !== 0 || this.backward)) {
578 this.range.deselect();
580 this.range.descroll();
581 this.lastRange = this.startRange;
582 this.range = this.ranges.first;
586 var range = this.startRange;
588 for (let i in this.indexIter(private_)) {
589 if (!private_ && this.range.window != this.ranges[i].window && this.range.window != this.ranges[i].window.parent) {
590 this.range.descroll();
591 this.range.deselect();
593 this.range = this.ranges[i];
595 let start = RangeFind.sameDocument(this.lastRange, this.range.range) && this.range.intersects(this.lastRange) ?
596 RangeFind.endpoint(this.lastRange, !(again ^ this.backward)) :
597 RangeFind.endpoint(this.range.range, !this.backward);
599 if (this.backward && !again)
600 start = RangeFind.endpoint(this.startRange, false);
602 var range = this.finder.Find(word, this.range.range, start, this.range.range);
608 this.lastRange = range.cloneRange();
610 this.lastString = word;
618 if (range && (!private_ || private_ < 0))
619 this.selectedRange = range;
623 get stale() this._stale || this.baseDocument.get() != this.content.document,
624 set stale(val) this._stale = val,
626 addListeners: function () {
627 for (let range in array.iterValues(this.ranges))
628 range.window.addEventListener("unload", this.closure.onUnload, true);
630 purgeListeners: function () {
631 for (let range in array.iterValues(this.ranges))
633 range.window.removeEventListener("unload", this.closure.onUnload, true);
635 catch (e if e.result === Cr.NS_ERROR_FAILURE) {}
637 onUnload: function (event) {
638 this.purgeListeners();
639 if (this.highlighted)
640 this.highlight(true);
644 Range: Class("RangeFind.Range", {
645 init: function (range, index) {
649 this.document = range.startContainer.ownerDocument;
650 this.window = this.document.defaultView;
651 this.docShell = this.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
652 .QueryInterface(Ci.nsIDocShell);
654 if (this.selection == null)
660 intersects: function (range) RangeFind.intersects(this.range, range),
663 this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset);
665 this.initialSelection = null;
666 if (this.selection.rangeCount)
667 this.initialSelection = this.selection.getRangeAt(0);
670 descroll: function () {
671 this.window.scrollTo(this.scroll.x, this.scroll.y);
674 deselect: function () {
675 if (this.selection) {
676 this.selection.removeAllRanges();
677 if (this.initialSelection)
678 this.selection.addRange(this.initialSelection);
682 get selectionController() this.docShell
683 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
684 .QueryInterface(Ci.nsISelectionController),
687 return this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
694 contains: function (range, r) {
696 return range.compareBoundaryPoints(range.START_TO_END, r) >= 0 &&
697 range.compareBoundaryPoints(range.END_TO_START, r) <= 0;
700 util.reportError(e, true);
704 intersects: function (range, r) {
706 return r.compareBoundaryPoints(range.START_TO_END, range) >= 0 &&
707 r.compareBoundaryPoints(range.END_TO_START, range) <= 0;
710 util.reportError(e, true);
714 endpoint: function (range, before) {
715 range = range.cloneRange();
716 range.collapse(before);
719 equal: function (r1, r2) {
721 return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2);
727 nodeRange: function (node) {
728 let range = node.ownerDocument.createRange();
730 range.selectNode(node);
735 sameDocument: function (r1, r2) r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument,
736 selectNodePath: ["a", "xhtml:a", "*[@onclick]"].map(function (p) "ancestor-or-self::" + p).join(" | ")
741 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: