aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/main.coffee60
-rw-r--r--background_scripts/marks.coffee2
-rw-r--r--content_scripts/hud.coffee40
-rw-r--r--content_scripts/mode_find.coffee156
-rw-r--r--content_scripts/mode_visual_edit.coffee35
-rw-r--r--content_scripts/vimium.css16
-rw-r--r--content_scripts/vimium_frontend.coffee292
-rw-r--r--lib/dom_utils.coffee12
-rw-r--r--lib/find_mode_history.coffee50
-rw-r--r--lib/settings.coffee88
-rw-r--r--lib/utils.coffee9
-rw-r--r--manifest.json1
-rw-r--r--pages/hud.coffee87
-rw-r--r--pages/hud.html3
-rw-r--r--pages/options.coffee67
-rw-r--r--pages/options.html2
-rw-r--r--tests/dom_tests/dom_tests.html1
-rw-r--r--tests/unit_tests/completion_test.coffee3
-rw-r--r--tests/unit_tests/settings_test.coffee20
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee11
20 files changed, 551 insertions, 404 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index d4b14f3c..40d570ee 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -187,37 +187,35 @@ getCompletionKeysRequest = (request, keysToCheck = "") ->
completionKeys: generateCompletionKeys(keysToCheck)
validFirstKeys: validFirstKeys
-#
-# Opens the url in the current tab.
-#
-openUrlInCurrentTab = (request) ->
- chrome.tabs.getSelected(null,
- (tab) -> chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) }))
+TabOperations =
+ # Opens the url in the current tab.
+ openUrlInCurrentTab: (request, callback = (->)) ->
+ chrome.tabs.getSelected null, (tab) ->
+ callback = (->) unless typeof callback == "function"
+ chrome.tabs.update tab.id, { url: Utils.convertToUrl(request.url) }, callback
-#
-# Opens request.url in new tab and switches to it if request.selected is true.
-#
-openUrlInNewTab = (request, callback) ->
- chrome.tabs.getSelected null, (tab) ->
- tabConfig =
- url: Utils.convertToUrl request.url
- index: tab.index + 1
- selected: true
- windowId: tab.windowId
- # FIXME(smblott). openUrlInNewTab is being called in two different ways with different arguments. We
- # should refactor it such that this check on callback isn't necessary.
+ # Opens request.url in new tab and switches to it if request.selected is true.
+ openUrlInNewTab: (request, callback = (->)) ->
+ chrome.tabs.getSelected null, (tab) ->
+ tabConfig =
+ url: Utils.convertToUrl request.url
+ index: tab.index + 1
+ selected: true
+ windowId: tab.windowId
+ openerTabId: tab.id
+ callback = (->) unless typeof callback == "function"
+ chrome.tabs.create tabConfig, callback
+
+ openUrlInIncognito: (request, callback = (->)) ->
callback = (->) unless typeof callback == "function"
- chrome.tabs.create tabConfig, callback
-
-openUrlInIncognito = (request) ->
- chrome.windows.create({ url: Utils.convertToUrl(request.url), incognito: true})
+ chrome.windows.create {url: Utils.convertToUrl(request.url), incognito: true}, callback
#
# Copies or pastes some data (request.data) to/from the clipboard.
# We return null to avoid the return value from the copy operations being passed to sendResponse.
#
copyToClipboard = (request) -> Clipboard.copy(request.data); null
-pasteFromClipboard = (request) -> Clipboard.paste(); null
+pasteFromClipboard = (request) -> Clipboard.paste()
#
# Selects the tab with the ID specified in request.id
@@ -256,7 +254,7 @@ BackgroundCommands =
if url == "pages/blank.html"
# "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.
url = if tab.incognito then "chrome://newtab" else chrome.runtime.getURL url
- openUrlInNewTab { url }, callback
+ TabOperations.openUrlInNewTab { url }, callback
duplicateTab: (callback) ->
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.duplicate(tab.id)
@@ -296,8 +294,8 @@ BackgroundCommands =
scrollX: tabQueueEntry.scrollX,
scrollY: tabQueueEntry.scrollY)
callback()))
- openCopiedUrlInCurrentTab: (request) -> openUrlInCurrentTab({ url: Clipboard.paste() })
- openCopiedUrlInNewTab: (request) -> openUrlInNewTab({ url: Clipboard.paste() })
+ openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab({ url: Clipboard.paste() })
+ openCopiedUrlInNewTab: (request) -> TabOperations.openUrlInNewTab({ url: Clipboard.paste() })
togglePinTab: (request) ->
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.update(tab.id, { pinned: !tab.pinned }))
@@ -652,9 +650,9 @@ portHandlers =
sendRequestHandlers =
getCompletionKeys: getCompletionKeysRequest
getCurrentTabUrl: getCurrentTabUrl
- openUrlInNewTab: openUrlInNewTab
- openUrlInIncognito: openUrlInIncognito
- openUrlInCurrentTab: openUrlInCurrentTab
+ openUrlInNewTab: TabOperations.openUrlInNewTab
+ openUrlInIncognito: TabOperations.openUrlInIncognito
+ openUrlInCurrentTab: TabOperations.openUrlInCurrentTab
openOptionsPageInNewTab: openOptionsPageInNewTab
registerFrame: registerFrame
unregisterFrame: unregisterFrame
@@ -725,7 +723,7 @@ showUpgradeMessage = ->
Settings.set "previousVersion", currentVersion
chrome.notifications.onClicked.addListener (id) ->
if id == notificationId
- openUrlInNewTab url: "https://github.com/philc/vimium#release-notes"
+ TabOperations.openUrlInNewTab url: "https://github.com/philc/vimium#release-notes"
else
# We need to wait for the user to accept the "notifications" permission.
chrome.permissions.onAdded.addListener showUpgradeMessage
@@ -740,3 +738,5 @@ chrome.windows.getAll { populate: true }, (windows) ->
chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler())
showUpgradeMessage()
+
+root.TabOperations = TabOperations
diff --git a/background_scripts/marks.coffee b/background_scripts/marks.coffee
index 6e5f08ba..70ec1c17 100644
--- a/background_scripts/marks.coffee
+++ b/background_scripts/marks.coffee
@@ -82,7 +82,7 @@ Marks =
@gotoPositionInTab extend markInfo, tabId: tab.id
else
# There is no existing matching tab, we'll have to create one.
- chrome.tabs.create { url: @getBaseUrl markInfo.url }, (tab) =>
+ TabOperations.openUrlInNewTab { url: @getBaseUrl markInfo.url }, (tab) =>
# Note. tabLoadedHandlers is defined in "main.coffee". The handler below will be called when the tab
# is loaded, its DOM is ready and it registers with the background page.
tabLoadedHandlers[tab.id] = => @gotoPositionInTab extend markInfo, tabId: tab.id
diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee
index 84b8abeb..bfad71b7 100644
--- a/content_scripts/hud.coffee
+++ b/content_scripts/hud.coffee
@@ -6,6 +6,7 @@ HUD =
tween: null
hudUI: null
_displayElement: null
+ findMode: null
# This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"
# test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that
@@ -26,6 +27,19 @@ HUD =
@hudUI.show {name: "show", text}
@tween.fade 1.0, 150
+ showFindMode: (@findMode = null) ->
+ return unless @enabled()
+ @hudUI.show {name: "showFindMode", text: ""}
+ @tween.fade 1.0, 150
+
+ search: (data) ->
+ @findMode.findInPlace data.query
+
+ # Show the number of matches in the HUD UI.
+ matchCount = if FindMode.query.parsedQuery.length > 0 then FindMode.query.matchCount else 0
+ showMatchText = FindMode.query.rawQuery.length > 0
+ @hudUI.postMessage {name: "updateMatchesCount", matchCount, showMatchText}
+
# Hide the HUD.
# If :immediate is falsy, then the HUD is faded out smoothly (otherwise it is hidden immediately).
# If :updateIndicator is truthy, then we also refresh the mode indicator. The only time we don't update the
@@ -42,6 +56,32 @@ HUD =
else
@tween.fade 0, 150, => @hide true, updateIndicator
+ hideFindMode: (data) ->
+ @findMode.checkReturnToViewPort()
+
+ # An element element won't receive a focus event if the search landed on it while we were in the HUD
+ # iframe. To end up with the correct modes active, we create a focus/blur event manually after refocusing
+ # this window.
+ window.focus()
+
+ focusNode = DomUtils.getSelectionFocusElement()
+ document.activeElement?.blur()
+ focusNode?.focus()
+
+ {event} = data
+
+ if event.keyCode == keyCodes.enter
+ handleEnterForFindMode()
+ if FindMode.query.hasResults
+ postExit = -> new PostFindMode
+ else if KeyboardUtils.isEscape event
+ # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we
+ # wait until the mode is closed before running it.
+ postExit = handleEscapeForFindMode
+
+ @findMode.exit()
+ postExit?()
+
isReady: do ->
ready = false
DomUtils.documentReady -> ready = true
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index ed08fbd5..9b47cfbd 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -54,5 +54,161 @@ class PostFindMode extends SuppressPrintable
handlerStack.remove()
@continueBubbling
+class FindMode extends Mode
+ @query:
+ rawQuery: ""
+ matchCount: 0
+ hasResults: false
+
+ constructor: (options = {}) ->
+ # Save the selection, so findInPlace can restore it.
+ @initialRange = getCurrentRange()
+ FindMode.query = rawQuery: ""
+ if options.returnToViewport
+ @scrollX = window.scrollX
+ @scrollY = window.scrollY
+ super extend options,
+ name: "find"
+ indicator: false
+ exitOnClick: true
+
+ HUD.showFindMode this
+
+ exit: (event) ->
+ super()
+ handleEscapeForFindMode() if event
+
+ restoreSelection: ->
+ range = @initialRange
+ selection = getSelection()
+ selection.removeAllRanges()
+ selection.addRange range
+
+ findInPlace: (query) ->
+ # If requested, restore the scroll position (so that failed searches leave the scroll position unchanged).
+ @checkReturnToViewPort()
+ FindMode.updateQuery query
+ # Restore the selection. That way, we're always searching forward from the same place, so we find the right
+ # match as the user adds matching characters, or removes previously-matched characters. See #1434.
+ @restoreSelection()
+ query = if FindMode.query.isRegex then FindMode.getNextQueryFromRegexMatches(0) else FindMode.query.parsedQuery
+ FindMode.query.hasResults = FindMode.execute query
+
+ @updateQuery: (query) ->
+ @query.rawQuery = query
+ # the query can be treated differently (e.g. as a plain string versus regex depending on the presence of
+ # escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal
+ # character. here we grep for the relevant escape sequences.
+ @query.isRegex = Settings.get 'regexFindMode'
+ hasNoIgnoreCaseFlag = false
+ @query.parsedQuery = @query.rawQuery.replace /(\\{1,2})([rRI]?)/g, (match, slashes, flag) =>
+ return match if flag == "" or slashes.length != 1
+ switch (flag)
+ when "r"
+ @query.isRegex = true
+ when "R"
+ @query.isRegex = false
+ when "I"
+ hasNoIgnoreCaseFlag = true
+ ""
+
+ # default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
+ @query.ignoreCase = !hasNoIgnoreCaseFlag && !Utils.hasUpperCase(@query.parsedQuery)
+
+ # if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them
+ # sequentially so the browser handles the scrolling / text selection.
+ if @query.isRegex
+ try
+ pattern = new RegExp(@query.parsedQuery, "g" + (if @query.ignoreCase then "i" else ""))
+ catch error
+ # if we catch a SyntaxError, assume the user is not done typing yet and return quietly
+ return
+ # innerText will not return the text of hidden elements, and strip out tags while preserving newlines
+ text = document.body.innerText
+ @query.regexMatches = text.match(pattern)
+ @query.activeRegexIndex = 0
+ @query.matchCount = @query.regexMatches?.length
+ # if we are doing a basic plain string match, we still want to grep for matches of the string, so we can
+ # show a the number of results. We can grep on document.body.innerText, as it should be indistinguishable
+ # from the internal representation used by window.find.
+ else
+ # escape all special characters, so RegExp just parses the string 'as is'.
+ # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
+ escapeRegExp = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
+ parsedNonRegexQuery = @query.parsedQuery.replace(escapeRegExp, (char) -> "\\" + char)
+ pattern = new RegExp(parsedNonRegexQuery, "g" + (if @query.ignoreCase then "i" else ""))
+ text = document.body.innerText
+ @query.matchCount = text.match(pattern)?.length
+
+ @getNextQueryFromRegexMatches: (stepSize) ->
+ # find()ing an empty query always returns false
+ return "" unless @query.regexMatches
+
+ totalMatches = @query.regexMatches.length
+ @query.activeRegexIndex += stepSize + totalMatches
+ @query.activeRegexIndex %= totalMatches
+
+ @query.regexMatches[@query.activeRegexIndex]
+
+ @getQuery: (backwards) ->
+ # check if the query has been changed by a script in another frame
+ mostRecentQuery = FindModeHistory.getQuery()
+ if (mostRecentQuery != @query.rawQuery)
+ @updateQuery mostRecentQuery
+
+ if @query.isRegex
+ @getNextQueryFromRegexMatches(if backwards then -1 else 1)
+ else
+ @query.parsedQuery
+
+ @saveQuery: -> FindModeHistory.saveQuery @query.rawQuery
+
+ # :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'.
+ @execute: (query, options) ->
+ result = null
+ options = extend {
+ backwards: false
+ caseSensitive: !@query.ignoreCase
+ colorSelection: true
+ }, options
+ query ?= FindMode.getQuery options.backwards
+
+ if options.colorSelection
+ document.body.classList.add("vimiumFindMode")
+ # ignore the selectionchange event generated by find()
+ document.removeEventListener("selectionchange", @restoreDefaultSelectionHighlight, true)
+
+ result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
+
+ if options.colorSelection
+ setTimeout(
+ -> document.addEventListener("selectionchange", @restoreDefaultSelectionHighlight, true)
+ , 0)
+
+ # We are either in normal mode ("n"), or find mode ("/"). We are not in insert mode. Nevertheless, if a
+ # previous find landed in an editable element, then that element may still be activated. In this case, we
+ # don't want to leave it behind (see #1412).
+ if document.activeElement and DomUtils.isEditable document.activeElement
+ document.activeElement.blur() unless DomUtils.isSelected document.activeElement
+
+ result
+
+ @restoreDefaultSelectionHighlight: -> document.body.classList.remove("vimiumFindMode")
+
+ checkReturnToViewPort: ->
+ window.scrollTo @scrollX, @scrollY if @options.returnToViewport
+
+getCurrentRange = ->
+ selection = getSelection()
+ if selection.type == "None"
+ range = document.createRange()
+ range.setStart document.body, 0
+ range.setEnd document.body, 0
+ range
+ else
+ selection.collapseToStart() if selection.type == "Range"
+ selection.getRangeAt 0
+
root = exports ? window
root.PostFindMode = PostFindMode
+root.FindMode = FindMode
diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee
index 8d1d96cc..ce3caafe 100644
--- a/content_scripts/mode_visual_edit.coffee
+++ b/content_scripts/mode_visual_edit.coffee
@@ -351,22 +351,21 @@ class Movement extends CountPrefix
# element), or if this instance has been created to execute only a single movement.
unless @options.parentMode or options.oneMovementOnly
do =>
- executeFind = (count, findBackwards) =>
- if query = getFindModeQuery findBackwards
- initialRange = @selection.getRangeAt(0).cloneRange()
- for [0...count]
- unless window.find query, Utils.hasUpperCase(query), findBackwards, true, false, true, false
- @setSelectionRange initialRange
- HUD.showForDuration("No matches for '" + query + "'", 1000)
- return
- # The find was successfull. If we're in caret mode, then we should now have a selection, so we can
- # drop back into visual mode.
- @changeMode VisualMode if @name == "caret" and 0 < @selection.toString().length
-
- @movements.n = (count) -> executeFind count, false
- @movements.N = (count) -> executeFind count, true
+ doFind = (count, backwards) =>
+ initialRange = @selection.getRangeAt(0).cloneRange()
+ for [0...count] by 1
+ unless FindMode.execute null, {colorSelection: false, backwards}
+ @setSelectionRange initialRange
+ HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
+ return
+ # The find was successfull. If we're in caret mode, then we should now have a selection, so we can
+ # drop back into visual mode.
+ @changeMode VisualMode if @name == "caret" and 0 < @selection.toString().length
+
+ @movements.n = (count) -> doFind count, false
+ @movements.N = (count) -> doFind count, true
@movements["/"] = ->
- @findMode = window.enterFindMode returnToViewport: true
+ @findMode = new FindMode returnToViewport: true
@findMode.onExit => @changeMode VisualMode
#
# End of Movement constructor.
@@ -375,8 +374,10 @@ class Movement extends CountPrefix
# it.
yank: (args = {}) ->
@yankedText = @selection.toString()
- @selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument
- @selection.collapseToStart() unless @options.parentMode
+ if @options.deleteFromDocument or args.deleteFromDocument
+ @selection.deleteFromDocument()
+ else
+ @selection.collapseToStart()
message = @yankedText.replace /\s+/g, " "
message = message[...12] + "..." if 15 < @yankedText.length
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 38a968fc..e02df7c2 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -284,6 +284,22 @@ iframe.vimiumHUDFrame {
opacity: 0;
}
+div.vimiumHUD span#hud-find-input, div.vimiumHUD span#hud-match-count {
+ display: inline;
+ outline: none;
+ white-space: nowrap;
+ overflow-y: hidden;
+}
+
+div.vimiumHUD span#hud-find-input br {
+ display: none;
+}
+
+div.vimiumHUD span#hud-find-input * {
+ display: inline;
+ white-space: nowrap;
+}
+
body.vimiumFindMode ::selection {
background: #ff9632;
}
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index bffbd457..9d850419 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -5,10 +5,6 @@
# "domReady".
#
-findModeQuery = { rawQuery: "", matchCount: 0 }
-findModeQueryHasResults = false
-findModeAnchorNode = null
-findModeInitialRange = null
isShowingHelpDialog = false
keyPort = null
isEnabledForUrl = true
@@ -433,7 +429,7 @@ extend window,
hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
# Deactivate any active modes on this element (PostFindMode, or a suspended edit mode).
@deactivateSingleton visibleInputs[selectedInputIndex].element
- visibleInputs[selectedInputIndex].element.focus()
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
@suppressEvent
else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
@exit()
@@ -446,7 +442,7 @@ extend window,
# Deactivate any active modes on this element (PostFindMode, or a suspended edit mode).
@deactivateSingleton visibleInputs[selectedInputIndex].element
- visibleInputs[selectedInputIndex].element.focus()
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
if visibleInputs.length == 1
@exit()
return
@@ -622,108 +618,7 @@ window.refreshCompletionKeys = (response) ->
isValidFirstKey = (keyChar) ->
validFirstKeys[keyChar] || /^[1-9]/.test(keyChar)
-# This implements find-mode query history (using the "findModeRawQueryList" setting) as a list of raw queries,
-# most recent first.
-FindModeHistory =
- storage: chrome.storage.local
- key: "findModeRawQueryList"
- max: 50
- rawQueryList: null
-
- init: ->
- unless @rawQueryList
- @rawQueryList = [] # Prevent repeated initialization.
- @key = "findModeRawQueryListIncognito" if isIncognitoMode
- @storage.get @key, (items) =>
- unless chrome.runtime.lastError
- @rawQueryList = items[@key] if items[@key]
- if isIncognitoMode and not items[@key]
- # This is the first incognito tab, so we need to initialize the incognito-mode query history.
- @storage.get "findModeRawQueryList", (items) =>
- unless chrome.runtime.lastError
- @rawQueryList = items.findModeRawQueryList
- @storage.set findModeRawQueryListIncognito: @rawQueryList
-
- chrome.storage.onChanged.addListener (changes, area) =>
- @rawQueryList = changes[@key].newValue if changes[@key]
-
- getQuery: (index = 0) ->
- @rawQueryList[index] or ""
-
- saveQuery: (query) ->
- if 0 < query.length
- @rawQueryList = @refreshRawQueryList query, @rawQueryList
- newSetting = {}; newSetting[@key] = @rawQueryList
- @storage.set newSetting
- # If there are any active incognito-mode tabs, then propagte this query to those tabs too.
- unless isIncognitoMode
- @storage.get "findModeRawQueryListIncognito", (items) =>
- if not chrome.runtime.lastError and items.findModeRawQueryListIncognito
- @storage.set
- findModeRawQueryListIncognito: @refreshRawQueryList query, items.findModeRawQueryListIncognito
-
- refreshRawQueryList: (query, rawQueryList) ->
- ([ query ].concat rawQueryList.filter (q) => q != query)[0..@max]
-
-# should be called whenever rawQuery is modified.
-updateFindModeQuery = ->
- # the query can be treated differently (e.g. as a plain string versus regex depending on the presence of
- # escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal
- # character. here we grep for the relevant escape sequences.
- findModeQuery.isRegex = Settings.get 'regexFindMode'
- hasNoIgnoreCaseFlag = false
- findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /(\\{1,2})([rRI]?)/g, (match, slashes, flag) ->
- return match if flag == "" or slashes.length != 1
- switch (flag)
- when "r"
- findModeQuery.isRegex = true
- when "R"
- findModeQuery.isRegex = false
- when "I"
- hasNoIgnoreCaseFlag = true
- ""
-
- # default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
- findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !Utils.hasUpperCase(findModeQuery.parsedQuery)
-
- # Don't count matches in the HUD.
- HUD.hide(true)
-
- # if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them
- # sequentially so the browser handles the scrolling / text selection.
- if findModeQuery.isRegex
- try
- pattern = new RegExp(findModeQuery.parsedQuery, "g" + (if findModeQuery.ignoreCase then "i" else ""))
- catch error
- # if we catch a SyntaxError, assume the user is not done typing yet and return quietly
- return
- # innerText will not return the text of hidden elements, and strip out tags while preserving newlines
- text = document.body.innerText
- findModeQuery.regexMatches = text.match(pattern)
- findModeQuery.activeRegexIndex = 0
- findModeQuery.matchCount = findModeQuery.regexMatches?.length
- # if we are doing a basic plain string match, we still want to grep for matches of the string, so we can
- # show a the number of results. We can grep on document.body.innerText, as it should be indistinguishable
- # from the internal representation used by window.find.
- else
- # escape all special characters, so RegExp just parses the string 'as is'.
- # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
- escapeRegExp = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
- parsedNonRegexQuery = findModeQuery.parsedQuery.replace(escapeRegExp, (char) -> "\\" + char)
- pattern = new RegExp(parsedNonRegexQuery, "g" + (if findModeQuery.ignoreCase then "i" else ""))
- text = document.body.innerText
- findModeQuery.matchCount = text.match(pattern)?.length
-
-updateQueryForFindMode = (rawQuery) ->
- findModeQuery.rawQuery = rawQuery
- updateFindModeQuery()
- performFindInPlace()
- showFindModeHUDForQuery()
-
-handleKeyCharForFindMode = (keyChar) ->
- updateQueryForFindMode findModeQuery.rawQuery + keyChar
-
-handleEscapeForFindMode = ->
+window.handleEscapeForFindMode = ->
document.body.classList.remove("vimiumFindMode")
# removing the class does not re-color existing selections. we recreate the current selection so it reverts
# back to the default color.
@@ -734,158 +629,38 @@ handleEscapeForFindMode = ->
window.getSelection().addRange(range)
focusFoundLink() || selectFoundInputElement()
-# Return true if character deleted, false otherwise.
-handleDeleteForFindMode = ->
- if findModeQuery.rawQuery.length == 0
- HUD.hide()
- false
- else
- updateQueryForFindMode findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1)
- true
-
# <esc> sends us into insert mode if possible, but <cr> does not.
# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save
# this query and do more searches with it'
-handleEnterForFindMode = ->
+window.handleEnterForFindMode = ->
focusFoundLink()
document.body.classList.add("vimiumFindMode")
- FindModeHistory.saveQuery findModeQuery.rawQuery
-
-class FindMode extends Mode
- constructor: (options = {}) ->
- @historyIndex = -1
- @partialQuery = ""
- if options.returnToViewport
- @scrollX = window.scrollX
- @scrollY = window.scrollY
- super
- name: "find"
- indicator: false
- exitOnEscape: true
- exitOnClick: true
-
- keydown: (event) =>
- window.scrollTo @scrollX, @scrollY if options.returnToViewport
- if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
- @exit() unless handleDeleteForFindMode()
- @suppressEvent
- else if event.keyCode == keyCodes.enter
- handleEnterForFindMode()
- @exit()
- @suppressEvent
- else if event.keyCode == keyCodes.upArrow
- if rawQuery = FindModeHistory.getQuery @historyIndex + 1
- @historyIndex += 1
- @partialQuery = findModeQuery.rawQuery if @historyIndex == 0
- updateQueryForFindMode rawQuery
- @suppressEvent
- else if event.keyCode == keyCodes.downArrow
- @historyIndex = Math.max -1, @historyIndex - 1
- rawQuery = if 0 <= @historyIndex then FindModeHistory.getQuery @historyIndex else @partialQuery
- updateQueryForFindMode rawQuery
- @suppressEvent
- else
- DomUtils.suppressPropagation(event)
- handlerStack.stopBubblingAndFalse
-
- keypress: (event) =>
- handlerStack.neverContinueBubbling =>
- if event.keyCode > 31
- keyChar = String.fromCharCode event.charCode
- handleKeyCharForFindMode keyChar if keyChar
-
- keyup: (event) => @suppressEvent
-
- exit: (event) ->
- super()
- handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event
- handleEscapeForFindMode() if event?.type == "click"
- if findModeQueryHasResults and event?.type != "click"
- new PostFindMode
-
-performFindInPlace = ->
- # Restore the selection. That way, we're always searching forward from the same place, so we find the right
- # match as the user adds matching characters, or removes previously-matched characters. See #1434.
- findModeRestoreSelection()
- query = if findModeQuery.isRegex then getNextQueryFromRegexMatches(0) else findModeQuery.parsedQuery
- findModeQueryHasResults = executeFind(query, { caseSensitive: !findModeQuery.ignoreCase })
-
-# :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'.
-executeFind = (query, options) ->
- result = null
- options = options || {}
-
- document.body.classList.add("vimiumFindMode")
-
- # ignore the selectionchange event generated by find()
- document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true)
- result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
- setTimeout(
- -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true)
- 0)
-
- # We are either in normal mode ("n"), or find mode ("/"). We are not in insert mode. Nevertheless, if a
- # previous find landed in an editable element, then that element may still be activated. In this case, we
- # don't want to leave it behind (see #1412).
- if document.activeElement and DomUtils.isEditable document.activeElement
- document.activeElement.blur() unless DomUtils.isSelected document.activeElement
-
- # we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do
- # preventDefault()
- findModeAnchorNode = document.getSelection().anchorNode
- result
-
-restoreDefaultSelectionHighlight = -> document.body.classList.remove("vimiumFindMode")
+ FindMode.saveQuery()
focusFoundLink = ->
- if (findModeQueryHasResults)
+ if (FindMode.query.hasResults)
link = getLinkFromSelection()
link.focus() if link
selectFoundInputElement = ->
- # if the found text is in an input element, getSelection().anchorNode will be null, so we use activeElement
- # instead. however, since the last focused element might not be the one currently pointed to by find (e.g.
- # the current one might be disabled and therefore unable to receive focus), we use the approximate
- # heuristic of checking that the last anchor node is an ancestor of our element.
- if (findModeQueryHasResults && document.activeElement &&
+ # Since the last focused element might not be the one currently pointed to by find (e.g. the current one
+ # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking
+ # that the last anchor node is an ancestor of our element.
+ findModeAnchorNode = document.getSelection().anchorNode
+ if (FindMode.query.hasResults && document.activeElement &&
DomUtils.isSelectable(document.activeElement) &&
DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
DomUtils.simulateSelect(document.activeElement)
-getNextQueryFromRegexMatches = (stepSize) ->
- # find()ing an empty query always returns false
- return "" unless findModeQuery.regexMatches
-
- totalMatches = findModeQuery.regexMatches.length
- findModeQuery.activeRegexIndex += stepSize + totalMatches
- findModeQuery.activeRegexIndex %= totalMatches
-
- findModeQuery.regexMatches[findModeQuery.activeRegexIndex]
-
-window.getFindModeQuery = (backwards) ->
- # check if the query has been changed by a script in another frame
- mostRecentQuery = FindModeHistory.getQuery()
- if (mostRecentQuery != findModeQuery.rawQuery)
- findModeQuery.rawQuery = mostRecentQuery
- updateFindModeQuery()
-
- if findModeQuery.isRegex
- getNextQueryFromRegexMatches(if backwards then -1 else 1)
- else
- findModeQuery.parsedQuery
-
findAndFocus = (backwards) ->
Marks.setPreviousPosition()
- query = getFindModeQuery backwards
-
- findModeQueryHasResults =
- executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase })
+ FindMode.query.hasResults = FindMode.execute null, {backwards}
- if findModeQueryHasResults
+ if FindMode.query.hasResults
focusFoundLink()
- new PostFindMode() if findModeQueryHasResults
+ new PostFindMode()
else
- HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000)
+ HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
window.performFind = -> findAndFocus()
@@ -992,43 +767,10 @@ window.goNext = ->
nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )
findAndFollowRel("next") || findAndFollowLink(nextStrings)
-showFindModeHUDForQuery = ->
- if findModeQuery.rawQuery and (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
- plural = if findModeQuery.matchCount == 1 then "" else "es"
- HUD.show("/" + findModeQuery.rawQuery + " (" + findModeQuery.matchCount + " Match#{plural})")
- else if findModeQuery.rawQuery
- HUD.show("/" + findModeQuery.rawQuery + " (No Matches)")
- else
- HUD.show("/")
-
-getCurrentRange = ->
- selection = getSelection()
- if selection.type == "None"
- range = document.createRange()
- range.setStart document.body, 0
- range.setEnd document.body, 0
- range
- else
- selection.collapseToStart() if selection.type == "Range"
- selection.getRangeAt 0
-
-findModeSaveSelection = ->
- findModeInitialRange = getCurrentRange()
-
-findModeRestoreSelection = (range = findModeInitialRange) ->
- selection = getSelection()
- selection.removeAllRanges()
- selection.addRange range
-
# Enters find mode. Returns the new find-mode instance.
-window.enterFindMode = (options = {}) ->
+window.enterFindMode = ->
Marks.setPreviousPosition()
- # Save the selection, so performFindInPlace can restore it.
- findModeSaveSelection()
- findModeQuery = rawQuery: ""
- findMode = new FindMode options
- HUD.show "/"
- findMode
+ new FindMode()
window.showHelpDialog = (html, fid) ->
return if (isShowingHelpDialog || !document.body || fid != frameId)
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 7c47179c..9658df2b 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -326,6 +326,18 @@ DomUtils =
document.body.removeChild div
coordinates
+ getSelectionFocusElement: ->
+ sel = window.getSelection()
+ if not sel.focusNode?
+ null
+ else if sel.focusNode == sel.anchorNode and sel.focusOffset == sel.anchorOffset
+ # The selection either *is* an element, or is inside an opaque element (eg. <input>).
+ sel.focusNode.childNodes[sel.focusOffset]
+ else if sel.focusNode.nodeType != sel.focusNode.ELEMENT_NODE
+ sel.focusNode.parentElement
+ else
+ sel.focusNode
+
# Get the text content of an element (and its descendents), but omit the text content of previously-visited
# nodes. See #1514.
# NOTE(smblott). This is currently O(N^2) (when called on N elements). An alternative would be to mark
diff --git a/lib/find_mode_history.coffee b/lib/find_mode_history.coffee
new file mode 100644
index 00000000..ff660bd2
--- /dev/null
+++ b/lib/find_mode_history.coffee
@@ -0,0 +1,50 @@
+# NOTE(mrmr1993): This is under lib/ since it is used by both content scripts and iframes from pages/.
+# This implements find-mode query history (using the "findModeRawQueryList" setting) as a list of raw queries,
+# most recent first.
+FindModeHistory =
+ storage: chrome?.storage.local # Guard against chrome being undefined (in the HUD iframe).
+ key: "findModeRawQueryList"
+ max: 50
+ rawQueryList: null
+
+ init: ->
+ @isIncognitoMode = chrome?.extension.inIncognitoContext
+
+ return unless @isIncognitoMode? # chrome is undefined in the HUD iframe during tests, so we do nothing.
+
+ unless @rawQueryList
+ @rawQueryList = [] # Prevent repeated initialization.
+ @key = "findModeRawQueryListIncognito" if @isIncognitoMode
+ @storage.get @key, (items) =>
+ unless chrome.runtime.lastError
+ @rawQueryList = items[@key] if items[@key]
+ if @isIncognitoMode and not items[@key]
+ # This is the first incognito tab, so we need to initialize the incognito-mode query history.
+ @storage.get "findModeRawQueryList", (items) =>
+ unless chrome.runtime.lastError
+ @rawQueryList = items.findModeRawQueryList
+ @storage.set findModeRawQueryListIncognito: @rawQueryList
+
+ chrome.storage.onChanged.addListener (changes, area) =>
+ @rawQueryList = changes[@key].newValue if changes[@key]
+
+ getQuery: (index = 0) ->
+ @rawQueryList[index] or ""
+
+ saveQuery: (query) ->
+ if 0 < query.length
+ @rawQueryList = @refreshRawQueryList query, @rawQueryList
+ newSetting = {}; newSetting[@key] = @rawQueryList
+ @storage.set newSetting
+ # If there are any active incognito-mode tabs, then propagte this query to those tabs too.
+ unless @isIncognitoMode
+ @storage.get "findModeRawQueryListIncognito", (items) =>
+ if not chrome.runtime.lastError and items.findModeRawQueryListIncognito
+ @storage.set
+ findModeRawQueryListIncognito: @refreshRawQueryList query, items.findModeRawQueryListIncognito
+
+ refreshRawQueryList: (query, rawQueryList) ->
+ ([ query ].concat rawQueryList.filter (q) => q != query)[0..@max]
+
+root = exports ? window
+root.FindModeHistory = FindModeHistory
diff --git a/lib/settings.coffee b/lib/settings.coffee
index 842f7618..99a20963 100644
--- a/lib/settings.coffee
+++ b/lib/settings.coffee
@@ -1,5 +1,17 @@
+# A "setting" is a stored key/value pair. An "option" is a setting which has a default value and whose value
+# can be changed on the options page.
+#
+# Option values which have never been changed by the user are in Settings.defaults.
+#
+# Settings whose values have been changed are:
+# 1. stored either in chrome.storage.sync or in chrome.storage.local (but never both), and
+# 2. cached in Settings.cache; on extension pages, Settings.cache uses localStorage (so it persists).
+#
+# In all cases except Settings.defaults, values are stored as jsonified strings.
+
Settings =
+ debug: false
storage: chrome.storage.sync
cache: {}
isLoaded: false
@@ -11,18 +23,21 @@ Settings =
@cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage
@onLoaded()
- @storage.get null, (items) =>
- unless chrome.runtime.lastError
- @handleUpdateFromChromeStorage key, value for own key, value of items
+ chrome.storage.local.get null, (localItems) =>
+ localItems = {} if chrome.runtime.lastError
+ @storage.get null, (syncedItems) =>
+ unless chrome.runtime.lastError
+ @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems
- chrome.storage.onChanged.addListener (changes, area) =>
- @propagateChangesFromChromeStorage changes if area == "sync"
+ chrome.storage.onChanged.addListener (changes, area) =>
+ @propagateChangesFromChromeStorage changes if area == "sync"
- @onLoaded()
+ @onLoaded()
# Called after @cache has been initialized. On extension pages, this will be called twice, but that does
# not matter because it's idempotent.
onLoaded: ->
+ @log "onLoaded: #{@onLoadedCallbacks.length} callback(s)"
@isLoaded = true
callback() while callback = @onLoadedCallbacks.pop()
@@ -33,46 +48,40 @@ Settings =
@handleUpdateFromChromeStorage key, change?.newValue for own key, change of changes
handleUpdateFromChromeStorage: (key, value) ->
+ @log "handleUpdateFromChromeStorage: #{key}"
# Note: value here is either null or a JSONified string. Therefore, even falsy settings values (like
# false, 0 or "") are truthy here. Only null is falsy.
if @shouldSyncKey key
unless value and key of @cache and @cache[key] == value
- defaultValue = @defaults[key]
- defaultValueJSON = JSON.stringify defaultValue
-
- if value and value != defaultValueJSON
- # Key/value has been changed to a non-default value.
- @cache[key] = value
- @performPostUpdateHook key, JSON.parse value
- else
- # The key has been reset to its default value.
- delete @cache[key] if key of @cache
- @performPostUpdateHook key, defaultValue
+ value ?= JSON.stringify @defaults[key]
+ @set key, JSON.parse(value), false
get: (key) ->
console.log "WARNING: Settings have not loaded yet; using the default value for #{key}." unless @isLoaded
if key of @cache and @cache[key]? then JSON.parse @cache[key] else @defaults[key]
- set: (key, value) ->
- # Don't store the value if it is equal to the default, so we can change the defaults in the future.
- if JSON.stringify(value) == JSON.stringify @defaults[key]
- @clear key
- else
- jsonValue = JSON.stringify value
- @cache[key] = jsonValue
- if @shouldSyncKey key
- setting = {}; setting[key] = jsonValue
+ set: (key, value, shouldSetInSyncedStorage = true) ->
+ @cache[key] = JSON.stringify value
+ @log "set: #{key} (length=#{@cache[key].length}, shouldSetInSyncedStorage=#{shouldSetInSyncedStorage})"
+ if @shouldSyncKey key
+ if shouldSetInSyncedStorage
+ setting = {}; setting[key] = @cache[key]
+ @log " chrome.storage.sync.set(#{key})"
@storage.set setting
- @performPostUpdateHook key, value
+ if Utils.isBackgroundPage()
+ # Remove options installed by the "copyNonDefaultsToChromeStorage-20150717" migration; see below.
+ @log " chrome.storage.local.remove(#{key})"
+ chrome.storage.local.remove key
+ @performPostUpdateHook key, value
clear: (key) ->
- delete @cache[key] if @has key
- @storage.remove key if @shouldSyncKey key
- @performPostUpdateHook key, @get key
+ @log "clear: #{key}"
+ @set key, @defaults[key]
has: (key) -> key of @cache
use: (key, callback) ->
+ @log "use: #{key} (isLoaded=#{@isLoaded})"
invokeCallback = => callback @get key
if @isLoaded then invokeCallback() else @onLoadedCallbacks.push invokeCallback
@@ -80,6 +89,10 @@ Settings =
postUpdateHooks: {}
performPostUpdateHook: (key, value) -> @postUpdateHooks[key]? value
+ # For development only.
+ log: (args...) ->
+ console.log "settings:", args... if @debug
+
# Default values for all settings.
defaults:
scrollStepSize: 60
@@ -150,6 +163,7 @@ Settings =
settingsVersion: Utils.getCurrentVersion()
helpDialog_showAdvancedCommands: false
+ optionsPage_showAdvancedOptions: false
Settings.init()
@@ -169,5 +183,19 @@ if Utils.isBackgroundPage()
rawQuery = Settings.get "findModeRawQuery"
chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])
+ # Migration (after 1.51, 2015/6/17).
+ # Copy options with non-default values (and which are not in synced storage) to chrome.storage.local;
+ # thereby making these settings accessible within content scripts.
+ do (migrationKey = "copyNonDefaultsToChromeStorage-20150717") ->
+ unless localStorage[migrationKey]
+ chrome.storage.sync.get null, (items) ->
+ unless chrome.runtime.lastError
+ updates = {}
+ for own key of localStorage
+ if Settings.shouldSyncKey(key) and not items[key]
+ updates[key] = localStorage[key]
+ chrome.storage.local.set updates, ->
+ localStorage[migrationKey] = not chrome.runtime.lastError
+
root = exports ? window
root.Settings = Settings
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 93045f32..d4beff03 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -323,10 +323,11 @@ class SimpleCache
null
rotate: (force = false) ->
- if force or @entries < Object.keys(@cache).length or @expiry < new Date() - @lastRotation
- @lastRotation = new Date()
- @previous = @cache
- @cache = {}
+ Utils.nextTick =>
+ if force or @entries < Object.keys(@cache).length or @expiry < new Date() - @lastRotation
+ @lastRotation = new Date()
+ @previous = @cache
+ @cache = {}
clear: ->
@rotate true
diff --git a/manifest.json b/manifest.json
index 80aca4c5..4ef5edfe 100644
--- a/manifest.json
+++ b/manifest.json
@@ -42,6 +42,7 @@
"lib/handler_stack.js",
"lib/clipboard.js",
"lib/settings.js",
+ "lib/find_mode_history.js",
"content_scripts/ui_component.js",
"content_scripts/link_hints.js",
"content_scripts/vomnibar.js",
diff --git a/pages/hud.coffee b/pages/hud.coffee
index 68283451..37debc4e 100644
--- a/pages/hud.coffee
+++ b/pages/hud.coffee
@@ -1,3 +1,49 @@
+findMode = null
+
+# Set the input element's text, and move the cursor to the end.
+setTextInInputElement = (inputElement, text) ->
+ inputElement.textContent = text
+ # Move the cursor to the end. Based on one of the solutions here:
+ # http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
+ range = document.createRange()
+ range.selectNodeContents inputElement
+ range.collapse false
+ selection = window.getSelection()
+ selection.removeAllRanges()
+ selection.addRange range
+
+document.addEventListener "keydown", (event) ->
+ inputElement = document.getElementById "hud-find-input"
+ return unless inputElement? # Don't do anything if we're not in find mode.
+ transferrableEvent = {}
+ for key, value of event
+ transferrableEvent[key] = value if typeof value in ["number", "string"]
+
+ if (event.keyCode in [keyCodes.backspace, keyCodes.deleteKey] and inputElement.textContent.length == 0) or
+ event.keyCode == keyCodes.enter or KeyboardUtils.isEscape event
+
+ UIComponentServer.postMessage
+ name: "hideFindMode"
+ event: transferrableEvent
+ query: findMode.rawQuery
+
+ else if event.keyCode == keyCodes.upArrow
+ if rawQuery = FindModeHistory.getQuery findMode.historyIndex + 1
+ findMode.historyIndex += 1
+ findMode.partialQuery = findMode.rawQuery if findMode.historyIndex == 0
+ setTextInInputElement inputElement, rawQuery
+ findMode.executeQuery()
+ else if event.keyCode == keyCodes.downArrow
+ findMode.historyIndex = Math.max -1, findMode.historyIndex - 1
+ rawQuery = if 0 <= findMode.historyIndex then FindModeHistory.getQuery findMode.historyIndex else findMode.partialQuery
+ setTextInInputElement inputElement, rawQuery
+ findMode.executeQuery()
+ else
+ return
+
+ DomUtils.suppressEvent event
+ false
+
handlers =
show: (data) ->
document.getElementById("hud").innerText = data.text
@@ -10,6 +56,47 @@ handlers =
document.getElementById("hud").classList.add "vimiumUIComponentHidden"
document.getElementById("hud").classList.remove "vimiumUIComponentVisible"
+ showFindMode: (data) ->
+ hud = document.getElementById "hud"
+ hud.innerText = "/\u200A" # \u200A is a "hair space", to leave enough space before the caret/first char.
+
+ inputElement = document.createElement "span"
+ inputElement.contentEditable = "plaintext-only"
+ setTextInInputElement inputElement, data.text ? ""
+ inputElement.id = "hud-find-input"
+ hud.appendChild inputElement
+
+ inputElement.addEventListener "input", executeQuery = (event) ->
+ # Replace \u00A0 (&nbsp;) with a normal space.
+ findMode.rawQuery = inputElement.textContent.replace "\u00A0", " "
+ UIComponentServer.postMessage {name: "search", query: findMode.rawQuery}
+
+ countElement = document.createElement "span"
+ countElement.id = "hud-match-count"
+ hud.appendChild countElement
+ inputElement.focus()
+
+ # Replace \u00A0 (&nbsp;) with a normal space.
+ UIComponentServer.postMessage {name: "search", query: inputElement.textContent.replace "\u00A0", " "}
+
+ findMode =
+ historyIndex: -1
+ partialQuery: ""
+ rawQuery: ""
+ executeQuery: executeQuery
+
+ updateMatchesCount: ({matchCount, showMatchText}) ->
+ countElement = document.getElementById "hud-match-count"
+ return unless countElement? # Don't do anything if we're not in find mode.
+
+ countText = if matchCount > 0
+ " (#{matchCount} Match#{if matchCount == 1 then "" else "es"})"
+ else
+ " (No matches)"
+ countElement.textContent = if showMatchText then countText else ""
+
UIComponentServer.registerHandler (event) ->
{data} = event
handlers[data.name]? data
+
+FindModeHistory.init()
diff --git a/pages/hud.html b/pages/hud.html
index bcb38e04..60d737e1 100644
--- a/pages/hud.html
+++ b/pages/hud.html
@@ -2,6 +2,9 @@
<head>
<title>HUD</title>
<link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+ <script type="text/javascript" src="../lib/dom_utils.js"></script>
+ <script type="text/javascript" src="../lib/keyboard_utils.js"></script>
+ <script type="text/javascript" src="../lib/find_mode_history.js"></script>
<script type="text/javascript" src="ui_component_server.js"></script>
<script type="text/javascript" src="hud.js"></script>
</head>
diff --git a/pages/options.coffee b/pages/options.coffee
index 21e81c8f..1cbe88fa 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -181,6 +181,24 @@ class ExclusionRulesOnPopupOption extends ExclusionRulesOption
else
@url + "*"
+Options =
+ exclusionRules: ExclusionRulesOption
+ filterLinkHints: CheckBoxOption
+ hideHud: CheckBoxOption
+ keyMappings: TextOption
+ linkHintCharacters: NonEmptyTextOption
+ linkHintNumbers: NonEmptyTextOption
+ newTabUrl: NonEmptyTextOption
+ nextPatterns: NonEmptyTextOption
+ previousPatterns: NonEmptyTextOption
+ regexFindMode: CheckBoxOption
+ scrollStepSize: NumberOption
+ smoothScroll: CheckBoxOption
+ grabBackFocus: CheckBoxOption
+ searchEngines: TextOption
+ searchUrl: NonEmptyTextOption
+ userDefinedLinkHintCss: TextOption
+
initOptionsPage = ->
onUpdated = ->
$("saveOptions").removeAttribute "disabled"
@@ -197,18 +215,20 @@ initOptionsPage = ->
show $("linkHintCharacters")
hide $("linkHintNumbers")
- toggleAdvancedOptions =
- do (advancedMode=false) ->
- (event) ->
- if advancedMode
- $("advancedOptions").style.display = "none"
- $("advancedOptionsButton").innerHTML = "Show Advanced Options"
- else
- $("advancedOptions").style.display = "table-row-group"
- $("advancedOptionsButton").innerHTML = "Hide Advanced Options"
- advancedMode = !advancedMode
- $("advancedOptionsButton").blur()
- event.preventDefault()
+ maintainAdvancedOptions = ->
+ if bgSettings.get "optionsPage_showAdvancedOptions"
+ $("advancedOptions").style.display = "table-row-group"
+ $("advancedOptionsButton").innerHTML = "Hide Advanced Options"
+ else
+ $("advancedOptions").style.display = "none"
+ $("advancedOptionsButton").innerHTML = "Show Advanced Options"
+ maintainAdvancedOptions()
+
+ toggleAdvancedOptions = (event) ->
+ bgSettings.set "optionsPage_showAdvancedOptions", not bgSettings.get "optionsPage_showAdvancedOptions"
+ maintainAdvancedOptions()
+ $("advancedOptionsButton").blur()
+ event.preventDefault()
activateHelpDialog = ->
showHelpDialog chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId
@@ -236,26 +256,8 @@ initOptionsPage = ->
document.activeElement.blur() if document?.activeElement?.blur
saveOptions()
- options =
- exclusionRules: ExclusionRulesOption
- filterLinkHints: CheckBoxOption
- hideHud: CheckBoxOption
- keyMappings: TextOption
- linkHintCharacters: NonEmptyTextOption
- linkHintNumbers: NonEmptyTextOption
- newTabUrl: NonEmptyTextOption
- nextPatterns: NonEmptyTextOption
- previousPatterns: NonEmptyTextOption
- regexFindMode: CheckBoxOption
- scrollStepSize: NumberOption
- smoothScroll: CheckBoxOption
- grabBackFocus: CheckBoxOption
- searchEngines: TextOption
- searchUrl: NonEmptyTextOption
- userDefinedLinkHintCss: TextOption
-
# Populate options. The constructor adds each new object to "Option.all".
- for name, type of options
+ for name, type of Options
new type(name,onUpdated)
maintainLinkHintsView()
@@ -317,3 +319,6 @@ document.addEventListener "DOMContentLoaded", ->
xhr.send()
+# Exported for tests.
+root = exports ? window
+root.Options = Options
diff --git a/pages/options.html b/pages/options.html
index 12a3ad21..22b041b7 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -283,7 +283,7 @@ b: http://b.com/?q=%s description
</span>
</td>
<td id="saveOptionsTableData" nowrap>
- <button id="advancedOptionsButton">Show Advanced Options</button>
+ <button id="advancedOptionsButton"></button>
<button id="saveOptions" disabled="true">No Changes</button>
</td>
</tr>
diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html
index f7cc430d..25c5f8ba 100644
--- a/tests/dom_tests/dom_tests.html
+++ b/tests/dom_tests/dom_tests.html
@@ -36,6 +36,7 @@
<script type="text/javascript" src="../../lib/handler_stack.js"></script>
<script type="text/javascript" src="../../lib/clipboard.js"></script>
<script type="text/javascript" src="../../lib/settings.js"></script>
+ <script type="text/javascript" src="../../lib/find_mode_history.js"></script>
<script type="text/javascript" src="../../content_scripts/ui_component.js"></script>
<script type="text/javascript" src="../../content_scripts/link_hints.js"></script>
<script type="text/javascript" src="../../content_scripts/vomnibar.js"></script>
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index 4a0cf746..9ce0a466 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -4,9 +4,6 @@ extend(global, require "../../background_scripts/completion_engines.js")
extend(global, require "../../background_scripts/completion.js")
extend global, require "./test_chrome_stubs.js"
-global.document =
- createElement: -> {}
-
context "bookmark completer",
setup ->
@bookmark3 = { title: "bookmark3", url: "bookmark3.com" }
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index 08145190..9dc25cef 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -7,6 +7,7 @@ Utils.isBackgroundPage = -> true
Utils.isExtensionPage = -> true
global.localStorage = {}
extend(global,require "../../lib/settings.js")
+extend(global,require "../../pages/options.js")
context "settings",
@@ -27,12 +28,6 @@ context "settings",
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
- should "not store values equal to the default", ->
- Settings.set 'scrollStepSize', 20
- assert.isTrue Settings.has 'scrollStepSize'
- Settings.set 'scrollStepSize', 60
- assert.isFalse Settings.has 'scrollStepSize'
-
should "revert to defaults if no key is stored", ->
Settings.set 'scrollStepSize', 20
Settings.clear 'scrollStepSize'
@@ -55,7 +50,7 @@ context "synced settings",
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
Settings.propagateChangesFromChromeStorage { scrollStepSize: { newValue: "60" } }
- assert.isFalse Settings.has 'scrollStepSize'
+ assert.equal Settings.get('scrollStepSize'), 60
should "propagate non-default values from synced storage", ->
chrome.storage.sync.set { scrollStepSize: JSON.stringify(20) }
@@ -64,12 +59,12 @@ context "synced settings",
should "propagate default values from synced storage", ->
Settings.set 'scrollStepSize', 20
chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) }
- assert.isFalse Settings.has 'scrollStepSize'
+ assert.equal Settings.get('scrollStepSize'), 60
should "clear a setting from synced storage", ->
Settings.set 'scrollStepSize', 20
chrome.storage.sync.remove 'scrollStepSize'
- assert.isFalse Settings.has 'scrollStepSize'
+ assert.equal Settings.get('scrollStepSize'), 60
should "trigger a postUpdateHook", ->
message = "Hello World"
@@ -80,3 +75,10 @@ context "synced settings",
should "sync a key which is not a known setting (without crashing)", ->
chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") }
+
+context "default valuess",
+
+ should "have a default value for every option", ->
+ for own key of Options
+ assert.isTrue key of Settings.defaults
+
diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee
index fe2fc298..0bb6ed81 100644
--- a/tests/unit_tests/test_chrome_stubs.coffee
+++ b/tests/unit_tests/test_chrome_stubs.coffee
@@ -8,6 +8,10 @@
exports.window = {}
exports.localStorage = {}
+global.document =
+ createElement: -> {}
+ addEventListener: ->
+
exports.chrome =
runtime:
getManifest: () ->
@@ -21,6 +25,7 @@ exports.chrome =
extension:
getURL: (path) -> path
+ getBackgroundPage: -> {}
tabs:
onSelectionChanged:
@@ -57,9 +62,9 @@ exports.chrome =
storage:
# chrome.storage.local
local:
- get: ->
- set: ->
- remove: ->
+ get: (_, callback) -> callback?()
+ set: (_, callback) -> callback?()
+ remove: (_, callback) -> callback?()
# chrome.storage.onChanged
onChanged: