From 3da3d903a87e9dccbde480ca090e742547608511 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 14 May 2010 18:46:10 -0400 Subject: [PATCH] Replace Finder with RangeFinder. --HG-- branch : testing --- common/content/finder.js | 557 ++++++--------------------------------- 1 file changed, 82 insertions(+), 475 deletions(-) diff --git a/common/content/finder.js b/common/content/finder.js index cf4ebba2..3de2ddf9 100644 --- a/common/content/finder.js +++ b/common/content/finder.js @@ -1,467 +1,11 @@ -// Copyright (c) 2006-2008 by Martin Stubenschrott -// Copyright (c) 2007-2009 by Doug Kearns -// Copyright (c) 2008-2009 by Kris Maglione +// Copyright (c) 2008-2010 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. /** @scope modules */ -// TODO: proper backwards search - implement our own component? -// : implement our own highlighter? -// : should cancel search highlighting in 'incsearch' mode and jump -// back to the presearch page location - can probably use the same -// solution as marks -// : 'linksearch' searches should highlight link matches only -// : changing any search settings should also update the search state including highlighting -// : incremental searches shouldn't permanently update search modifiers -// -// TODO: Clean up this rat's nest. --Kris - -/** - * @instance finder - */ -const Finder = Module("finder", { - requires: ["config"], - - init: function () { - const self = this; - - this._found = false; // true if the last search was successful - this._backwards = false; // currently searching backwards - this._searchString = ""; // current search string (without modifiers) - this._searchPattern = ""; // current search string (includes modifiers) - this._lastSearchPattern = ""; // the last searched pattern (includes modifiers) - this._lastSearchString = ""; // the last searched string (without modifiers) - this._lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with this is not set - this._caseSensitive = false; // search string is case sensitive - this._linksOnly = false; // search is limited to link text only - - /* Stolen from toolkit.jar in Firefox, for the time being. The private - * methods were unstable, and changed. The new version is not remotely - * compatible with what we do. - * The following only applies to this object, and may not be - * necessary, or accurate, but, just in case: - * The Original Code is mozilla.org viewsource frontend. - * - * The Initial Developer of the Original Code is - * Netscape Communications Corporation. - * Portions created by the Initial Developer are Copyright (c) 2003 - * by the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Blake Ross (Original Author) - * Masayuki Nakano - * Ben Basson - * Jason Barnabe - * Asaf Romano - * Ehsan Akhgari - * Graeme McCutcheon - */ - this._highlighter = { - - doc: null, - - spans: [], - - search: function (aWord, matchCase) { - var finder = services.create("find"); - if (matchCase !== undefined) - self._caseSensitive = matchCase; - - var range; - while ((range = finder.Find(aWord, this.searchRange, this.startPt, this.endPt))) - yield range; - }, - - highlightDoc: function highlightDoc(win, aWord) { - this.doc = content.document; // XXX - Array.forEach(win.frames, function (frame) this.highlightDoc(frame, aWord), this); - - var doc = win.document; - if (!doc || !(doc instanceof HTMLDocument)) - return; - - if (!aWord) { - let elems = this._highlighter.spans; - for (let i = elems.length; --i >= 0;) { - let elem = elems[i]; - let docfrag = doc.createDocumentFragment(); - let next = elem.nextSibling; - let parent = elem.parentNode; - - let child; - while ((child = elem.firstChild)) - docfrag.appendChild(child); - - parent.removeChild(elem); - parent.insertBefore(docfrag, next); - parent.normalize(); - } - return; - } - - var baseNode = ; - baseNode = util.xmlToDom(baseNode, window.content.document); - - var body = doc.body; - var count = body.childNodes.length; - this.searchRange = doc.createRange(); - this.startPt = doc.createRange(); - this.endPt = doc.createRange(); - - this.searchRange.setStart(body, 0); - this.searchRange.setEnd(body, count); - - this.startPt.setStart(body, 0); - this.startPt.setEnd(body, 0); - this.endPt.setStart(body, count); - this.endPt.setEnd(body, count); - - liberator.interrupted = false; - let n = 0; - for (let retRange in this.search(aWord, this._caseSensitive)) { - // Highlight - var nodeSurround = baseNode.cloneNode(true); - var node = this.highlight(retRange, nodeSurround); - this.startPt = node.ownerDocument.createRange(); - this.startPt.setStart(node, node.childNodes.length); - this.startPt.setEnd(node, node.childNodes.length); - if (n++ % 20 == 0) - liberator.threadYield(true); - if (liberator.interrupted) - break; - } - }, - - highlight: function highlight(aRange, aNode) { - var startContainer = aRange.startContainer; - var startOffset = aRange.startOffset; - var endOffset = aRange.endOffset; - var docfrag = aRange.extractContents(); - var before = startContainer.splitText(startOffset); - var parent = before.parentNode; - aNode.appendChild(docfrag); - parent.insertBefore(aNode, before); - this.spans.push(aNode); - return aNode; - }, - - /** - * Clears all search highlighting. - */ - clear: function () { - this.spans.forEach(function (span) { - if (span.parentNode) { - let el = span.firstChild; - while (el) { - span.removeChild(el); - span.parentNode.insertBefore(el, span); - el = span.firstChild; - } - span.parentNode.removeChild(span); - } - }); - this.spans = []; - }, - - isHighlighted: function (doc) this.doc == doc && this.spans.length > 0 - }; - }, - - // set searchString, searchPattern, caseSensitive, linksOnly - _processUserPattern: function (pattern) { - //// strip off pattern terminator and offset - //if (backwards) - // pattern = pattern.replace(/\?.*/, ""); - //else - // pattern = pattern.replace(/\/.*/, ""); - - this._searchPattern = pattern; - - // links only search - \l wins if both modifiers specified - if (/\\l/.test(pattern)) - this._linksOnly = true; - else if (/\L/.test(pattern)) - this._linksOnly = false; - else if (options["linksearch"]) - this._linksOnly = true; - else - this._linksOnly = false; - - // strip links-only modifiers - pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; }); - - // case sensitivity - \c wins if both modifiers specified - if (/\c/.test(pattern)) - this._caseSensitive = false; - else if (/\C/.test(pattern)) - this._caseSensitive = true; - else if (options["ignorecase"] && options["smartcase"] && /[A-Z]/.test(pattern)) - this._caseSensitive = true; - else if (options["ignorecase"]) - this._caseSensitive = false; - else - this._caseSensitive = true; - - // strip case-sensitive modifiers - pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; }); - - // remove any modifier escape \ - pattern = pattern.replace(/\\(\\[cClL])/g, "$1"); - - this._searchString = pattern; - }, - - /** - * Called when the search dialog is requested. - * - * @param {number} mode The search mode, either modes.SEARCH_FORWARD or - * modes.SEARCH_BACKWARD. - * @default modes.SEARCH_FORWARD - */ - openPrompt: function (mode) { - this._backwards = mode == modes.SEARCH_BACKWARD; - commandline.open(this._backwards ? "?" : "/", "", mode); - // TODO: focus the top of the currently visible screen - }, - - // TODO: backwards seems impossible i fear - /** - * Searches the current buffer for str. - * - * @param {string} str The string to find. - */ - find: function (str) { - let fastFind = config.browser.fastFind; - - this._processUserPattern(str); - fastFind.caseSensitive = this._caseSensitive; - this._found = fastFind.find(this._searchString, this._linksOnly) != Ci.nsITypeAheadFind.FIND_NOTFOUND; - - if (!this._found) - this.setTimeout(function () liberator.echoerr("E486: Pattern not found: " + this._searchPattern, commandline.FORCE_SINGLELINE), 0); - }, - - /** - * Searches the current buffer again for the most recently used search - * string. - * - * @param {boolean} reverse Whether to search forwards or backwards. - * @default false - */ - findAgain: function (reverse) { - // This hack is needed to make n/N work with the correct string, if - // we typed /foo after the original search. Since searchString is - // readonly we have to call find() again to update it. - if (config.browser.fastFind.searchString != this._lastSearchString) - this.find(this._lastSearchString); - - let up = reverse ? !this._lastSearchBackwards : this._lastSearchBackwards; - let result = config.browser.fastFind.findAgain(up, this._linksOnly); - - if (result == Ci.nsITypeAheadFind.FIND_NOTFOUND) - liberator.echoerr("E486: Pattern not found: " + this._lastSearchPattern, commandline.FORCE_SINGLELINE); - else if (result == Ci.nsITypeAheadFind.FIND_WRAPPED) { - // hack needed, because wrapping causes a "scroll" event which clears - // our command line - setTimeout(function () { - let msg = up ? "search hit TOP, continuing at BOTTOM" : "search hit BOTTOM, continuing at TOP"; - commandline.echo(msg, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE); - }, 0); - } - else { - commandline.echo((up ? "?" : "/") + this._lastSearchPattern, null, commandline.FORCE_SINGLELINE); - - if (options["hlsearch"]) - this.highlight(this._lastSearchString); - } - }, - - /** - * Called when the user types a key in the search dialog. Triggers a - * search attempt if 'incsearch' is set. - * - * @param {string} str The search string. - */ - onKeyPress: function (str) { - if (options["incsearch"]) - this.find(str); - }, - - /** - * Called when the key is pressed to trigger a search. - * - * @param {string} str The search string. - * @param {boolean} forcedBackward Whether to search forwards or - * backwards. This overrides the direction set in - * (@link #openPrompt). - * @default false - */ - onSubmit: function (str, forcedBackward) { - if (typeof forcedBackward === "boolean") - this._backwards = forcedBackward; - - if (str) - var pattern = str; - else { - liberator.assert(this._lastSearchPattern, "E35: No previous search pattern"); - pattern = this._lastSearchPattern; - } - - this.clear(); - - if (!options["incsearch"] || !str || !this._found) { - // prevent any current match from matching again - if (!window.content.getSelection().isCollapsed) - window.content.getSelection().getRangeAt(0).collapse(this._backwards); - - this.find(pattern); - } - - this._lastSearchBackwards = this._backwards; - //lastSearchPattern = pattern.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX - this._lastSearchPattern = pattern; - this._lastSearchString = this._searchString; - - // TODO: move to find() when reverse incremental searching is kludged in - // need to find again for reverse searching - if (this._backwards) - this.setTimeout(function () { this.findAgain(false); }, 0); - - if (options["hlsearch"]) - this.highlight(this._searchString); - - modes.reset(); - }, - - /** - * Called when the search is canceled. For example, if someone presses - * while typing a search. - */ - onCancel: function () { - // TODO: code to reposition the document to the place before search started - }, - - /** - * Highlights all occurances of str in the buffer. - * - * @param {string} str The string to highlight. - */ - highlight: function (str) { - // FIXME: Thunderbird incompatible - if (config.name == "Muttator") - return; - - if (this._highlighter.isHighlighted(content.document)) - return; - - if (!str) - str = this._lastSearchString; - - this._highlighter.highlightDoc(window.content, str); - - // recreate selection since highlightDoc collapses the selection - if (window.content.getSelection().isCollapsed) - config.browser.fastFind.findAgain(this._backwards, this._linksOnly); - - // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"]) - }, - - /** - * Clears all search highlighting. - */ - clear: function () { - this._highlighter.clear(); - } -}, { -}, { - commandline: function () { - // Event handlers for search - closure is needed - commandline.registerCallback("change", modes.SEARCH_FORWARD, this.closure.onKeyPress); - commandline.registerCallback("submit", modes.SEARCH_FORWARD, this.closure.onSubmit); - commandline.registerCallback("cancel", modes.SEARCH_FORWARD, this.closure.onCancel); - // TODO: allow advanced myModes in register/triggerCallback - commandline.registerCallback("change", modes.SEARCH_BACKWARD, this.closure.onKeyPress); - commandline.registerCallback("submit", modes.SEARCH_BACKWARD, this.closure.onSubmit); - commandline.registerCallback("cancel", modes.SEARCH_BACKWARD, this.closure.onCancel); - - }, - commands: function () { - commands.add(["noh[lsearch]"], - "Remove the search highlighting", - function () { finder.clear(); }, - { argCount: "0" }); - }, - mappings: function () { - var myModes = config.browserModes; - myModes = myModes.concat([modes.CARET]); - - mappings.add(myModes, - ["/"], "Search forward for a pattern", - function () { finder.openPrompt(modes.SEARCH_FORWARD); }); - - mappings.add(myModes, - ["?"], "Search backwards for a pattern", - function () { finder.openPrompt(modes.SEARCH_BACKWARD); }); - - mappings.add(myModes, - ["n"], "Find next", - function () { finder.findAgain(false); }); - - mappings.add(myModes, - ["N"], "Find previous", - function () { finder.findAgain(true); }); - - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"], - "Find word under cursor", - function () { - this._found = false; - finder.onSubmit(buffer.getCurrentWord(), false); - }); - - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"], - "Find word under cursor backwards", - function () { - this._found = false; - finder.onSubmit(buffer.getCurrentWord(), true); - }); - }, - options: function () { - options.add(["hlsearch", "hls"], - "Highlight previous search pattern matches", - "boolean", "false", { - setter: function (value) { - try { - if (value) - finder.highlight(); - else - finder.clear(); - } - catch (e) {} - - return value; - } - }); - - options.add(["ignorecase", "ic"], - "Ignore case in search patterns", - "boolean", true); - - options.add(["incsearch", "is"], - "Show where the search pattern matches as it is typed", - "boolean", true); - - options.add(["linksearch", "lks"], - "Limit the search to hyperlink text", - "boolean", false); - - options.add(["smartcase", "scs"], - "Override the 'ignorecase' option if the pattern contains uppercase characters", - "boolean", true); - } -}); - +/** @instance rangefinder */ const RangeFinder = Module("rangefinder", { requires: ["config"], @@ -605,45 +149,81 @@ const RangeFinder = Module("rangefinder", { }, commands: function () { + commands.add(["noh[lsearch]"], + "Remove the search highlighting", + function () { rangefinder.clear(); }, + { argCount: "0" }); }, mappings: function () { var myModes = config.browserModes.concat([modes.CARET]); mappings.add(myModes, - ["g/"], "Search forward for a pattern", + ["/"], "Search forward for a pattern", function () { rangefinder.openPrompt(modes.FIND_FORWARD); }); mappings.add(myModes, - ["g?"], "Search backwards for a pattern", + ["?"], "Search backwards for a pattern", function () { rangefinder.openPrompt(modes.FIND_BACKWARD); }); mappings.add(myModes, - ["g."], "Find next", + ["n"], "Find next", function () { rangefinder.findAgain(false); }); mappings.add(myModes, - ["g,"], "Find previous", + ["N"], "Find previous", function () { rangefinder.findAgain(true); }); - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["g*"], + mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"], "Find word under cursor", function () { rangefinder._found = false; rangefinder.onSubmit(buffer.getCurrentWord(), false); }); - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["g#"], + mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"], "Find word under cursor backwards", function () { rangefinder._found = false; rangefinder.onSubmit(buffer.getCurrentWord(), true); }); + }, modes: function () { modes.addMode("FIND_FORWARD", true); modes.addMode("FIND_BACKWARD", true); }, options: function () { + options.add(["hlsearch", "hls"], + "Highlight previous search pattern matches", + "boolean", "false", { + setter: function (value) { + try { + if (value) + rangefinder.highlight(); + else + rangefinder.clear(); + } + catch (e) {} + + return value; + } + }); + + options.add(["ignorecase", "ic"], + "Ignore case in search patterns", + "boolean", true); + + options.add(["incsearch", "is"], + "Show where the search pattern matches as it is typed", + "boolean", true); + + options.add(["linksearch", "lks"], + "Limit the search to hyperlink text", + "boolean", false); + + options.add(["smartcase", "scs"], + "Override the 'ignorecase' option if the pattern contains uppercase characters", + "boolean", true); } }); @@ -657,20 +237,29 @@ const RangeFind = Class("RangeFind", { this.finder.caseSensitive = this.matchCase; this.ranges = this.makeFrameList(content); - this.range = RangeFind.Range(tabs.localStore.focusedFrame || content); - this.startRange = (this.range.selection.rangeCount ? this.range.selection.getRangeAt(0) : this.ranges[0].range).cloneRange(); - this.startRange.collapse(!backward); - this.range = this.findRange(this.startRange); - this.ranges.first = this.range; + this.reset(); this.highlighted = null; this.lastString = ""; - this.lastRange = null; this.forward = null; this.found = false; }, + get selectedRange() { + let range = RangeFind.Range(tabs.localStore.focusedFrame || content); + range = (range.selection.rangeCount ? range.selection.getRangeAt(0) : this.ranges[0].range).cloneRange(); + return range; + }, + + reset: function () { + this.startRange = this.selectedRange; + this.startRange.collapse(!this.reverse); + this.lastRange = this.selectedRange; + this.range = this.findRange(this.startRange); + this.ranges.first = this.range; + }, + sameDocument: function (r1, r2) r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument, compareRanges: function (r1, r2) @@ -708,15 +297,21 @@ const RangeFind = Class("RangeFind", { let backup = null; function pushRange(start, end) { + function push(r) { + r = RangeFind.Range(r, frames.length); + if (r) + frames.push(r); + } + let range = start.startContainer.ownerDocument.createRange(); range.setStart(start.startContainer, start.startOffset); range.setEnd(end.startContainer, end.startOffset); if (!self.elementPath) - frames.push(RangeFind.Range(range, frames.length)); + push(range); else for (let r in self.findSubRanges(range)) - frames.push(RangeFind.Range(r, frames.length)); + push(r); } function rec(win) { let doc = win.document; @@ -743,7 +338,7 @@ const RangeFind = Class("RangeFind", { // This doesn't work yet. resetCaret: function () { - let equal = function (r1, r2) !r1.compareBoundaryPoints(Range.START_TO_START, r2) && !r1.compareBoundaryPoints(Range.END_TO_END, r2); + let equal = RangeFind.equal; letselection = this.win.getSelection(); if (selection.rangeCount == 0) selection.addRange(this.pageStart); @@ -809,6 +404,8 @@ const RangeFind = Class("RangeFind", { this.range.descroll(); this.lastRange = this.startRange; this.range = this.ranges.first; + } else if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange)) { + this.reset(); } if (word == "") @@ -954,6 +551,9 @@ const RangeFind = Class("RangeFind", { this.window = this.document.defaultView; this.range = range; + if (this.selection == null) + return false; + this.save(); }, @@ -991,13 +591,20 @@ const RangeFind = Class("RangeFind", { .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsISelectionDisplay) .QueryInterface(Ci.nsISelectionController), - get selection() this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL) + get selection() { + try { + return this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL) + } catch (e) { + return null; + }} + }), endpoint: function (range, before) { range = range.cloneRange(); range.collapse(before); return range; - } + }, + equal: function (r1, r2) !r1.compareBoundaryPoints(Range.START_TO_START, r2) && !r1.compareBoundaryPoints(Range.END_TO_END, r2) }); // vim: set fdm=marker sw=4 ts=4 et: