From 57886630c05cb5b9d0d5a5151dd4e08b2243e161 Mon Sep 17 00:00:00 2001 From: Doug Kearns Date: Fri, 14 Sep 2007 13:38:17 +0000 Subject: [PATCH] add a 'hlsearchstyle' option to allow for match styling and a 'linksearch' option to restrict page searches to link text --- NEWS | 4 + chrome/content/vimperator/find.js | 163 ++++++++++++++++---------- chrome/content/vimperator/mappings.js | 12 +- chrome/content/vimperator/options.js | 20 +++- 4 files changed, 131 insertions(+), 68 deletions(-) diff --git a/NEWS b/NEWS index 47a19f43..5d4a9a52 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,10 @@
 2007-XX-XX:
 	* version 0.5.2
+	* added 'hlsearchstyle' option to allow for user CSS styling of the
+	  highlighted text strings when 'hlsearch' is set
+	* added 'linksearch' option to restrict page searches to link text - \U
+	  and \u can be used in the search pattern to override 'linksearch'
 	* added a visual bell and replaced 'beep' with 'visualbell'
 	* added 'hlsearch','incsearch', 'ignorecase' and 'smartcase' options
 
diff --git a/chrome/content/vimperator/find.js b/chrome/content/vimperator/find.js
index 417300b7..0e47db0b 100644
--- a/chrome/content/vimperator/find.js
+++ b/chrome/content/vimperator/find.js
@@ -26,16 +26,29 @@ the provisions above, a recipient may use your version of this file under
 the terms of any one of the MPL, the GPL or the LGPL.
 }}} ***** END LICENSE BLOCK *****/
 
-// TODO:  should cancel search highlighting in 'incsearch' mode
+// TODO: proper backwards search - implement our own component?
+//     : implement our own highlighter?
+//     : frameset pages
+//     :  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
+
 // make sure you only create this object when the "vimperator" object is ready
 function Search() //{{{
 {
-    var self = this;                  // needed for callbacks since "this" is the "vimperator" object in a callback
-    var found = false;                // true if the last search was successful
-    var backwards = false;            // currently searching backwards
-    var lastsearch = "";              // keep track of the last searched string
-    var lastsearch_backwards = false; // like "backwards", but for the last search, so if you cancel a search with  this is not set
-    var case_sensitive = true;
+    var self = this;                   // needed for callbacks since "this" is the "vimperator" object in a callback
+    var found = false;                 // true if the last search was successful
+    var backwards = false;             // currently searching backwards
+    var search_string = "";            // current search string (without modifiers)
+    var search_pattern = "";           // current search string (includes modifiers)
+    var last_search_pattern = "";      // the last searched pattern (includes modifiers)
+    var last_search_string = "";       // the last searched string (without modifiers)
+    var last_search_backwards = false; // like "backwards", but for the last search, so if you cancel a search with  this is not set
+    var case_sensitive = false;        // search string is case sensitive
+    var links_only = false;            // search is limited to link text only
 
     // Event handlers for search - closure is needed
     vimperator.registerCallback("change", vimperator.modes.SEARCH_FORWARD, function(command) { self.searchKeyPressed(command); });
@@ -46,43 +59,46 @@ function Search() //{{{
     vimperator.registerCallback("submit", vimperator.modes.SEARCH_BACKWARD, function(command) { self.searchSubmitted(command); });
     vimperator.registerCallback("cancel", vimperator.modes.SEARCH_BACKWARD, function() { self.searchCanceled(); });
 
-    // clean the pattern search string of modifiers and set the
-    // case-sensitivity flag
-    function processPattern(pattern)
+    // set search_string, search_pattern, case_sensitive, links_only
+    function processUserPattern(pattern)
     {
-        // strip off pattern terminator and trailing /junk
+        // strip off pattern terminator and offset
         if (backwards)
             pattern = pattern.replace(/\?.*/, "");
         else
             pattern = pattern.replace(/\/.*/, "");
 
-        if (!pattern)
-            pattern = lastsearch;
+        search_pattern = pattern;
 
-        if (/\\C/.test(pattern))
-        {
-            case_sensitive = true;
-            pattern = pattern.replace(/\\C/, "");
-        }
-        else if (/\\c/.test(pattern))
-        {
+        // case sensitivity - \c wins if both modifiers specified
+        if (/\c/.test(pattern))
             case_sensitive = false;
-            pattern = pattern.replace(/\\c/, "");
-        }
+        else if (/\C/.test(pattern))
+            case_sensitive = true;
         else if (vimperator.options["ignorecase"] && vimperator.options["smartcase"] && /[A-Z]/.test(pattern))
-        {
             case_sensitive = true;
-        }
         else if (vimperator.options["ignorecase"])
-        {
             case_sensitive = false;
-        }
         else
-        {
             case_sensitive = true;
-        }
 
-        return pattern;
+        // links only search - \u wins if both modifiers specified
+        if (/\\u/.test(pattern))
+            links_only = false;
+        else if (/\U/.test(pattern))
+            links_only = true;
+        else if (vimperator.options["linksearch"])
+            links_only = true;
+        else
+            links_only = false;
+
+        // strip modifiers
+        pattern = pattern.replace(/(\\)?\\[cCuU]/g, function($0, $1) { return $1 ? $0 : "" });
+
+        // remove the modifer escape \
+        pattern = pattern.replace(/\\(\\[cCuU])/g, '$1')
+
+        search_string = pattern;
     }
 
     // Called when the search dialog is asked for
@@ -108,10 +124,14 @@ function Search() //{{{
     this.find = function(str, backwards)
     {
         var fastFind = getBrowser().fastFind;
-        str = processPattern(str);
+
+        processUserPattern(str);
 
         fastFind.caseSensitive = case_sensitive;
-        found = fastFind.find(str, false) != Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;
+        found = fastFind.find(search_string, links_only) != Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;
+
+        if (!found)
+            vimperator.echoerr("E486: Pattern not found: " + search_pattern);
 
         return found;
     }
@@ -120,16 +140,12 @@ function Search() //{{{
     this.findAgain = function(reverse)
     {
         // this hack is needed to make n/N work with the correct string, if
-        // we typed /foo after the original search
-        // TODO: this should also clear the current item highlighting
-        if (getBrowser().fastFind.searchString != lastsearch)
-        {
-            this.clear();
-            this.find(lastsearch, false);
-            this.highlight(lastsearch);
-        }
+        // we typed /foo after the original search.  Since searchString is
+        // readonly we have to call find() again to update it.
+        if (getBrowser().fastFind.searchString != last_search_string)
+            this.find(last_search_string, false);
 
-        var up = reverse ? !lastsearch_backwards : lastsearch_backwards;
+        var up = reverse ? !last_search_backwards : last_search_backwards;
         var result;
 
         if (up)
@@ -139,7 +155,7 @@ function Search() //{{{
 
         if (result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
         {
-            vimperator.echoerr("E486: Pattern not found: " + lastsearch);
+            vimperator.echoerr("E486: Pattern not found: " + last_search_pattern);
         }
         else if (result == Components.interfaces.nsITypeAheadFind.FIND_WRAPPED)
         {
@@ -150,21 +166,22 @@ function Search() //{{{
                     vimperator.echoerr("search hit TOP, continuing at BOTTOM");
                 else
                     vimperator.echoerr("search hit BOTTOM, continuing at TOP");
-            }, 10);
+            }, 0);
         }
