diff options
25 files changed, 450 insertions, 399 deletions
@@ -41,7 +41,7 @@ Navigating the current page: F open a link in a new tab r reload gs view source - i enter insert mode -- all commands will be ignored until you hit esc to exit + i enter insert mode -- all commands will be ignored until you hit Esc to exit yy copy the current url to the clipboard yf copy a link url to the clipboard gf cycle forward to the next frame @@ -56,11 +56,13 @@ Navigating to new pages: Using find: - / enter find mode -- type your search query and hit enter to search or esc to cancel - See here for advanced usage (regular expressions): https://github.com/philc/vimium/wiki/Find-Mode + / enter find mode + -- type your search query and hit enter to search, or Esc to cancel n cycle forward to the next find match N cycle backward to the previous find match +For advanced usage, see [regular expressions](https://github.com/philc/vimium/wiki/Find-Mode) on the wiki. + Navigating your history: H go back in history @@ -68,21 +70,28 @@ Navigating your history: Manipulating tabs: - J, gT go one tab left - K, gt go one tab right - g0 go to the first tab - g$ go to the last tab - t create tab - yt duplicate current tab - x close current tab - X restore closed tab (i.e. unwind the 'x' command) - T search through your open tabs - <a-p> pin/unpin current tab + J, gT go one tab left + K, gt go one tab right + g0 go to the first tab + g$ go to the last tab + t create tab + yt duplicate current tab + x close current tab + X restore closed tab (i.e. unwind the 'x' command) + T search through your open tabs + <a-p> pin/unpin current tab + +Using marks: + + ma, mA set local mark "a" (global mark "A") + `a, `A jump to local mark "a" (global mark "A") + `` jump back to the position before the previous jump + -- that is, before the previous gg, G, n, N, / or `a Additional advanced browsing commands: - ]] Follow the link labeled 'next' or '>'. Helpful for browsing paginated sites. - [[ Follow the link labeled 'previous' or '<'. Helpful for browsing paginated sites. + ]], [[ Follow the link labeled 'next' or '>' ('previous' or '<') + - helpful for browsing paginated sites <a-f> open multiple links in a new tab gi focus the first (or n-th) text input box on the page gu go up one level in the URL hierarchy @@ -92,7 +101,7 @@ Additional advanced browsing commands: v enter visual mode; use p/P to paste-and-go, use y to yank V enter visual line mode -Vimium supports command repetition so, for example, hitting `5t` will open 5 tabs in rapid succession. `<ESC>` (or +Vimium supports command repetition so, for example, hitting `5t` will open 5 tabs in rapid succession. `<Esc>` (or `<c-[>`) will clear any partial commands in the queue and will also exit insert and find modes. There are some advanced commands which aren't documented here; refer to the help dialog (type `?`) for a full @@ -101,8 +110,7 @@ list. Custom Key Mappings ------------------- -You may remap or unmap any of the default key bindings in the "Key mappings" section under "Advanced Options" -on the options page. +You may remap or unmap any of the default key bindings in the "Custom key mappings" on the options page. Enter one of the following key mapping commands per line: @@ -119,10 +127,11 @@ Examples: - `unmap <c-d>` removes any mapping for ctrl+d and restores Chrome's default behavior. - `unmap r` removes any mapping for the r key. -Available Vimium commands can be found via the "Show Available Commands" link near the key mapping box. The -command name appears to the right of the description in parenthesis. +Available Vimium commands can be found via the "Show available commands" link +near the key mapping box on the options page. The command name appears to the +right of the description in parenthesis. -You can add comments to your key mappings by starting a line with `"` or `#`. +You can add comments to key mappings by starting a line with `"` or `#`. The following special keys are available for mapping: @@ -150,7 +159,13 @@ Release Notes ([here](https://github.com/philc/vimium/wiki/Search-Completion) and [here](https://github.com/philc/vimium/wiki/Tips-and-Tricks#repeat-recent-queries)). - A much improved interface for custom search engines. -- Bug fixes: bookmarklets accessed from the vomnibar. +- Added <tt>\`\`</tt> to jump back to the previous position after selected + jump-like movements (`gg`, `G`, `/`, `n`, `N` and local mark movements). +- Global marks are now persistent across tab closes and browser sessions, and + are synced between browser instances. +- Bug fixes, including: + - Bookmarklets accessed from the vomnibar. + - Global marks on non-Windows platforms. 1.51 (2015-05-02) @@ -373,7 +388,7 @@ does not support command repetition. - Bug fixes related to entering insert mode when the page first loads, and when focusing Flash embeds. - Added command listing to the Options page for easy reference. - `J` & `K` have reversed for tab switching: `J` goes left and `K` goes right. -- `<c-[>` is now equivalent to ESC, to match the behavior of VIM. +- `<c-[>` is now equivalent to `Esc`, to match the behavior of VIM. - `<c-e>` and `<c-y>` are now mapped to scroll down and up respectively. - The characters used for link hints are now configurable under Advanced Options. @@ -383,7 +398,7 @@ does not support command repetition. - Command `yy` to yank (copy) the current tab's url to the clipboard. - Better Linux support. - Fix for `Shift+F` link hints. -- `ESC` now clears the keyQueue. So, for example, hitting `g`, `ESC`, `g` will no longer scroll the page. +- `Esc` now clears the keyQueue. So, for example, hitting `g`, `Esc`, `g` will no longer scroll the page. 1.1 (2010-01-03) diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index fc6263ee..189929b4 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -166,10 +166,14 @@ class Suggestion stripPatterns: [ # Google search specific replacements; this replaces query parameters which are known to not be helpful. # There's some additional information here: http://www.teknoids.net/content/google-search-parameters-2012 - [ "^https?://www\.google\.(com|ca|com\.au|co\.uk|ie)/.*[&?]q=" + [ "^https?://www\\.google\\.(com|ca|com\\.au|co\\.uk|ie)/.*[&?]q=" "ei gws_rd url ved usg sa usg sig2 bih biw cd aqs ie sourceid es_sm" .split(/\s+/).map (param) -> new RegExp "\&#{param}=[^&]+" ] + # On Google maps, we get a new history entry for every pan and zoom event. + [ "^https?://www\\.google\\.(com|ca|com\\.au|co\\.uk|ie)/maps/place/.*/@" + [ new RegExp "/@.*" ] ] + # General replacements; replaces leading and trailing fluff. [ '.', [ "^https?://", "\\W+$" ].map (re) -> new RegExp re ] ] diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee index 09261ff6..7926b45b 100644 --- a/background_scripts/completion_search.coffee +++ b/background_scripts/completion_search.coffee @@ -47,7 +47,7 @@ CompletionSearch = get: (searchUrl, url, callback) -> xhr = new XMLHttpRequest() xhr.open "GET", url, true - xhr.timeout = 1000 + xhr.timeout = 2500 xhr.ontimeout = xhr.onerror = -> callback null xhr.send() @@ -146,8 +146,8 @@ CompletionSearch = console.log "GET", url if @debug catch suggestions = [] - # We allow failures to be cached too, but remove them after just thirty minutes. - Utils.setTimeout 30 * 60 * 1000, => @completionCache.set completionCacheKey, null + # We allow failures to be cached too, but remove them after just thirty seconds. + Utils.setTimeout 30 * 1000, => @completionCache.set completionCacheKey, null console.log "fail", url if @debug callback suggestions diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 99a5672b..835b8a9a 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -35,7 +35,8 @@ namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/ # Event handlers selectionChangedHandlers = [] -tabLoadedHandlers = {} # tabId -> function() +# Note. tabLoadedHandlers handlers is exported for use also by "marks.coffee". +root.tabLoadedHandlers = {} # tabId -> function() # A secret, available only within the current instantiation of Vimium. The secret is big, likely unguessable # in practice, but less than 2^31. @@ -149,17 +150,26 @@ helpDialogHtmlForCommandGroup = (group, commandsToKey, availableCommands, bindings = (commandsToKey[command] || [""]).join(", ") if (showUnboundCommands || commandsToKey[command]) isAdvanced = Commands.advancedCommands.indexOf(command) >= 0 - html.push( - "<tr class='vimiumReset #{"advanced" if isAdvanced}'>", - "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>", - "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description) - - if (showCommandNames) - html.push("<span class='vimiumReset commandName'>(#{command})</span>") - - html.push("</td></tr>") + description = availableCommands[command].description + if bindings.length < 12 + helpDialogHtmlForCommand html, isAdvanced, bindings, description, showCommandNames, command + else + # If the length of the bindings is too long, then we display the bindings on a separate row from the + # description. This prevents the column alignment from becoming out of whack. + helpDialogHtmlForCommand html, isAdvanced, bindings, "", false, "" + helpDialogHtmlForCommand html, isAdvanced, "", description, showCommandNames, command html.join("\n") +helpDialogHtmlForCommand = (html, isAdvanced, bindings, description, showCommandNames, command) -> + html.push "<tr class='vimiumReset #{"advanced" if isAdvanced}'>" + if description + html.push "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>" + html.push "<td class='vimiumReset'>#{if description and bindings then ':' else ''}</td><td class='vimiumReset'>", description + html.push("<span class='vimiumReset commandName'>(#{command})</span>") if showCommandNames + else + html.push "<td class='vimiumReset' colspan='3' style='text-align: left;'>", Utils.escapeHtml(bindings) + html.push("</td></tr>") + # # Fetches the contents of a file bundled with this extension. # @@ -217,20 +227,6 @@ selectSpecificTab = (request) -> chrome.windows.update(tab.windowId, { focused: true }) chrome.tabs.update(request.id, { selected: true })) -# -# Used by the content scripts to get settings from the local storage. -# -handleSettings = (request, port) -> - switch request.operation - when "get" # Get a single settings value. - port.postMessage key: request.key, value: Settings.get request.key - when "set" # Set a single settings value. - Settings.set request.key, request.value - when "fetch" # Fetch multiple settings values. - values = request.values - values[key] = Settings.get key for own key of values - port.postMessage { values } - chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) -> if (selectionChangedHandlers.length > 0) selectionChangedHandlers.pop().call() @@ -650,7 +646,6 @@ bgLog = (request, sender) -> # Port handler mapping portHandlers = keyDown: handleKeyDown, - settings: handleSettings, completions: handleCompletions sendRequestHandlers = @@ -743,6 +738,4 @@ chrome.windows.getAll { populate: true }, (windows) -> (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response? chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler()) -# Start pulling changes from synchronized storage. -Settings.init() showUpgradeMessage() diff --git a/background_scripts/marks.coffee b/background_scripts/marks.coffee index 15d41205..6e5f08ba 100644 --- a/background_scripts/marks.coffee +++ b/background_scripts/marks.coffee @@ -1,34 +1,97 @@ -root = window.Marks = {} - -marks = {} - -root.create = (req, sender) -> - marks[req.markName] = - tabId: sender.tab.id - scrollX: req.scrollX - scrollY: req.scrollY - -chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) -> - if changeInfo.url? - removeMarksForTab tabId - -chrome.tabs.onRemoved.addListener (tabId, removeInfo) -> - # XXX(jez): what about restored tabs? - removeMarksForTab tabId - -removeMarksForTab = (id) -> - for markName, mark of marks - if mark.tabId is id - delete marks[markName] - -root.goto = (req, sender) -> - mark = marks[req.markName] - chrome.tabs.update mark.tabId, selected: true - chrome.tabs.sendMessage mark.tabId, - name: "setScrollPosition" - scrollX: mark.scrollX - scrollY: mark.scrollY - chrome.tabs.sendMessage mark.tabId, - name: "showHUDforDuration", - text: "Jumped to global mark '#{req.markName}'" - duration: 1000 + +Marks = + # This returns the key which is used for storing mark locations in chrome.storage.sync. + getLocationKey: (markName) -> "vimiumGlobalMark|#{markName}" + + # Get the part of a URL we use for matching here (that is, everything up to the first anchor). + getBaseUrl: (url) -> url.split("#")[0] + + # Create a global mark. We record vimiumSecret with the mark so that we can tell later, when the mark is + # used, whether this is the original Vimium session or a subsequent session. This affects whether or not + # tabId can be considered valid. + create: (req, sender) -> + chrome.storage.local.get "vimiumSecret", (items) => + markInfo = + vimiumSecret: items.vimiumSecret + markName: req.markName + url: @getBaseUrl sender.tab.url + tabId: sender.tab.id + scrollX: req.scrollX + scrollY: req.scrollY + + if markInfo.scrollX? and markInfo.scrollY? + @saveMark markInfo + else + # The front-end frame hasn't provided the scroll position (because it's not the top frame within its + # tab). We need to ask the top frame what its scroll position is. (With the frame Id set to 0, below, + # the request will only be handled by the top frame within the tab.) + chrome.tabs.sendMessage sender.tab.id, name: "getScrollPosition", frameId: 0, (response) => + @saveMark extend markInfo, scrollX: response.scrollX, scrollY: response.scrollY + + saveMark: (markInfo) -> + item = {} + item[@getLocationKey markInfo.markName] = markInfo + chrome.storage.sync.set item + + # Goto a global mark. We try to find the original tab. If we can't find that, then we try to find another + # tab with the original URL, and use that. And if we can't find such an existing tab, then we create a new + # one. Whichever of those we do, we then set the scroll position to the original scroll position. + goto: (req, sender) -> + chrome.storage.local.get "vimiumSecret", (items) => + vimiumSecret = items.vimiumSecret + key = @getLocationKey req.markName + chrome.storage.sync.get key, (items) => + markInfo = items[key] + if not markInfo + # The mark is not defined. + chrome.tabs.sendMessage sender.tab.id, + name: "showHUDforDuration", + text: "Global mark not set: '#{req.markName}'." + duration: 1000 + else if markInfo.vimiumSecret != vimiumSecret + # This is a different Vimium instantiation, so markInfo.tabId is definitely out of date. + @focusOrLaunch markInfo + else + # Check whether markInfo.tabId still exists. According to here (https://developer.chrome.com/extensions/tabs), + # tab Ids are unqiue within a Chrome session. So, if we find a match, we can use it. + chrome.tabs.get markInfo.tabId, (tab) => + if not chrome.runtime.lastError and tab?.url and markInfo.url == @getBaseUrl tab.url + # The original tab still exists. + @gotoPositionInTab markInfo + else + # The original tab no longer exists. + @focusOrLaunch markInfo + + # Focus an existing tab and scroll to the given position within it. + gotoPositionInTab: ({ tabId, scrollX, scrollY, markName }) -> + chrome.tabs.update tabId, { selected: true }, -> + chrome.tabs.sendMessage tabId, + { name: "setScrollPosition", scrollX: scrollX, scrollY: scrollY }, -> + chrome.tabs.sendMessage tabId, + name: "showHUDforDuration", + text: "Jumped to global mark '#{markName}'." + duration: 1000 + + # The tab we're trying to find no longer exists. We either find another tab with a matching URL and use it, + # or we create a new tab. + focusOrLaunch: (markInfo) -> + chrome.tabs.query { url: markInfo.url }, (tabs) => + if 0 < tabs.length + # We have a matching tab: use it (prefering, if there are more than one, one in the current window). + @pickTabInWindow tabs, (tab) => + @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) => + # 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 + + # Given a list of tabs, pick one in the current window, if possible, otherwise just pick any. + pickTabInWindow: (tabs, continuation) -> + chrome.windows.getCurrent ({ id }) -> + tabsInWindow = tabs.filter (tab) -> tab.windowId == id + continuation tabsInWindow[0] ? tabs[0] + +root = exports ? window +root.Marks = Marks diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee index f38d6b45..84b8abeb 100644 --- a/content_scripts/hud.coffee +++ b/content_scripts/hud.coffee @@ -48,7 +48,7 @@ HUD = -> ready and document.body != null # A preference which can be toggled in the Options page. */ - enabled: -> !settings.get("hideHud") + enabled: -> !Settings.get("hideHud") class Tween opacity: 0 diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 3cebac4c..2bcc7508 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -26,20 +26,15 @@ LinkHints = linkActivator: undefined # While in delayMode, all keypresses have no effect. delayMode: false - # Handle the link hinting marker generation and matching. Must be initialized after settings have been + # Handle the link hinting marker generation and matching. Must be initialized after Settings have been # loaded, so that we can retrieve the option setting. getMarkerMatcher: -> - if settings.get("filterLinkHints") then filterHints else alphabetHints + if Settings.get("filterLinkHints") then filterHints else alphabetHints # lock to ensure only one instance runs at a time isActive: false # Call this function on exit (if defined). onExit: null - # - # To be called after linkHints has been generated from linkHintsBase. - # - init: -> - # We need this as a top-level function because our command system doesn't yet support arguments. activateModeToOpenInNewTab: -> @activateMode(OPEN_IN_NEW_BG_TAB) activateModeToOpenInNewForegroundTab: -> @activateMode(OPEN_IN_NEW_FG_TAB) @@ -60,7 +55,7 @@ LinkHints = # For these modes, we filter out those elements which don't have an HREF (since there's nothing we can do # with them). elements = (el for el in elements when el.element.href?) if mode in [ COPY_LINK_URL, OPEN_INCOGNITO ] - if settings.get "filterLinkHints" + if Settings.get "filterLinkHints" # When using text filtering, we sort the elements such that we visit descendants before their ancestors. # This allows us to exclude the text used for matching descendants from that used for matching their # ancestors. @@ -389,7 +384,7 @@ alphabetHints = # may be of different lengths. # hintStrings: (linkCount) -> - linkHintCharacters = settings.get("linkHintCharacters") + linkHintCharacters = Settings.get("linkHintCharacters") # Determine how many digits the link hints will require in the worst case. Usually we do not need # all of these digits for every link single hint, so we can show shorter hints for a few of the links. digitsNeeded = Math.ceil(@logXOfBase(linkCount, linkHintCharacters.length)) @@ -460,7 +455,7 @@ filterHints = @labelMap[forElement] = labelText generateHintString: (linkHintNumber) -> - (numberToHintString linkHintNumber + 1, settings.get "linkHintNumbers").toUpperCase() + (numberToHintString linkHintNumber + 1, Settings.get "linkHintNumbers").toUpperCase() generateLinkText: (element) -> linkText = "" @@ -519,7 +514,7 @@ filterHints = if (!@hintKeystrokeQueue.pop() && !@linkTextKeystrokeQueue.pop()) return { linksMatched: [] } else if (keyChar) - if (settings.get("linkHintNumbers").indexOf(keyChar) >= 0) + if (Settings.get("linkHintNumbers").indexOf(keyChar) >= 0) @hintKeystrokeQueue.push(keyChar) else # since we might renumber the hints, the current hintKeyStrokeQueue diff --git a/content_scripts/marks.coffee b/content_scripts/marks.coffee index 316ab951..067d05a8 100644 --- a/content_scripts/marks.coffee +++ b/content_scripts/marks.coffee @@ -1,45 +1,79 @@ -root = window.Marks = {} - -root.activateCreateMode = -> - handlerStack.push keydown: (e) -> - keyChar = KeyboardUtils.getKeyChar(event) - return unless keyChar isnt "" - - if /[A-Z]/.test keyChar - chrome.runtime.sendMessage { - handler: 'createMark', - markName: keyChar - scrollX: window.scrollX, - scrollY: window.scrollY - }, -> HUD.showForDuration "Created global mark '#{keyChar}'", 1000 - else if /[a-z]/.test keyChar - [baseLocation, sep, hash] = window.location.href.split '#' - localStorage["vimiumMark|#{baseLocation}|#{keyChar}"] = JSON.stringify - scrollX: window.scrollX, - scrollY: window.scrollY - HUD.showForDuration "Created local mark '#{keyChar}'", 1000 - - @remove() - - false - -root.activateGotoMode = -> - handlerStack.push keydown: (e) -> - keyChar = KeyboardUtils.getKeyChar(event) - return unless keyChar isnt "" - - if /[A-Z]/.test keyChar - chrome.runtime.sendMessage - handler: 'gotoMark' - markName: keyChar - else if /[a-z]/.test keyChar - [baseLocation, sep, hash] = window.location.href.split '#' - markString = localStorage["vimiumMark|#{baseLocation}|#{keyChar}"] - if markString? - mark = JSON.parse markString - window.scrollTo mark.scrollX, mark.scrollY - HUD.showForDuration "Jumped to local mark '#{keyChar}'", 1000 - - @remove() - - false + +Marks = + previousPositionRegisters: [ "`", "'" ] + localRegisters: {} + mode: null + + exit: (continuation = null) -> + @mode?.exit() + @mode = null + continuation?() + + # This returns the key which is used for storing mark locations in localStorage. + getLocationKey: (keyChar) -> + "vimiumMark|#{window.location.href.split('#')[0]}|#{keyChar}" + + getMarkString: -> + JSON.stringify scrollX: window.scrollX, scrollY: window.scrollY + + setPreviousPosition: -> + markString = @getMarkString() + @localRegisters[reg] = markString for reg in @previousPositionRegisters + + showMessage: (message, keyChar) -> + HUD.showForDuration "#{message} \"#{keyChar}\".", 1000 + + # If <Shift> is depressed, then it's a global mark, otherwise it's a local mark. This is consistent + # vim's [A-Z] for global marks and [a-z] for local marks. However, it also admits other non-Latin + # characters. The exceptions are "`" and "'", which are always considered local marks. + isGlobalMark: (event, keyChar) -> + event.shiftKey and keyChar not in @previousPositionRegisters + + activateCreateMode: -> + @mode = new Mode + name: "create-mark" + indicator: "Create mark..." + exitOnEscape: true + suppressAllKeyboardEvents: true + keypress: (event) => + keyChar = String.fromCharCode event.charCode + @exit => + if @isGlobalMark event, keyChar + # We record the current scroll position, but only if this is the top frame within the tab. + # Otherwise, we'll fetch the scroll position of the top frame from the background page later. + [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame() + chrome.runtime.sendMessage + handler: 'createMark' + markName: keyChar + scrollX: scrollX + scrollY: scrollY + , => @showMessage "Created global mark", keyChar + else + localStorage[@getLocationKey keyChar] = @getMarkString() + @showMessage "Created local mark", keyChar + + activateGotoMode: (registryEntry) -> + @mode = new Mode + name: "goto-mark" + indicator: "Go to mark..." + exitOnEscape: true + suppressAllKeyboardEvents: true + keypress: (event) => + @exit => + keyChar = String.fromCharCode event.charCode + if @isGlobalMark event, keyChar + chrome.runtime.sendMessage + handler: 'gotoMark' + markName: keyChar + else + markString = @localRegisters[keyChar] ? localStorage[@getLocationKey keyChar] + if markString? + @setPreviousPosition() + position = JSON.parse markString + window.scrollTo position.scrollX, position.scrollY + @showMessage "Jumped to local mark", keyChar + else + @showMessage "Local mark not set", keyChar + +root = exports ? window +root.Marks = Marks diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index f631b4cd..ffabc111 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -47,6 +47,13 @@ class Mode @id = "#{@name}-#{@count}" @log "activate:", @id + # If options.suppressAllKeyboardEvents is truthy, then all keyboard events are suppressed. This avoids + # the need for modes which suppress all keyboard events 1) to provide handlers for all of those events, + # or 2) to worry about event suppression and event-handler return values. + if @options.suppressAllKeyboardEvents + for type in [ "keydown", "keypress", "keyup" ] + @options[type] = @alwaysSuppressEvent @options[type] + @push keydown: @options.keydown || null keypress: @options.keypress || null @@ -171,6 +178,13 @@ class Mode # case), because they do not need to be concerned with the value they yield. alwaysContinueBubbling: handlerStack.alwaysContinueBubbling + # Shorthand for an event handler which always suppresses event propagation. + alwaysSuppressEvent: (handler = null) -> + (event) => + handler? event + DomUtils.suppressPropagation event + @stopBubblingAndFalse + # Activate a new instance of this mode, together with all of its original options (except its main # keybaord-event handlers; these will be recreated). cloneMode: -> diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 27fc9cdc..81c71fcd 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -117,8 +117,7 @@ checkVisibility = (element) -> # CoreScroller contains the core function (scroll) and logic for relative scrolls. All scrolls are ultimately # translated to relative scrolls. CoreScroller is not exported. CoreScroller = - init: (frontendSettings) -> - @settings = frontendSettings + init: -> @time = 0 @lastEvent = null @keyIsDown = false @@ -139,7 +138,7 @@ CoreScroller = @time += 1 # Return true if CoreScroller would not initiate a new scroll right now. - wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll" + wouldNotInitiateScroll: -> @lastEvent?.repeat and Settings.get "smoothScroll" # Calibration fudge factors for continuous scrolling. The calibration value starts at 1.0. We then # increase it (until it exceeds @maxCalibration) if we guess that the scroll is too slow, or decrease it @@ -153,7 +152,7 @@ CoreScroller = scroll: (element, direction, amount, continuous = true) -> return unless amount - unless @settings.get "smoothScroll" + unless Settings.get "smoothScroll" # Jump scrolling. performScroll element, direction, amount checkVisibility element @@ -215,11 +214,11 @@ CoreScroller = # Scroller contains the two main scroll functions which are used by clients. Scroller = - init: (frontendSettings) -> + init: -> handlerStack.push _name: 'scroller/active-element' DOMActivate: (event) -> handlerStack.alwaysContinueBubbling -> activatedElement = event.target - CoreScroller.init frontendSettings + CoreScroller.init() # scroll the active element in :direction by :amount * :factor. # :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions. diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index c8c83029..3055ecea 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -42,62 +42,6 @@ textInputXPath = (-> )() # -# settings provides a browser-global localStorage-backed dict. get() and set() are synchronous, but load() -# must be called beforehand to ensure get() will return up-to-date values. -# -settings = - isLoaded: false - port: null - eventListeners: {} - values: - scrollStepSize: null - linkHintCharacters: null - linkHintNumbers: null - filterLinkHints: null - hideHud: null - previousPatterns: null - nextPatterns: null - regexFindMode: null - userDefinedLinkHintCss: null - helpDialog_showAdvancedCommands: null - smoothScroll: null - grabBackFocus: null - searchEngines: null - - init: -> - @port = chrome.runtime.connect name: "settings" - @port.onMessage.addListener (response) => @receiveMessage response - - # If the port is closed, the background page has gone away (since we never close it ourselves). Stub the - # settings object so we don't keep trying to connect to the extension even though it's gone away. - @port.onDisconnect.addListener => - @port = null - for own property, value of this - # @get doesn't depend on @port, so we can continue to support it to try and reduce errors. - @[property] = (->) if "function" == typeof value and property != "get" - - get: (key) -> @values[key] - - set: (key, value) -> - @init() unless @port - - @values[key] = value - @port.postMessage operation: "set", key: key, value: value - - load: -> - @init() unless @port - @port.postMessage operation: "fetch", values: @values - - receiveMessage: (response) -> - @values = response.values if response.values? - @values[response.key] = response.value if response.key? and response.value? - @isLoaded = true - listener() while listener = @eventListeners.load?.pop() - - addEventListener: (eventName, callback) -> - (@eventListeners[eventName] ||= []).push callback - -# # Give this frame a unique (non-zero) id. # frameId = 1 + Math.floor(Math.random()*999999999) @@ -119,15 +63,15 @@ class GrabBackFocus extends Mode _name: "grab-back-focus-mousedown" mousedown: => @alwaysContinueBubbling => @exit() - activate = => - return @exit() unless settings.get "grabBackFocus" - @push - _name: "grab-back-focus-focus" - focus: (event) => @grabBackFocus event.target - # An input may already be focused. If so, grab back the focus. - @grabBackFocus document.activeElement if document.activeElement - - if settings.isLoaded then activate() else settings.addEventListener "load", activate + Settings.use "grabBackFocus", (grabBackFocus) => + if grabBackFocus + @push + _name: "grab-back-focus-focus" + focus: (event) => @grabBackFocus event.target + # An input may already be focused. If so, grab back the focus. + @grabBackFocus document.activeElement if document.activeElement + else + @exit() grabBackFocus: (element) -> return @continueBubbling unless DomUtils.isEditable element @@ -176,15 +120,12 @@ window.initializeModes = -> new NormalMode new PassKeysMode new InsertMode permanent: true - Scroller.init settings + Scroller.init() # # Complete initialization work that sould be done prior to DOMReady. # initializePreDomReady = -> - settings.addEventListener("load", LinkHints.init.bind(LinkHints)) - settings.load() - initializeModes() checkIfEnabledForUrl() refreshCompletionKeys() @@ -201,12 +142,12 @@ initializePreDomReady = -> window.removeEventListener "focus", onFocus requestHandlers = - showHUDforDuration: (request) -> HUD.showForDuration request.text, request.duration + showHUDforDuration: handleShowHUDforDuration toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId) focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame request refreshCompletionKeys: refreshCompletionKeys getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY - setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY + setScrollPosition: setScrollPosition executePageCommand: executePageCommand currentKeyQueue: (request) -> keyQueue = request.keyQueue @@ -220,7 +161,7 @@ initializePreDomReady = -> # from the former. return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://' # These requests are delivered to the options page, but there are no handlers there. - return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame" ] + return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame", "setIcon" ] shouldHandleRequest = isEnabledForUrl # We always handle the message if it's one of these listed message types. shouldHandleRequest ||= request.name in [ "executePageCommand", "checkEnabledAfterURLChange" ] @@ -261,13 +202,11 @@ window.installListeners = -> # # Whenever we get the focus: -# - Reload settings (they may have changed). # - Tell the background page this frame's URL. # - Check if we should be enabled. # onFocus = (event) -> if event.target == window - settings.load() chrome.runtime.sendMessage handler: "frameFocused", frameId: frameId checkIfEnabledForUrl true @@ -302,9 +241,10 @@ unregisterFrame = -> tab_is_closing: DomUtils.isTopFrame() executePageCommand = (request) -> + commandType = request.command.split(".")[0] # Vomnibar commands are handled in the tab's main/top frame. They are handled even if Vimium is otherwise # disabled in the frame. - if request.command.split(".")[0] == "Vomnibar" + if commandType == "Vomnibar" if DomUtils.isTopFrame() # We pass the frameId from request. That's the frame which originated the request, so that's the frame # which should receive the focus when the vomnibar closes. @@ -322,9 +262,18 @@ executePageCommand = (request) -> refreshCompletionKeys(request) -setScrollPosition = (scrollX, scrollY) -> - if (scrollX > 0 || scrollY > 0) - DomUtils.documentReady(-> window.scrollTo(scrollX, scrollY)) +handleShowHUDforDuration = ({ text, duration }) -> + if DomUtils.isTopFrame() + DomUtils.documentReady -> HUD.showForDuration text, duration + +setScrollPosition = ({ scrollX, scrollY }) -> + if DomUtils.isTopFrame() + DomUtils.documentReady -> + window.focus() + document.body.focus() + if 0 < scrollX or 0 < scrollY + Marks.setPreviousPosition() + window.scrollTo scrollX, scrollY # # Called from the backend in order to change frame focus. @@ -360,18 +309,22 @@ window.focusThisFrame = do -> setTimeout (-> highlightedFrameElement.remove()), 200 extend window, - scrollToBottom: -> Scroller.scrollTo "y", "max" - scrollToTop: -> Scroller.scrollTo "y", 0 + scrollToBottom: -> + Marks.setPreviousPosition() + Scroller.scrollTo "y", "max" + scrollToTop: -> + Marks.setPreviousPosition() + Scroller.scrollTo "y", 0 scrollToLeft: -> Scroller.scrollTo "x", 0 scrollToRight: -> Scroller.scrollTo "x", "max" - scrollUp: -> Scroller.scrollBy "y", -1 * settings.get("scrollStepSize") - scrollDown: -> Scroller.scrollBy "y", settings.get("scrollStepSize") + scrollUp: -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") + scrollDown: -> Scroller.scrollBy "y", Settings.get("scrollStepSize") scrollPageUp: -> Scroller.scrollBy "y", "viewSize", -1/2 scrollPageDown: -> Scroller.scrollBy "y", "viewSize", 1/2 scrollFullPageUp: -> Scroller.scrollBy "y", "viewSize", -1 scrollFullPageDown: -> Scroller.scrollBy "y", "viewSize" - scrollLeft: -> Scroller.scrollBy "x", -1 * settings.get("scrollStepSize") - scrollRight: -> Scroller.scrollBy "x", settings.get("scrollStepSize") + scrollLeft: -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") + scrollRight: -> Scroller.scrollBy "x", Settings.get("scrollStepSize") extend window, reload: -> window.location.reload() @@ -717,7 +670,7 @@ 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' + 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 @@ -922,6 +875,7 @@ window.getFindModeQuery = (backwards) -> findModeQuery.parsedQuery findAndFocus = (backwards) -> + Marks.setPreviousPosition() query = getFindModeQuery backwards findModeQueryHasResults = @@ -1029,12 +983,12 @@ findAndFollowRel = (value) -> return true window.goPrevious = -> - previousPatterns = settings.get("previousPatterns") || "" + previousPatterns = Settings.get("previousPatterns") || "" previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length ) findAndFollowRel("prev") || findAndFollowLink(previousStrings) window.goNext = -> - nextPatterns = settings.get("nextPatterns") || "" + nextPatterns = Settings.get("nextPatterns") || "" nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length ) findAndFollowRel("next") || findAndFollowLink(nextStrings) @@ -1068,6 +1022,7 @@ findModeRestoreSelection = (range = findModeInitialRange) -> # Enters find mode. Returns the new find-mode instance. window.enterFindMode = (options = {}) -> + Marks.setPreviousPosition() # Save the selection, so performFindInPlace can restore it. findModeSaveSelection() findModeQuery = rawQuery: "" @@ -1089,7 +1044,7 @@ window.showHelpDialog = (html, fid) -> VimiumHelpDialog = # This setting is pulled out of local storage. It's false by default. - getShowAdvancedCommands: -> settings.get("helpDialog_showAdvancedCommands") + getShowAdvancedCommands: -> Settings.get("helpDialog_showAdvancedCommands") init: () -> this.dialogElement = document.getElementById("vimiumHelpDialog") @@ -1105,7 +1060,7 @@ window.showHelpDialog = (html, fid) -> event.preventDefault() showAdvanced = VimiumHelpDialog.getShowAdvancedCommands() VimiumHelpDialog.showAdvancedCommands(!showAdvanced) - settings.set("helpDialog_showAdvancedCommands", !showAdvanced) + Settings.set("helpDialog_showAdvancedCommands", !showAdvanced) showAdvancedCommands: (visible) -> VimiumHelpDialog.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].innerHTML = @@ -1184,7 +1139,6 @@ window.onbeforeunload = -> scrollY: window.scrollY) root = exports ? window -root.settings = settings root.handlerStack = handlerStack root.frameId = frameId root.windowIsFocused = windowIsFocused diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 4bd8e8fd..6c08ce92 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -8,7 +8,7 @@ Vomnibar = # the form "keyword=X", for direct activation of a custom search engine. parseRegistryEntry: (registryEntry = { options: [] }, callback = null) -> options = {} - searchEngines = settings.get("searchEngines") ? "" + searchEngines = Settings.get("searchEngines") ? "" SearchEngines.refreshAndUse searchEngines, (engines) -> for option in registryEntry.options [ key, value ] = option.split "=" diff --git a/lib/settings.coffee b/lib/settings.coffee index dd667dbd..ca4e77b0 100644 --- a/lib/settings.coffee +++ b/lib/settings.coffee @@ -1,118 +1,86 @@ -# -# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync. -# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those -# changes to localStorage and into vimium's internal state. -# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating -# changes to localStorage and into vimium's internal state. -# -# The effect is best-effort synchronization of vimium options/settings between -# chrome/vimium instances. -# -# NOTE: -# Values handled within this module are ALWAYS already JSON.stringifed, so -# they're always non-empty strings. -# - -root = exports ? window -Sync = +Settings = storage: chrome.storage.sync - doNotSync: ["settingsVersion", "previousVersion"] + cache: {} + isLoaded: false + onLoadedCallbacks: [] - # This is called in main.coffee. init: -> - chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area - @fetchAsync() + if Utils.isExtensionPage() + # On extension pages, we use localStorage (or a copy of it) as the cache. + @cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage + @onLoaded() - # Asynchronous fetch from synced storage, called only at startup. - fetchAsync: -> @storage.get null, (items) => unless chrome.runtime.lastError - for own key, value of items - Settings.storeAndPropagate key, value if @shouldSyncKey key + @handleUpdateFromChromeStorage key, value for own key, value of items - # Asynchronous message from synced storage. - handleStorageUpdate: (changes, area) -> - for own key, change of changes - Settings.storeAndPropagate key, change?.newValue if @shouldSyncKey key + chrome.storage.onChanged.addListener (changes, area) => + @propagateChangesFromChromeStorage changes if area == "sync" - # Only called synchronously from within vimium, never on a callback. - # No need to propagate updates to the rest of vimium, that's already been done. - set: (key, value) -> - if @shouldSyncKey key - setting = {}; setting[key] = value - @storage.set setting + @onLoaded() - # Only called synchronously from within vimium, never on a callback. - clear: (key) -> - @storage.remove key if @shouldSyncKey key + # Called after @cache has been initialized. On extension pages, this will be called twice, but that does + # not matter because it's idempotent. + onLoaded: -> + @isLoaded = true + callback() while callback = @onLoadedCallbacks.pop() + + shouldSyncKey: (key) -> + (key of @defaults) and key not in [ "settingsVersion", "previousVersion" ] + + propagateChangesFromChromeStorage: (changes) -> + @handleUpdateFromChromeStorage key, change?.newValue for own key, change of changes + + handleUpdateFromChromeStorage: (key, value) -> + # 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 - # Should we synchronize this key? - shouldSyncKey: (key) -> key not in @doNotSync - -# -# Used by all parts of Vimium to manipulate localStorage. -# - -# Select the object to use as the cache for settings. -if Utils.isExtensionPage() - if Utils.isBackgroundPage() - settingsCache = localStorage - else - settingsCache = extend {}, localStorage # Make a copy of the cached settings from localStorage -else - settingsCache = {} - -root.Settings = Settings = - cache: settingsCache - init: -> Sync.init() get: (key) -> - if (key of @cache) then JSON.parse(@cache[key]) else @defaults[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 (value == @defaults[key]) - @clear(key) + # 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 - Sync.set key, jsonValue + if @shouldSyncKey key + setting = {}; setting[key] = jsonValue + @storage.set setting + @performPostUpdateHook key, value clear: (key) -> - if @has key - delete @cache[key] - Sync.clear key + delete @cache[key] if @has key + @storage.remove key if @shouldSyncKey key + @performPostUpdateHook key, @get key has: (key) -> key of @cache - # For settings which require action when their value changes, add hooks to this object, to be called from - # options/options.coffee (when the options page is saved), and by Settings.storeAndPropagate (when an - # update propagates from chrome.storage.sync). - postUpdateHooks: {} + use: (key, callback) -> + invokeCallback = => callback @get key + if @isLoaded then invokeCallback() else @onLoadedCallbacks.push invokeCallback - # postUpdateHooks convenience wrapper - performPostUpdateHook: (key, value) -> - @postUpdateHooks[key]? value - - # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate). - storeAndPropagate: (key, value) -> - return unless key of @defaults - return if value and key of @cache and @cache[key] is value - defaultValue = @defaults[key] - defaultValueJSON = JSON.stringify(defaultValue) - - if value and value != defaultValueJSON - # Key/value has been changed to non-default value at remote instance. - @cache[key] = value - @performPostUpdateHook key, JSON.parse(value) - else - # Key has been reset to default value at remote instance. - if key of @cache - delete @cache[key] - @performPostUpdateHook key, defaultValue + # For settings which require action when their value changes, add hooks to this object. + postUpdateHooks: {} + performPostUpdateHook: (key, value) -> @postUpdateHooks[key]? value - # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans - # or strings + # Default values for all settings. defaults: scrollStepSize: 60 smoothScroll: true @@ -144,7 +112,7 @@ root.Settings = Settings = exclusionRules: [ # Disable Vimium on Gmail. - { pattern: "http*://mail.google.com/*", passKeys: "" } + { pattern: "https?://mail.google.com/*", passKeys: "" } ] # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in @@ -159,31 +127,30 @@ root.Settings = Settings = # default/fall back search engine searchUrl: "https://www.google.com/search?q=" # put in an example search engine - searchEngines: [ - "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia" - "" - "# More examples." - "#" - "# (Vimium has built-in completion for these.)" - "#" - "# g: http://www.google.com/search?q=%s Google" - "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..." - "# y: http://www.youtube.com/results?search_query=%s Youtube" - "# b: https://www.bing.com/search?q=%s Bing" - "# d: https://duckduckgo.com/?q=%s DuckDuckGo" - "# az: http://www.amazon.com/s/?field-keywords=%s Amazon" - "#" - "# Another example (for Vimium does not have completion)." - "#" - "# m: https://www.google.com/maps/search/%s Google Maps" - ].join "\n" + searchEngines: + """ + w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia + + # More examples. + # + # (Vimium supports search completion Wikipedia, as + # above, and for these.) + # + # g: http://www.google.com/search?q=%s Google + # l: http://www.google.com/search?q=%s&btnI I'm feeling lucky... + # y: http://www.youtube.com/results?search_query=%s Youtube + # gm: https://www.google.com/maps?q=%s Google maps + # b: https://www.bing.com/search?q=%s Bing + # d: https://duckduckgo.com/?q=%s DuckDuckGo + # az: http://www.amazon.com/s/?field-keywords=%s Amazon + """ newTabUrl: "chrome://newtab" grabBackFocus: false settingsVersion: Utils.getCurrentVersion() + helpDialog_showAdvancedCommands: false -# Export Sync via Settings for tests. -root.Settings.Sync = Sync +Settings.init() # Perform migration from old settings versions, if this is the background page. if Utils.isBackgroundPage() @@ -200,3 +167,6 @@ if Utils.isBackgroundPage() unless chrome.runtime.lastError or items.findModeRawQueryList rawQuery = Settings.get "findModeRawQuery" chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else []) + +root = exports ? window +root.Settings = Settings diff --git a/manifest.json b/manifest.json index 6c85911f..80aca4c5 100644 --- a/manifest.json +++ b/manifest.json @@ -41,6 +41,7 @@ "lib/rect.js", "lib/handler_stack.js", "lib/clipboard.js", + "lib/settings.js", "content_scripts/ui_component.js", "content_scripts/link_hints.js", "content_scripts/vomnibar.js", diff --git a/pages/options.coffee b/pages/options.coffee index c8c21850..ea4301a9 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -1,8 +1,13 @@ $ = (id) -> document.getElementById id -Settings.init() bgExclusions = chrome.extension.getBackgroundPage().Exclusions +# We have to use Settings from the background page here (not Settings, directly) to avoid a race condition for +# the page popup. Specifically, we must ensure that the settings have been updated on the background page +# *before* the popup closes. This ensures that any exclusion-rule changes are in place before the page +# regains the focus. +bgSettings = chrome.extension.getBackgroundPage().Settings + # # Class hierarchy for various types of option. class Option @@ -21,20 +26,17 @@ class Option # Fetch a setting from localStorage, remember the @previous value and populate the DOM element. # Return the fetched value. fetch: -> - @populateElement @previous = Settings.get @field + @populateElement @previous = bgSettings.get @field @previous # Write this option's new value back to localStorage, if necessary. save: -> value = @readValueFromElement() - if not @areEqual value, @previous - Settings.set @field, @previous = value - - # Compare values; this is overridden by sub-classes. - areEqual: (a,b) -> a == b + if JSON.stringify(value) != JSON.stringify @previous + bgSettings.set @field, @previous = value restoreToDefault: -> - Settings.clear @field + bgSettings.clear @field @fetch() # Static method. @@ -120,12 +122,6 @@ class ExclusionRulesOption extends Option passKeys: @getPassKeys(element).value.trim() rules.filter (rule) -> rule.pattern - areEqual: (a,b) -> - # Flatten each list of rules to a newline-separated string representation, and then use string equality. - # This is correct because patterns and passKeys cannot themselves contain newlines. - flatten = (rule) -> if rule and rule.pattern then rule.pattern + "\n" + rule.passKeys else "" - a.map(flatten).join("\n") == b.map(flatten).join("\n") - # Accessors for the three main sub-elements of an "exclusionRuleTemplateInstance". getPattern: (element) -> element.querySelector(".pattern") getPassKeys: (element) -> element.querySelector(".passKeys") @@ -258,7 +254,6 @@ initOptionsPage = -> searchEngines: TextOption searchUrl: NonEmptyTextOption userDefinedLinkHintCss: TextOption - omniSearchWeight: NumberOption # Populate options. The constructor adds each new object to "Option.all". for name, type of options diff --git a/pages/options.css b/pages/options.css index 282a523b..75bbe159 100644 --- a/pages/options.css +++ b/pages/options.css @@ -107,7 +107,7 @@ input#linkHintNumbers { input#linkHintCharacters { width: 100%; } -input#scrollStepSize, input#omniSearchWeight { +input#scrollStepSize { width: 50px; margin-right: 3px; padding-left: 3px; diff --git a/pages/options.html b/pages/options.html index 7b66efde..12a3ad21 100644 --- a/pages/options.html +++ b/pages/options.html @@ -3,7 +3,6 @@ <title>Vimium Options</title> <link rel="stylesheet" type="text/css" href="options.css"> <script src="content_script_loader.js"></script> - <script type="text/javascript" src="../lib/settings.js"></script> <script type="text/javascript" src="options.js"></script> </head> diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index d5659fdc..cf9ed7b0 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -169,14 +169,14 @@ class VomnibarUI completion = @completions[@selection] @hide -> completion.performAction openInNewTab else if action == "delete" - inputIsEmpty = @input.value.length == 0 - if inputIsEmpty and @customSearchMode? + if @customSearchMode? and @input.selectionEnd == 0 # Normally, with custom search engines, the keyword (e,g, the "w" of "w query terms") is suppressed. - # If the input is empty, then reinstate the keyword (the "w"). - @input.value = @customSearchMode + # If the cursor is at the start of the input, then reinstate the keyword (the "w"). + @input.value = @customSearchMode + @input.value.ltrim() + @input.selectionStart = @input.selectionEnd = @customSearchMode.length @customSearchMode = null @update true - else if inputIsEmpty and @seenTabToOpenCompletionList + else if @seenTabToOpenCompletionList and @input.value.trim().length == 0 @seenTabToOpenCompletionList = false @update true else diff --git a/tests/dom_tests/chrome.coffee b/tests/dom_tests/chrome.coffee index 4c9bfa52..d4e6930d 100644 --- a/tests/dom_tests/chrome.coffee +++ b/tests/dom_tests/chrome.coffee @@ -7,6 +7,9 @@ root.chromeMessages = [] document.hasFocus = -> true +fakeManifest = + version: "1.51" + root.chrome = runtime: connect: -> @@ -18,16 +21,17 @@ root.chrome = onMessage: addListener: -> sendMessage: (message) -> chromeMessages.unshift message - getManifest: -> + getManifest: -> fakeManifest getURL: (url) -> "../../#{url}" storage: local: get: -> set: -> sync: - get: -> + get: (_, callback) -> callback? {} set: -> onChanged: addListener: -> extension: inIncognitoContext: false + getURL: (url) -> chrome.runtime.getURL url diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee index 8c2b73c3..8f293075 100644 --- a/tests/dom_tests/dom_tests.coffee +++ b/tests/dom_tests/dom_tests.coffee @@ -34,12 +34,20 @@ initializeModeState = -> handlerStack.bubbleEvent "registerKeyQueue", keyQueue: "" +# Tell Settings that it's been loaded. +Settings.isLoaded = true + +# Shoulda.js doesn't support async code, so we try not to use any. +Utils.nextTick = (func) -> func() + # # Retrieve the hint markers as an array object. # getHintMarkers = -> Array::slice.call document.getElementsByClassName("vimiumHintMarker"), 0 +stubSettings = (key, value) -> stub Settings.cache, key, JSON.stringify value + # # Generate tests that are common to both default and filtered # link hinting modes. @@ -52,8 +60,8 @@ createGeneralHintTests = (isFilteredMode) -> initializeModeState() testContent = "<a>test</a>" + "<a>tress</a>" document.getElementById("test-div").innerHTML = testContent - stub settings.values, "filterLinkHints", false - stub settings.values, "linkHintCharacters", "ab" + stubSettings "filterLinkHints", false + stubSettings "linkHintCharacters", "ab" tearDown -> document.getElementById("test-div").innerHTML = "" @@ -92,8 +100,8 @@ context "Test link hints for focusing input elements correctly", testDiv = document.getElementById("test-div") testDiv.innerHTML = "" - stub settings.values, "filterLinkHints", false - stub settings.values, "linkHintCharacters", "ab" + stubSettings "filterLinkHints", false + stubSettings "linkHintCharacters", "ab" # Every HTML5 input type except for hidden. We should be able to activate all of them with link hints. inputTypes = ["button", "checkbox", "color", "date", "datetime", "datetime-local", "email", "file", @@ -129,12 +137,11 @@ context "Alphabetical link hints", setup -> initializeModeState() - stub settings.values, "filterLinkHints", false - stub settings.values, "linkHintCharacters", "ab" + stubSettings "filterLinkHints", false + stubSettings "linkHintCharacters", "ab" # Three hints will trigger double hint chars. createLinks 3 - LinkHints.init() LinkHints.activateMode() tearDown -> @@ -161,8 +168,8 @@ context "Filtered link hints", # elements. setup -> - stub settings.values, "filterLinkHints", true - stub settings.values, "linkHintNumbers", "0123456789" + stubSettings "filterLinkHints", true + stubSettings "linkHintNumbers", "0123456789" context "Text hints", @@ -170,7 +177,6 @@ context "Filtered link hints", initializeModeState() testContent = "<a>test</a>" + "<a>tress</a>" + "<a>trait</a>" + "<a>track<img alt='alt text'/></a>" document.getElementById("test-div").innerHTML = testContent - LinkHints.init() LinkHints.activateMode() tearDown -> @@ -289,7 +295,7 @@ context "Find prev / next links", <a href='#first'>nextcorrupted</a> <a href='#second'>next page</a> """ - stub settings.values, "nextPatterns", "next" + stubSettings "nextPatterns", "next" goNext() assert.equal '#second', window.location.hash @@ -297,7 +303,7 @@ context "Find prev / next links", document.getElementById("test-div").innerHTML = """ <a href='#first'>>></a> """ - stub settings.values, "nextPatterns", ">>" + stubSettings "nextPatterns", ">>" goNext() assert.equal '#first', window.location.hash @@ -306,7 +312,7 @@ context "Find prev / next links", <a href='#first'>lorem ipsum next</a> <a href='#second'>next!</a> """ - stub settings.values, "nextPatterns", "next" + stubSettings "nextPatterns", "next" goNext() assert.equal '#second', window.location.hash diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index 5ccd39e7..f7cc430d 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -35,6 +35,7 @@ <script type="text/javascript" src="../../lib/rect.js"></script> <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="../../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/exclusion_test.coffee b/tests/unit_tests/exclusion_test.coffee index 28c17a2f..0e4b87bc 100644 --- a/tests/unit_tests/exclusion_test.coffee +++ b/tests/unit_tests/exclusion_test.coffee @@ -15,7 +15,6 @@ root.Marks = extend(global, require "../../lib/utils.js") Utils.getCurrentVersion = -> '1.44' extend(global,require "../../lib/settings.js") -Settings.init() extend(global, require "../../background_scripts/exclusions.js") extend(global, require "../../background_scripts/commands.js") extend(global, require "../../background_scripts/main.js") diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index ded7b5f8..08145190 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -14,7 +14,6 @@ context "settings", stub global, 'localStorage', {} Settings.cache = global.localStorage # Point the settings cache to the new localStorage object. Settings.postUpdateHooks = {} # Avoid running update hooks which include calls to outside of settings. - Settings.init() should "save settings in localStorage as JSONified strings", -> Settings.set 'dummy', "" @@ -39,16 +38,23 @@ context "settings", Settings.clear 'scrollStepSize' assert.equal Settings.get('scrollStepSize'), 60 +context "synced settings", + + setup -> + stub global, 'localStorage', {} + Settings.cache = global.localStorage # Point the settings cache to the new localStorage object. + Settings.postUpdateHooks = {} # Avoid running update hooks which include calls to outside of settings. + should "propagate non-default value via synced storage listener", -> Settings.set 'scrollStepSize', 20 assert.equal Settings.get('scrollStepSize'), 20 - Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } } + Settings.propagateChangesFromChromeStorage { scrollStepSize: { newValue: "40" } } assert.equal Settings.get('scrollStepSize'), 40 should "propagate default value via synced storage listener", -> Settings.set 'scrollStepSize', 20 assert.equal Settings.get('scrollStepSize'), 20 - Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } } + Settings.propagateChangesFromChromeStorage { scrollStepSize: { newValue: "60" } } assert.isFalse Settings.has 'scrollStepSize' should "propagate non-default values from synced storage", -> diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index 16f0e144..fe2fc298 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -70,14 +70,14 @@ exports.chrome = chrome.runtime.lastError = undefined key_value = {} key_value[key] = { newValue: value } - @func(key_value,'synced storage stub') if @func + @func(key_value,'sync') if @func callEmpty: (key) -> chrome.runtime.lastError = undefined if @func items = {} items[key] = {} - @func(items,'synced storage stub') + @func(items,'sync') session: MAX_SESSION_RESULTS: 25 diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index f9ed3636..67c3b333 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -3,7 +3,6 @@ extend global, require "./test_chrome_stubs.js" extend(global, require "../../lib/utils.js") Utils.getCurrentVersion = -> '1.43' extend(global, require "../../lib/settings.js") -Settings.init() context "isUrl", should "accept valid URLs", -> |