-        else // just clear the command line if something has been found
+        else
         {
-            vimperator.echo("");
+            vimperator.echo((up ? "?" : "/") + last_search_pattern);
+
+            if (vimperator.options["hlsearch"])
+                this.highlight(last_search_string);
         }
     }
 
-    // Called when the user types a key in the search dialog. Triggers a find attempt
+    // Called when the user types a key in the search dialog. Triggers a find attempt if 'incsearch' is set
     this.searchKeyPressed = function(command)
     {
-        if (!vimperator.options["incsearch"])
-            return;
-
-        this.find(command, backwards);
+        if (vimperator.options["incsearch"])
+            this.find(command, backwards);
     }
 
     // Called when the enter key is pressed to trigger a search
@@ -174,17 +191,24 @@ function Search() //{{{
         if (typeof forced_backward === "boolean")
             backwards = forced_backward;
 
+        // use the last pattern if none specified
+        if (!command)
+            command = last_search_pattern;
+
         this.clear();
         this.find(command, backwards);
-        this.highlight(command);
 
-        // need to find again to draw the highlight of the current search
-        // result over the "highlight all" search results
-        // very hacky, but seems to work
-        setTimeout(function() { self.findAgain(false); }, 10);
+        last_search_backwards = backwards;
+        last_search_pattern = command.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX
+        last_search_string = search_string;
 
-        lastsearch_backwards = backwards;
-        lastsearch = command;
+        // TODO: move to find() when reverse incremental searching is kludged in
+        // need to find again for reverse searching
+        if (backwards)
+            setTimeout(function() { self.findAgain(false); }, 0);
+
+        if (vimperator.options["hlsearch"])
+            this.highlight(search_string);
 
         vimperator.setMode(vimperator.modes.NORMAL);
         vimperator.focusContent();
@@ -196,25 +220,42 @@ function Search() //{{{
     {
         //removeMode(MODE_SEARCH);
         vimperator.setMode(vimperator.modes.NORMAL);
-        this.clear();
         vimperator.focusContent();
     }
 
-    this.highlight = function(word)
+    this.highlight = function(text)
     {
-        if (!word)
-            word = lastsearch;
+        // already highlighted?
+        if (window.content.document.getElementById("__firefox-findbar-search-id"))
+            return;
+
+        if (!text)
+            text = last_search_string;
 
         // NOTE: setCaseSensitivity() in FF2 does NOT set the
         // accessibility.typeaheadfind.casesensitive pref as needed by
         // highlightDoc()
         gFindBar.mTypeAheadCaseSensitive = case_sensitive ? 1 : 0;
-        gFindBar.highlightDoc("yellow", "black", word);
+        gFindBar.highlightDoc("white", "black", text);
+
+        // TODO: seems fast enough for now
+        // NOTE: FF2 highlighting spans all have this id rather than a class attribute
+        var spans = vimperator.buffer.evaluateXPath('//span[@id="__firefox-findbar-search-id"]')
+        for (var i = 0; i < spans.snapshotLength; i++)
+            spans.snapshotItem(i).setAttribute("style", vimperator.options["hlsearchstyle"]);
+
+        // recreate selection since _highlightDoc collapses the selection backwards
+        getBrowser().fastFind.findNext();
+
+        // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [@type="simple"])
     }
 
     this.clear = function()
     {
         gFindBar.highlightDoc();
+        // need to manually collapse the selection if the document is not
+        // highlighted
+        getBrowser().fastFind.collapseSelection();
     }
 
 } //}}}
diff --git a/chrome/content/vimperator/mappings.js b/chrome/content/vimperator/mappings.js
index 40fdf6da..2ca9a642 100644
--- a/chrome/content/vimperator/mappings.js
+++ b/chrome/content/vimperator/mappings.js
@@ -945,7 +945,9 @@ function Mappings() //{{{
             usage: ["/{pattern}[/]"],
             help: "Search forward for the first occurance of {pattern}.
" + "When \"\\c\" appears anywhere in the pattern the whole pattern is handled as though 'ignorecase' is on. " + - "\"\\C\" forces case-sensitive matching for the whole pattern." + "\"\\C\" forces case-sensitive matching for the whole pattern.
" + + "If \"\\u\" appears in the pattern only the text of links is searched for a match as though 'linksearch' is on. " + + "\"\\U\" forces the entire page to be searched for a match." } )); addDefaultMap(new Map(vimperator.modes.NORMAL, ["?"], @@ -954,9 +956,11 @@ function Mappings() //{{{ short_help: "Search backwards for a pattern", usage: ["?{pattern}[?]"], help: "Search backward for the first occurance of {pattern}.
" + - "When '\\c' appears anywhere in the pattern the whole pattern is handled as though 'ignorecase' is on. " + - "'\\C' forces case-sensitive matching for the whole pattern.
" + - "NOTE: incremental searching currenly only works in the forward direction." + "When \"\\c\" appears anywhere in the pattern the whole pattern is handled as though 'ignorecase' is on. " + + "\"\\C\" forces case-sensitive matching for the whole pattern.
" + + "If \"\\u\" appears in the pattern only the text of links is searched for a match as though 'linksearch' is on. " + + "\"\\U\" forces the entire page to be searched for a match.
" + + "NOTE: incremental searching currently only works in the forward direction." } )); addDefaultMap(new Map(vimperator.modes.NORMAL, ["n"], diff --git a/chrome/content/vimperator/options.js b/chrome/content/vimperator/options.js index 1a39caf9..37e9755e 100644 --- a/chrome/content/vimperator/options.js +++ b/chrome/content/vimperator/options.js @@ -387,7 +387,7 @@ function Options() //{{{ )); addOption(new Option(["focusedhintstyle", "fhs"], "string", { - short_help: "CSS specification of focused hints appearance", + short_help: "CSS specification of focused hints", default_value: "z-index:5000; font-family:monospace; font-size:12px; color:ButtonText; background-color:ButtonShadow; " + "border-color:ButtonShadow; border-width:1px; border-style:solid; padding:0px 1px 0px 1px; position:absolute;" } @@ -421,12 +421,12 @@ function Options() //{{{ )); addOption(new Option(["hintstyle", "hs"], "string", { - short_help: "CSS specification of unfocused hints appearance", + short_help: "CSS specification of unfocused hints", default_value: "z-index:5000; font-family:monospace; font-size:12px; color:white; background-color:red; " + "border-color:ButtonShadow; border-width:0px; border-style:solid; padding:0px 1px 0px 1px; position:absolute;" } )); - addOption(new Option(["hinttags"], "string", + addOption(new Option(["hinttags", "ht"], "string", { short_help: "XPath string of hintable elements activated by 'f' and 'F'", default_value: DEFAULT_HINTTAGS @@ -439,6 +439,12 @@ function Options() //{{{ default_value: false } )); + addOption(new Option(["hlsearchstyle", "hlss"], "string", + { + short_help: "CSS specification of highlighted search items", + default_value: "color: black; background-color: yellow; padding: 0; display: inline;" + } + )); addOption(new Option(["ignorecase", "ic"], "boolean", { short_help: "Ignore case in search patterns", @@ -452,6 +458,13 @@ function Options() //{{{ default_value: true } )); + addOption(new Option(["linksearch", "ls"], "boolean", + { + short_help: "Limit the search to hyperlink text", + help: "This includes (X)HTML elements with an \"href\" atrribute and XLink \"simple\" links.", + default_value: false + } + )); addOption(new Option(["maxhints", "mh"], "number", { short_help: "Maximum number of simultaneously shown hints", @@ -572,6 +585,7 @@ function Options() //{{{ addOption(new Option(["visualbell", "vb"], "boolean", { short_help: "Use visual bell instead of beeping on errors", + setter: function(value) { Options.setPref("visualbell", value); Options.setFirefoxPref("accessibility.typeaheadfind.enablesound", !value); }, default_value: false } ));