diff options
| -rw-r--r-- | CREDITS | 1 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | background_scripts/completion.coffee | 26 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 129 | ||||
| -rw-r--r-- | background_scripts/settings.coffee | 23 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 95 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 68 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 9 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 8 | ||||
| -rw-r--r-- | content_scripts/mode_passkeys.coffee | 4 | ||||
| -rw-r--r-- | content_scripts/mode_visual_edit.coffee | 23 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 3 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 123 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 6 | ||||
| -rw-r--r-- | manifest.json | 1 | ||||
| -rw-r--r-- | pages/help_dialog.html | 1 | ||||
| -rw-r--r-- | pages/options.coffee | 10 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 56 | ||||
| -rw-r--r-- | tests/unit_tests/completion_test.coffee | 8 | ||||
| -rw-r--r-- | tests/unit_tests/settings_test.coffee | 2 |
20 files changed, 223 insertions, 375 deletions
@@ -42,5 +42,6 @@ Contributors: Werner Laurensse (github: ab3) Timo Sand <timo.j.sand@gmail.com> (github: deiga) Shiyong Chen <billbill290@gmail.com> (github: UncleBill) + Utkarsh Upadhyay <musically.ut@gmail.com) (github: musically-ut) Feel free to add real names in addition to GitHub usernames. @@ -174,7 +174,7 @@ Release Notes - Added `gU`, which goes to the root of the current URL. - Added `yt`, which duplicates the current tab. - Added `W`, which moves the current tab to a new window. -- Added marks for saving and jumping to sections of a page. `mX` to set a mark and `X` to return to it. +- Added marks for saving and jumping to sections of a page. `mX` to set a mark and `` `X`` to return to it. - Added "LinkHints.activateModeToOpenIncognito", currently an advanced, unbound command. - Disallowed repeat tab closings, since this causes trouble for many people. - Update our Chrome APIs so Vimium works on Chrome 28+. diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index 177892fb..6a1c0d30 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -204,7 +204,7 @@ class DomainCompleter domains: null filter: (queryTerms, onComplete) -> - return onComplete([]) if queryTerms.length > 1 + return onComplete([]) unless queryTerms.length == 1 if @domains @performSearch(queryTerms, onComplete) else @@ -344,11 +344,33 @@ class SearchEngineCompleter computeRelevancy: -> 1 refresh: -> - this.searchEngines = root.Settings.getSearchEngines() + @searchEngines = SearchEngineCompleter.getSearchEngines() getSearchEngineMatches: (queryTerms) -> (1 < queryTerms.length and @searchEngines[queryTerms[0]]) or {} + # Static data and methods for parsing the configured search engines. We keep a cache of the search-engine + # mapping in @searchEnginesMap. + @searchEnginesMap: null + + # Parse the custom search engines setting and cache it in SearchEngineCompleter.searchEnginesMap. + @parseSearchEngines: (searchEnginesText) -> + searchEnginesMap = SearchEngineCompleter.searchEnginesMap = {} + for line in searchEnginesText.split /\n/ + tokens = line.trim().split /\s+/ + continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#") + keywords = tokens[0].split ":" + continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ]. + searchEnginesMap[keywords[0]] = + url: tokens[1] + description: tokens[2..].join(" ") + + # Fetch the search-engine map, building it if necessary. + @getSearchEngines: -> + unless SearchEngineCompleter.searchEnginesMap? + SearchEngineCompleter.parseSearchEngines Settings.get "searchEngines" + SearchEngineCompleter.searchEnginesMap + # A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top # 10. Queries from the vomnibar frontend script come through a multi completer. class MultiCompleter diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 0c7d9343..a26fd8a8 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -26,6 +26,7 @@ validFirstKeys = {} singleKeyCommands = [] focusedFrame = null frameIdsForTab = {} +root.urlForTab = {} # Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12> # This regular expression captures two groups: the first is a named key, the second is the remainder of @@ -57,7 +58,7 @@ completers = bookmarks: new MultiCompleter([completionSources.bookmarks]) tabs: new MultiCompleter([completionSources.tabs]) -chrome.runtime.onConnect.addListener((port, name) -> +chrome.runtime.onConnect.addListener (port, name) -> senderTabId = if port.sender.tab then port.sender.tab.id else null # If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore # the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring @@ -69,14 +70,8 @@ chrome.runtime.onConnect.addListener((port, name) -> delete tabLoadedHandlers[senderTabId] toCall.call() - # domReady is the appropriate time to show the "vimium has been upgraded" message. - # TODO: This might be broken on pages with frames. - if (shouldShowUpgradeMessage()) - chrome.tabs.sendMessage(senderTabId, { name: "showUpgradeNotification", version: currentVersion }) - if (portHandlers[port.name]) port.onMessage.addListener(portHandlers[port.name]) -) chrome.runtime.onMessage.addListener((request, sender, sendResponse) -> if (sendRequestHandlers[request.handler]) @@ -183,14 +178,6 @@ openUrlInIncognito = (request) -> chrome.windows.create({ url: Utils.convertToUrl(request.url), incognito: true}) # -# Called when the user has clicked the close icon on the "Vimium has been updated" message. -# We should now dismiss that message in all tabs. -# -upgradeNotificationClosed = (request) -> - Settings.set("previousVersion", currentVersion) - sendRequestToAllTabs({ name: "hideUpgradeNotification" }) - -# # 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. # @@ -368,56 +355,23 @@ updateOpenTabs = (tab, deleteFrames = false) -> # Frames are recreated on refresh delete frameIdsForTab[tab.id] if deleteFrames -setBrowserActionIcon = (tabId,path) -> - chrome.browserAction.setIcon({ tabId: tabId, path: path }) - -chrome.browserAction.setBadgeBackgroundColor - # This is Vimium blue (from the icon). - # color: [102, 176, 226, 255] - # This is a slightly darker blue. It makes the badge more striking in the corner of the eye, and the symbol - # easier to read. - color: [82, 156, 206, 255] - -setBadge = do -> - current = null - timer = null - updateBadge = (badge, tabId) -> -> chrome.browserAction.setBadgeText text: badge, tabId: tabId - (request, sender) -> - badge = request.badge - if badge? and badge != current - current = badge - clearTimeout timer if timer - # We wait a few moments. This avoids badge flicker when there are rapid changes. - timer = setTimeout updateBadge(badge, sender.tab.id), 50 - -# Updates the browserAction icon to indicate whether Vimium is enabled or disabled on the current page. -# Also propagates new enabled/disabled/passkeys state to active window, if necessary. -# This lets you disable Vimium on a page without needing to reload. -# Exported via root because it's called from the page popup. -root.updateActiveState = updateActiveState = (tabId) -> - enabledIcon = "icons/browser_action_enabled.png" - disabledIcon = "icons/browser_action_disabled.png" - partialIcon = "icons/browser_action_partial.png" - chrome.tabs.get tabId, (tab) -> - setBadge { badge: "" }, tab: { id: tabId } - chrome.tabs.sendMessage tabId, { name: "getActiveState" }, (response) -> - if response - isCurrentlyEnabled = response.enabled - currentPasskeys = response.passKeys - config = isEnabledForUrl { url: tab.url }, { tab: tab } - enabled = config.isEnabledForUrl - passKeys = config.passKeys - if (enabled and passKeys) - setBrowserActionIcon(tabId,partialIcon) - else if (enabled) - setBrowserActionIcon(tabId,enabledIcon) - else - setBrowserActionIcon(tabId,disabledIcon) - # Propagate the new state only if it has changed. - if (isCurrentlyEnabled != enabled || currentPasskeys != passKeys) - chrome.tabs.sendMessage(tabId, { name: "setState", enabled: enabled, passKeys: passKeys, incognito: tab.incognito }) - else - setBrowserActionIcon tabId, disabledIcon +# Here's how we set the page icon. The default is "disabled", so if we do nothing else, then we get the +# grey-out disabled icon. Thereafter, we only set tab-specific icons, so there's no need to update the icon +# when we visit a tab on which Vimium isn't running. +# +# For active tabs, when a frame starts, it requests its active state via isEnabledForUrl. We also check the +# state every time a frame gets the focus. In both cases, the frame then updates the tab's icon accordingly. +# +# Exclusion rule changes (from either the options page or the page popup) propagate via the subsequent focus +# change. In particular, whenever a frame next gets the focus, it requests its new state and sets the icon +# accordingly. +# +setIcon = (request, sender) -> + path = switch request.icon + when "enabled" then "icons/browser_action_enabled.png" + when "partial" then "icons/browser_action_partial.png" + when "disabled" then "icons/browser_action_disabled.png" + chrome.browserAction.setIcon tabId: sender.tab.id, path: path handleUpdateScrollPosition = (request, sender) -> updateScrollPosition(sender.tab, request.scrollX, request.scrollY) @@ -434,7 +388,6 @@ chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) -> runAt: "document_start" chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError updateOpenTabs(tab) if changeInfo.url? - updateActiveState(tabId) chrome.tabs.onAttached.addListener (tabId, attachedInfo) -> # We should update all the tabs in the old window and the new window. @@ -469,8 +422,7 @@ chrome.tabs.onRemoved.addListener (tabId) -> tabInfoMap.deletor = -> delete tabInfoMap[tabId] setTimeout tabInfoMap.deletor, 1000 delete frameIdsForTab[tabId] - -chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId) + delete urlForTab[tabId] unless chrome.sessions chrome.windows.onRemoved.addListener (windowId) -> delete tabQueue[windowId] @@ -627,16 +579,6 @@ sendRequestToAllTabs = (args) -> for tab in window.tabs chrome.tabs.sendMessage(tab.id, args, null)) -# -# Returns true if the current extension version is greater than the previously recorded version in -# localStorage, and false otherwise. -# -shouldShowUpgradeMessage = -> - # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new - # installs. - Settings.set("previousVersion", currentVersion) unless Settings.get("previousVersion") - Utils.compareVersions(currentVersion, Settings.get("previousVersion")) == 1 - openOptionsPageInNewTab = -> chrome.tabs.getSelected(null, (tab) -> chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: tab.index + 1 })) @@ -654,6 +596,7 @@ unregisterFrame = (request, sender) -> handleFrameFocused = (request, sender) -> tabId = sender.tab.id + urlForTab[tabId] = request.url if frameIdsForTab[tabId]? frameIdsForTab[tabId] = [request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...] @@ -687,7 +630,6 @@ sendRequestHandlers = unregisterFrame: unregisterFrame frameFocused: handleFrameFocused nextFrame: (request) -> BackgroundCommands.nextFrame 1, request.frameId - upgradeNotificationClosed: upgradeNotificationClosed updateScrollPosition: handleUpdateScrollPosition copyToClipboard: copyToClipboard pasteFromClipboard: pasteFromClipboard @@ -696,7 +638,7 @@ sendRequestHandlers = refreshCompleter: refreshCompleter createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) - setBadge: setBadge + setIcon: setIcon sendMessageToFrames: sendMessageToFrames log: bgLog @@ -727,8 +669,30 @@ if Settings.has("keyMappings") populateValidFirstKeys() populateSingleKeyCommands() -if shouldShowUpgradeMessage() - sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion }) + +# Show notification on upgrade. +showUpgradeMessage = -> + # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new + # installs. + Settings.set "previousVersion", currentVersion unless Settings.get "previousVersion" + if Utils.compareVersions(currentVersion, Settings.get "previousVersion" ) == 1 + notificationId = "VimiumUpgradeNotification" + notification = + type: "basic" + iconUrl: chrome.runtime.getURL "icons/vimium.png" + title: "Vimium Upgrade" + message: "Vimium has been upgraded to version #{currentVersion}. Click here for more information." + isClickable: true + if chrome.notifications?.create? + chrome.notifications.create notificationId, notification, -> + unless chrome.runtime.lastError + Settings.set "previousVersion", currentVersion + chrome.notifications.onClicked.addListener (id) -> + if id == notificationId + 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 # Ensure that tabInfoMap is populated when Vimium is installed. chrome.windows.getAll { populate: true }, (windows) -> @@ -741,3 +705,4 @@ chrome.windows.getAll { populate: true }, (windows) -> # Start pulling changes from synchronized storage. Sync.init() +showUpgradeMessage() diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index 3528e8a9..a4d95c81 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -33,7 +33,7 @@ root.Settings = Settings = root.refreshCompletionKeysAfterMappingSave() searchEngines: (value) -> - root.Settings.parseSearchEngines value + root.SearchEngineCompleter.parseSearchEngines value exclusionRules: (value) -> root.Exclusions.postUpdateHook value @@ -42,27 +42,6 @@ root.Settings = Settings = performPostUpdateHook: (key, value) -> @postUpdateHooks[key] value if @postUpdateHooks[key] - # Here we have our functions that parse the search engines - # this is a map that we use to store our search engines for use. - searchEnginesMap: {} - - # Parse the custom search engines setting and cache it. - parseSearchEngines: (searchEnginesText) -> - @searchEnginesMap = {} - for line in searchEnginesText.split /\n/ - tokens = line.trim().split /\s+/ - continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#") - keywords = tokens[0].split ":" - continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ]. - @searchEnginesMap[keywords[0]] = - url: tokens[1] - description: tokens[2..].join(" ") - - # Fetch the search-engine map, building it if necessary. - getSearchEngines: -> - this.parseSearchEngines(@get("searchEngines") || "") if Object.keys(@searchEnginesMap).length == 0 - @searchEnginesMap - # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans # or strings defaults: diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 72fde9e1..6bc37aaf 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -8,16 +8,15 @@ # In 'filter' mode, our link hints are numbers, and the user can narrow down the range of possibilities by # typing the text of the link itself. # -# The "name" property below is a short-form name to appear in the link-hints mode name. Debugging only. The -# key appears in the mode's badge. +# The "name" property below is a short-form name to appear in the link-hints mode's name. It's for debug only. # -OPEN_IN_CURRENT_TAB = { name: "curr-tab", key: "" } -OPEN_IN_NEW_BG_TAB = { name: "bg-tab", key: "B" } -OPEN_IN_NEW_FG_TAB = { name: "fg-tab", key: "F" } -OPEN_WITH_QUEUE = { name: "queue", key: "Q" } -COPY_LINK_URL = { name: "link", key: "C" } -OPEN_INCOGNITO = { name: "incognito", key: "I" } -DOWNLOAD_LINK_URL = { name: "download", key: "D" } +OPEN_IN_CURRENT_TAB = name: "curr-tab" +OPEN_IN_NEW_BG_TAB = name: "bg-tab" +OPEN_IN_NEW_FG_TAB = name: "fg-tab" +OPEN_WITH_QUEUE = name: "queue" +COPY_LINK_URL = name: "link" +OPEN_INCOGNITO = name: "incognito" +DOWNLOAD_LINK_URL = name: "download" LinkHints = hintMarkerContainingDiv: null @@ -33,6 +32,8 @@ LinkHints = 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. @@ -55,61 +56,66 @@ LinkHints = return @isActive = true - @setOpenLinkMode(mode) - hintMarkers = (@createMarkerFor(el) for el in @getVisibleClickableElements()) + elements = @getVisibleClickableElements() + # 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 ] + hintMarkers = (@createMarkerFor(el) for el in elements) @getMarkerMatcher().fillInMarkers(hintMarkers) - # Note(philc): Append these markers as top level children instead of as child nodes to the link itself, - # because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat - # that if you scroll the page and the link has position=fixed, the marker will not stay fixed. - @hintMarkerContainingDiv = DomUtils.addElementList(hintMarkers, - { id: "vimiumHintMarkerContainer", className: "vimiumReset" }) - @hintMode = new Mode name: "hint/#{mode.name}" - badge: "#{mode.key}?" + indicator: false passInitialKeyupEvents: true - keydown: @onKeyDownInMode.bind(this, hintMarkers), - # trap all key events + keydown: @onKeyDownInMode.bind this, hintMarkers + # Trap all other key events. keypress: -> false keyup: -> false + @setOpenLinkMode mode + + # Note(philc): Append these markers as top level children instead of as child nodes to the link itself, + # because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat + # that if you scroll the page and the link has position=fixed, the marker will not stay fixed. + @hintMarkerContainingDiv = DomUtils.addElementList(hintMarkers, + { id: "vimiumHintMarkerContainer", className: "vimiumReset" }) + setOpenLinkMode: (@mode) -> if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE if @mode is OPEN_IN_NEW_BG_TAB - HUD.show("Open link in new tab") + @hintMode.setIndicator "Open link in new tab" else if @mode is OPEN_IN_NEW_FG_TAB - HUD.show("Open link in new tab and switch to it") + @hintMode.setIndicator "Open link in new tab and switch to it" else - HUD.show("Open multiple links in a new tab") + @hintMode.setIndicator "Open multiple links in a new tab" @linkActivator = (link) -> # When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on # windows) to open it in a new tab if necessary. - DomUtils.simulateClick(link, { - shiftKey: @mode is OPEN_IN_NEW_FG_TAB, - metaKey: KeyboardUtils.platform == "Mac", - ctrlKey: KeyboardUtils.platform != "Mac", - altKey: false}) + DomUtils.simulateClick link, + shiftKey: @mode is OPEN_IN_NEW_FG_TAB + metaKey: KeyboardUtils.platform == "Mac" + ctrlKey: KeyboardUtils.platform != "Mac" + altKey: false else if @mode is COPY_LINK_URL - HUD.show("Copy link URL to Clipboard") - @linkActivator = (link) -> - chrome.runtime.sendMessage({handler: "copyToClipboard", data: link.href}) + @hintMode.setIndicator "Copy link URL to Clipboard" + @linkActivator = (link) => + if link.href? + chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href + url = link.href + url = url[0..25] + "...." if 28 < url.length + @onExit = -> HUD.showForDuration "Yanked #{url}", 2000 + else + @onExit = -> HUD.showForDuration "No link to yank.", 2000 else if @mode is OPEN_INCOGNITO - HUD.show("Open link in incognito window") - + @hintMode.setIndicator "Open link in incognito window" @linkActivator = (link) -> - chrome.runtime.sendMessage( - handler: 'openUrlInIncognito' - url: link.href) + chrome.runtime.sendMessage handler: 'openUrlInIncognito', url: link.href else if @mode is DOWNLOAD_LINK_URL - HUD.show("Download link URL") + @hintMode.setIndicator "Download link URL" @linkActivator = (link) -> - DomUtils.simulateClick(link, { - altKey: true, - ctrlKey: false, - metaKey: false }) + DomUtils.simulateClick link, altKey: true, ctrlKey: false, metaKey: false else # OPEN_IN_CURRENT_TAB - HUD.show("Open link in current tab") + @hintMode.setIndicator "Open link in current tab" @linkActivator = (link) -> DomUtils.simulateClick.bind(DomUtils, link)() # @@ -276,8 +282,8 @@ LinkHints = handlerStack.push keyup: (event) => if event.keyCode == keyCode - @setOpenLinkMode previousMode if @isActive handlerStack.remove() + @setOpenLinkMode previousMode if @isActive true # TODO(philc): Ignore keys that have modifiers. @@ -347,7 +353,8 @@ LinkHints = DomUtils.removeElement LinkHints.hintMarkerContainingDiv LinkHints.hintMarkerContainingDiv = null @hintMode.exit() - HUD.hide() + @onExit?() + @onExit = null @isActive = false # we invoke the deactivate() function directly instead of using setTimeout(callback, 0) so that diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index bded402c..a2ac5b8c 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -6,13 +6,6 @@ # name: # A name for this mode. # -# badge: -# A badge (to appear on the browser popup). -# Optional. Define a badge if the badge is constant; for example, in find mode the badge is always "/". -# Otherwise, do not define a badge, but instead override the updateBadge method; for example, in passkeys -# mode, the badge may be "P" or "", depending on the configuration state. Or, if the mode *never* shows a -# badge, then do neither. -# # keydown: # keypress: # keyup: @@ -48,7 +41,6 @@ class Mode @handlers = [] @exitHandlers = [] @modeIsActive = true - @badge = @options.badge || "" @name = @options.name || "anonymous" @count = ++count @@ -59,7 +51,15 @@ class Mode keydown: @options.keydown || null keypress: @options.keypress || null keyup: @options.keyup || null - updateBadge: (badge) => @alwaysContinueBubbling => @updateBadge badge + indicator: => + # Update the mode indicator. Setting @options.indicator to a string shows a mode indicator in the + # HUD. Setting @options.indicator to 'false' forces no mode indicator. If @options.indicator is + # undefined, then the request propagates to the next mode. + # The active indicator can also be changed with @setIndicator(). + if @options.indicator? + if @options.indicator then HUD?.show @options.indicator else HUD?.hide true, false + @stopBubblingAndTrue + else @continueBubbling # If @options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed. if @options.exitOnEscape @@ -131,10 +131,17 @@ class Mode if KeyboardUtils.isPrintable event then @stopBubblingAndFalse else @stopBubblingAndTrue Mode.modes.push @ - Mode.updateBadge() + @setIndicator() @logModes() # End of Mode constructor. + setIndicator: (indicator = @options.indicator) -> + @options.indicator = indicator + Mode.setIndicator() + + @setIndicator: -> + handlerStack.bubbleEvent "indicator" + push: (handlers) -> handlers._name ||= "mode-#{@id}" @handlers.push handlerStack.push handlers @@ -152,17 +159,12 @@ class Mode handler() for handler in @exitHandlers handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ - Mode.updateBadge() @modeIsActive = false + @setIndicator() deactivateSingleton: (singleton) -> Mode.singletons?[Utils.getIdentity singleton]?.exit() - # The badge is chosen by bubbling an "updateBadge" event down the handler stack allowing each mode the - # opportunity to choose a badge. This is overridden in sub-classes. - updateBadge: (badge) -> - badge.badge ||= @badge - # Shorthand for an otherwise long name. This wraps a handler with an arbitrary return value, and always # yields @continueBubbling instead. This simplifies handlers if they always continue bubbling (a common # case), because they do not need to be concerned with the value they yield. @@ -174,14 +176,6 @@ class Mode delete @options[key] for key in [ "keydown", "keypress", "keyup" ] new @constructor @options - # Static method. Used externally and internally to initiate bubbling of an updateBadge event and to send - # the resulting badge to the background page. We only update the badge if this document (hence this frame) - # has the focus. - @updateBadge: -> - if document.hasFocus() - handlerStack.bubbleEvent "updateBadge", badge = badge: "" - chrome.runtime.sendMessage { handler: "setBadge", badge: badge.badge }, -> - # Debugging routines. logModes: -> if @debug @@ -200,31 +194,5 @@ class Mode mode.exit() for mode in @modes @modes = [] -# BadgeMode is a pseudo mode for triggering badge updates on focus changes and state updates. It sits at the -# bottom of the handler stack, and so it receives state changes *after* all other modes, and can override the -# badge choice of the other modes. -class BadgeMode extends Mode - constructor: () -> - super - name: "badge" - trackState: true - - # FIXME(smblott) BadgeMode is currently triggering an updateBadge event on every focus event. That's a - # lot, considerably more than necessary. Really, it only needs to trigger when we change frame, or when - # we change tab. - @push - _name: "mode-#{@id}/focus" - "focus": => @alwaysContinueBubbling -> Mode.updateBadge() - - updateBadge: (badge) -> - # If we're not enabled, then post an empty badge. - badge.badge = "" unless @enabled - - # When the registerStateChange event bubbles to the bottom of the stack, all modes have been notified. So - # it's now time to update the badge. - registerStateChange: -> - Mode.updateBadge() - root = exports ? window root.Mode = Mode -root.BadgeMode = BadgeMode diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 67f2a7dc..ed08fbd5 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -33,8 +33,6 @@ class PostFindMode extends SuppressPrintable super name: "post-find" - # We show a "?" badge, but only while an Escape activates insert mode. - badge: "?" # PostFindMode shares a singleton with the modes launched by focusInput; each displaces the other. singleton: element exitOnBlur: element @@ -54,14 +52,7 @@ class PostFindMode extends SuppressPrintable @suppressEvent else handlerStack.remove() - @badge = "" - Mode.updateBadge() @continueBubbling - updateBadge: (badge) -> - badge.badge ||= @badge - # Suppress the "I" badge from insert mode. - InsertMode.suppressEvent badge # Always truthy. - root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 90162d5a..7ca2e561 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -24,6 +24,7 @@ class InsertMode extends Mode defaults = name: "insert" + indicator: if @permanent then null else "Insert mode" keypress: handleKeyEvent keyup: handleKeyEvent keydown: handleKeyEvent @@ -68,18 +69,13 @@ class InsertMode extends Mode activateOnElement: (element) -> @log "#{@id}: activating (permanent)" if @debug and @permanent @insertModeLock = element - Mode.updateBadge() exit: (_, target) -> if (target and target == @insertModeLock) or @global or target == undefined @log "#{@id}: deactivating (permanent)" if @debug and @permanent and @insertModeLock @insertModeLock = null # Exit, but only if this isn't the permanently-installed instance. - if @permanent then Mode.updateBadge() else super() - - updateBadge: (badge) -> - badge.badge ||= @badge if @badge - badge.badge ||= "I" if @isActive badge + super() unless @permanent # Static stuff. This allows PostFindMode to suppress the permanently-installed InsertMode instance. @suppressedEvent: null diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 64db5447..cf74a844 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -16,9 +16,5 @@ class PassKeysMode extends Mode else @continueBubbling - # Disabled, pending experimentation with how/whether to use badges (smblott, 2015/01/17). - # updateBadge: (badge) -> - # badge.badge ||= "P" if @passKeys and not @keyQueue - root = exports ? window root.PassKeysMode = PassKeysMode diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index a5758a64..9c599959 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -361,30 +361,30 @@ class Movement extends CountPrefix @movements.n = (count) -> executeFind count, false @movements.N = (count) -> executeFind count, true @movements["/"] = -> - @findMode = window.enterFindMode() + @findMode = window.enterFindMode returnToViewport: true @findMode.onExit => @changeMode VisualMode # # End of Movement constructor. - # Yank the selection; always exits; either deletes the selection or removes it; set @yankedText and return + # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and return # it. yank: (args = {}) -> @yankedText = @selection.toString() @selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument - @selection.removeAllRanges() unless @options.parentMode + @selection.collapseToStart() unless @options.parentMode message = @yankedText.replace /\s+/g, " " message = message[...12] + "..." if 15 < @yankedText.length plural = if @yankedText.length == 1 then "" else "s" - HUD.showForDuration "Yanked #{@yankedText.length} character#{plural}: \"#{message}\".", 2500 @options.onYank?.call @, @yankedText @exit() + HUD.showForDuration "Yanked #{@yankedText.length} character#{plural}: \"#{message}\".", 2500 @yankedText exit: (event, target) -> unless @options.parentMode or @options.oneMovementOnly - @selection.removeAllRanges() if event?.type == "keydown" and KeyboardUtils.isEscape event + @selection.collapseToStart() if event?.type == "keydown" and KeyboardUtils.isEscape event # Disabled, pending discussion of fine-tuning the UX. Simpler alternative is implemented above. # # If we're exiting on escape and there is a range selection, then we leave it in place. However, an @@ -466,7 +466,7 @@ class VisualMode extends Movement defaults = name: "visual" - badge: "V" + indicator: if options.indicator? then options.indicator else "Visual mode" singleton: VisualMode exitOnEscape: true super extend defaults, options @@ -489,8 +489,8 @@ class VisualMode extends Movement @selection.removeAllRanges() if @selection.type != "Range" - HUD.showForDuration "No usable selection, entering caret mode...", 2500 @changeMode CaretMode + HUD.showForDuration "No usable selection, entering caret mode...", 2500 return @push @@ -567,7 +567,7 @@ class VisualMode extends Movement class VisualLineMode extends VisualMode constructor: (options = {}) -> - super extend { name: "visual/line" }, options + super extend { name: "visual/line", indicator: "Visual mode (line)" }, options @extendSelection() @commands.v = -> @changeMode VisualMode @@ -587,7 +587,7 @@ class CaretMode extends Movement defaults = name: "caret" - badge: "C" + indicator: "Caret mode" singleton: VisualMode exitOnEscape: true super extend defaults, options @@ -597,8 +597,8 @@ class CaretMode extends Movement when "None" @establishInitialSelectionAnchor() if @selection.type == "None" - HUD.showForDuration "Create a selection before entering visual mode.", 2500 @exit() + HUD.showForDuration "Create a selection before entering visual mode.", 2500 return when "Range" @collapseSelectionToAnchor() @@ -652,7 +652,7 @@ class EditMode extends Movement defaults = name: "edit" - badge: "E" + indicator: "Edit mode" exitOnEscape: true exitOnBlur: @element super extend defaults, options @@ -748,7 +748,6 @@ class EditMode extends Movement # and (possibly) deletes it. enterVisualModeForMovement: (count, options = {}) -> @launchSubMode VisualMode, extend options, - badge: "M" initialCountPrefix: count oneMovementOnly: true diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 5cc3fd82..b7de5140 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -123,6 +123,9 @@ CoreScroller = @lastEvent = null @keyIsDown = false + # NOTE(smblott) With extreme keyboard configurations, Chrome sometimes does not get a keyup event for + # every keydown, in which case tapping "j" scrolls indefinitely. This appears to be a Chrome/OS/XOrg bug + # of some kind. See #1549. handlerStack.push _name: 'scroller/track-key-status' keydown: (event) => diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 931b8edf..b5a5f51e 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -141,13 +141,13 @@ window.initializeModes = -> constructor: -> super name: "normal" + indicator: false # There is no mode indicator in normal mode. keydown: (event) => onKeydown.call @, event keypress: (event) => onKeypress.call @, event keyup: (event) => onKeyup.call @, event # Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and # activates/deactivates itself accordingly. - new BadgeMode new NormalMode new PassKeysMode new InsertMode permanent: true @@ -174,10 +174,9 @@ initializePreDomReady = -> isEnabledForUrl = false chrome.runtime.sendMessage = -> chrome.runtime.connect = -> + window.removeEventListener "focus", onFocus requestHandlers = - hideUpgradeNotification: -> HUD.hideUpgradeNotification() - showUpgradeNotification: (request) -> HUD.showUpgradeNotification(request.version) showHUDforDuration: (request) -> HUD.showForDuration request.text, request.duration toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId) focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame request @@ -185,8 +184,6 @@ initializePreDomReady = -> getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY executePageCommand: executePageCommand - getActiveState: getActiveState - setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue } @@ -197,7 +194,7 @@ initializePreDomReady = -> # from the former. return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://' # We handle the message if we're enabled, or if it's one of these listed message types. - return unless isEnabledForUrl or request.name in [ "getActiveState", "setState", "executePageCommand" ] + return unless isEnabledForUrl or request.name in [ "executePageCommand" ] # These requests are delivered to the options page, but there are no handlers there. return if request.handler in [ "registerFrame", "unregisterFrame" ] # We don't handle these here. They're handled elsewhere (e.g. in the vomnibar/UI component). @@ -226,34 +223,25 @@ window.initializeWhenEnabled = -> for type in [ "keydown", "keypress", "keyup", "click", "focus", "blur", "mousedown" ] do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event installListener document, "DOMActivate", (event) -> handlerStack.bubbleEvent 'DOMActivate', event - installListener window, "focus", registerFocus installedListeners = true FindModeHistory.init() -setState = (request) -> - isEnabledForUrl = request.enabled - passKeys = request.passKeys - isIncognitoMode = request.incognito - initializeWhenEnabled() if isEnabledForUrl - handlerStack.bubbleEvent "registerStateChange", - enabled: isEnabledForUrl - passKeys: passKeys - -getActiveState = -> - Mode.updateBadge() - return { enabled: isEnabledForUrl, passKeys: passKeys } - # -# The backend needs to know which frame has focus. +# 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. # -registerFocus = (event) -> +onFocus = (event) -> if event.target == window - # Settings may have changed since the frame last had focus. settings.load() + chrome.runtime.sendMessage handler: "frameFocused", frameId: frameId, url: window.location.toString() checkIfEnabledForUrl() - # Don't register frameset containers; focusing them is no use. - unless document.body?.tagName.toLowerCase() == "frameset" - chrome.runtime.sendMessage handler: "frameFocused", frameId: frameId + +# We install these listeners directly (that is, we don't use installListener) because we still need to receive +# events when Vimium is not enabled. +window.addEventListener "focus", onFocus +window.addEventListener "hashchange", onFocus # # Initialization tasks that must wait for the document to be ready. @@ -434,7 +422,6 @@ extend window, constructor: -> super name: "focus-selector" - badge: "?" exitOnClick: true keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab @@ -600,12 +587,21 @@ checkIfEnabledForUrl = -> isIncognitoMode = response.incognito if isEnabledForUrl initializeWhenEnabled() - else if (HUD.isReady()) + else if HUD.isReady() # Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load. HUD.hide() handlerStack.bubbleEvent "registerStateChange", enabled: isEnabledForUrl passKeys: passKeys + # Update the page icon, if necessary. + if document.hasFocus() + chrome.runtime.sendMessage + handler: "setIcon" + icon: + if isEnabledForUrl and not passKeys then "enabled" + else if isEnabledForUrl then "partial" + else "disabled" + # Exported to window, but only for DOM tests. window.refreshCompletionKeys = (response) -> @@ -727,7 +723,6 @@ handleKeyCharForFindMode = (keyChar) -> updateQueryForFindMode findModeQuery.rawQuery + keyChar handleEscapeForFindMode = -> - exitFindMode() 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. @@ -741,8 +736,7 @@ handleEscapeForFindMode = -> # Return true if character deleted, false otherwise. handleDeleteForFindMode = -> if findModeQuery.rawQuery.length == 0 - exitFindMode() - performFindInPlace() + HUD.hide() false else updateQueryForFindMode findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1) @@ -752,22 +746,25 @@ handleDeleteForFindMode = -> # <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 = -> - exitFindMode() focusFoundLink() document.body.classList.add("vimiumFindMode") FindModeHistory.saveQuery findModeQuery.rawQuery class FindMode extends Mode - constructor: -> + constructor: (options = {}) -> @historyIndex = -1 @partialQuery = "" + if options.returnToViewport + @scrollX = window.scrollX + @scrollY = window.scrollY super name: "find" - badge: "/" + 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 @@ -790,8 +787,8 @@ class FindMode extends Mode DomUtils.suppressPropagation(event) handlerStack.stopBubblingAndFalse - keypress: (event) -> - handlerStack.neverContinueBubbling -> + keypress: (event) => + handlerStack.neverContinueBubbling => if event.keyCode > 31 keyChar = String.fromCharCode event.charCode handleKeyCharForFindMode keyChar if keyChar @@ -1024,15 +1021,13 @@ findModeRestoreSelection = (range = findModeInitialRange) -> selection.addRange range # Enters find mode. Returns the new find-mode instance. -window.enterFindMode = -> +window.enterFindMode = (options = {}) -> # Save the selection, so performFindInPlace can restore it. findModeSaveSelection() - findModeQuery = { rawQuery: "" } - HUD.show("/") - new FindMode() - -exitFindMode = -> - HUD.hide() + findModeQuery = rawQuery: "" + findMode = new FindMode options + HUD.show "/" + findMode window.showHelpDialog = (html, fid) -> return if (isShowingHelpDialog || !document.body || fid != frameId) @@ -1102,7 +1097,6 @@ toggleHelpDialog = (html, fid) -> HUD = _tweenId: -1 _displayElement: null - _upgradeNotificationElement: 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 @@ -1120,26 +1114,6 @@ HUD = HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150) HUD.displayElement().style.display = "" - showUpgradeNotification: (version) -> - HUD.upgradeNotificationElement().innerHTML = "Vimium has been upgraded to #{version}. See - <a class='vimiumReset' target='_blank' - href='https://github.com/philc/vimium#release-notes'> - what's new</a>.<a class='vimiumReset close-button' href='#'>×</a>" - links = HUD.upgradeNotificationElement().getElementsByTagName("a") - links[0].addEventListener("click", HUD.onUpdateLinkClicked, false) - links[1].addEventListener "click", (event) -> - event.preventDefault() - HUD.onUpdateLinkClicked() - Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150) - - onUpdateLinkClicked: (event) -> - HUD.hideUpgradeNotification() - chrome.runtime.sendMessage({ handler: "upgradeNotificationClosed" }) - - hideUpgradeNotification: (clickEvent) -> - Tween.fade(HUD.upgradeNotificationElement(), 0, 150, - -> HUD.upgradeNotificationElement().style.display = "none") - # # Retrieves the HUD HTML element. # @@ -1150,26 +1124,23 @@ HUD = HUD._displayElement.style.right = "150px" HUD._displayElement - upgradeNotificationElement: -> - if (!HUD._upgradeNotificationElement) - HUD._upgradeNotificationElement = HUD.createHudElement() - # Position this just to the left of our normal HUD. - HUD._upgradeNotificationElement.style.right = "315px" - HUD._upgradeNotificationElement - createHudElement: -> element = document.createElement("div") element.className = "vimiumReset vimiumHUD" document.body.appendChild(element) element - hide: (immediate) -> + # 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 + # mode indicator, is when hide() is called for the mode indicator itself. + hide: (immediate = false, updateIndicator = true) -> clearInterval(HUD._tweenId) - if (immediate) - HUD.displayElement().style.display = "none" + if immediate + HUD.displayElement().style.display = "none" unless updateIndicator + Mode.setIndicator() if updateIndicator else - HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150, - -> HUD.displayElement().style.display = "none") + HUD._tweenId = Tween.fade HUD.displayElement(), 0, 150, -> HUD.hide true, updateIndicator isReady: -> document.body != null diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee index b0fefc7d..b09d3183 100644 --- a/lib/handler_stack.coffee +++ b/lib/handler_stack.coffee @@ -84,10 +84,8 @@ class HandlerStack # Debugging. logResult: (eventNumber, type, event, handler, result) -> - # FIXME(smblott). Badge updating is too noisy, so we filter it out. However, we do need to look at how - # many badge update events are happening. It seems to be more than necessary. We also filter out - # registerKeyQueue as unnecessarily noisy and not particularly helpful. - return if type in [ "updateBadge", "registerKeyQueue" ] + # Key queue events aren't usually useful for debugging, so we filter them out. + return if type in [ "registerKeyQueue" ] label = switch result when @stopBubblingAndTrue then "stop/true" diff --git a/manifest.json b/manifest.json index beb68530..4ba222fb 100644 --- a/manifest.json +++ b/manifest.json @@ -27,6 +27,7 @@ "clipboardRead", "storage", "sessions", + "notifications", "<all_urls>" ], "content_scripts": [ diff --git a/pages/help_dialog.html b/pages/help_dialog.html index 0884f2cd..77c3e2bf 100644 --- a/pages/help_dialog.html +++ b/pages/help_dialog.html @@ -46,6 +46,7 @@ </div> <div class="vimiumReset vimiumColumn" style="text-align:right"> <span class="vimiumReset">Version {{version}}</span><br/> + <a href="https://github.com/philc/vimium#release-notes" class="vimiumReset">What's new?</a> </div> </div> </div> diff --git a/pages/options.coffee b/pages/options.coffee index d2950348..6545189b 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -271,8 +271,12 @@ initPopupPage = -> exclusions = null document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html") + # As the active URL, we choose the most recently registered URL from a frame in the tab, or the tab's own + # URL. + url = chrome.extension.getBackgroundPage().urlForTab[tab.id] || tab.url + updateState = -> - rule = bgExclusions.getRule tab.url, exclusions.readValueFromElement() + rule = bgExclusions.getRule url, exclusions.readValueFromElement() $("state").innerHTML = "Vimium will " + if rule and rule.passKeys "exclude <span class='code'>#{rule.passKeys}</span>" @@ -291,8 +295,6 @@ initPopupPage = -> Option.saveOptions() $("saveOptions").innerHTML = "Saved" $("saveOptions").disabled = true - chrome.tabs.query { windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, (tabs) -> - chrome.extension.getBackgroundPage().updateActiveState(tabs[0].id) $("saveOptions").addEventListener "click", saveOptions @@ -302,7 +304,7 @@ initPopupPage = -> window.close() # Populate options. Just one, here. - exclusions = new ExclusionRulesOnPopupOption(tab.url, "exclusionRules", onUpdated) + exclusions = new ExclusionRulesOnPopupOption url, "exclusionRules", onUpdated updateState() document.addEventListener "keyup", updateState diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee index 4afa9d7d..f81982ac 100644 --- a/tests/dom_tests/dom_tests.coffee +++ b/tests/dom_tests/dom_tests.coffee @@ -495,10 +495,6 @@ context "PostFindMode", testContent = "<input type='text' id='first'/>" document.getElementById("test-div").innerHTML = testContent document.getElementById("first").focus() - # For these tests, we need to push GrabBackFocus out of the way. When it exits, it updates the badge, - # which interferes with event suppression within insert mode. This cannot happen in normal operation, - # because GrabBackFocus exits on the first keydown. - Mode.top().exit() @postFindMode = new PostFindMode tearDown -> @@ -527,55 +523,3 @@ context "PostFindMode", sendKeyboardEvent "escape" assert.isTrue @postFindMode.modeIsActive -context "Mode badges", - setup -> - initializeModeState() - testContent = "<input type='text' id='first'/>" - document.getElementById("test-div").innerHTML = testContent - - tearDown -> - document.getElementById("test-div").innerHTML = "" - - should "have no badge in normal mode", -> - Mode.updateBadge() - assert.isTrue chromeMessages[0].badge == "" - - should "have an I badge in insert mode by focus", -> - document.getElementById("first").focus() - # Focus triggers an event in the handler stack, so we check element "1", here. - assert.isTrue chromeMessages[1].badge == "I" - - should "have no badge after leaving insert mode by focus", -> - document.getElementById("first").focus() - document.getElementById("first").blur() - assert.isTrue chromeMessages[0].badge == "" - - should "have an I badge in global insert mode", -> - new InsertMode global: true - assert.isTrue chromeMessages[0].badge == "I" - - should "have no badge after leaving global insert mode", -> - mode = new InsertMode global: true - mode.exit() - assert.isTrue chromeMessages[0].badge == "" - - should "have a ? badge in PostFindMode (immediately)", -> - document.getElementById("first").focus() - new PostFindMode - assert.isTrue chromeMessages[0].badge == "?" - - should "have no badge in PostFindMode (subsequently)", -> - document.getElementById("first").focus() - new PostFindMode - sendKeyboardEvent "a" - assert.isTrue chromeMessages[0].badge == "" - - should "have no badge when disabled", -> - handlerStack.bubbleEvent "registerStateChange", - enabled: false - passKeys: "" - - document.getElementById("first").focus() - # Focus triggers an event in the handler stack, so we check element "1", here. - assert.isTrue chromeMessages[1].badge == "" - diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee index b7b73cc2..56fcc456 100644 --- a/tests/unit_tests/completion_test.coffee +++ b/tests/unit_tests/completion_test.coffee @@ -152,8 +152,9 @@ context "domain completer", setup -> @history1 = { title: "history1", url: "http://history1.com", lastVisitTime: hours(1) } @history2 = { title: "history2", url: "http://history2.com", lastVisitTime: hours(1) } + @undef = { title: "history2", url: "http://undefined.net", lastVisitTime: hours(1) } - stub(HistoryCache, "use", (onComplete) => onComplete([@history1, @history2])) + stub(HistoryCache, "use", (onComplete) => onComplete([@history1, @history2, @undef])) global.chrome.history = onVisited: { addListener: -> } onVisitRemoved: { addListener: -> } @@ -174,6 +175,9 @@ context "domain completer", should "returns no results when there's more than one query term, because clearly it's not a domain", -> assert.arrayEqual [], filterCompleter(@completer, ["his", "tory"]) + should "not return any results for empty queries", -> + assert.arrayEqual [], filterCompleter(@completer, []) + context "domain completer (removing entries)", setup -> @history1 = { title: "history1", url: "http://history1.com", lastVisitTime: hours(2) } @@ -238,7 +242,7 @@ context "search engines", @completer = new SearchEngineCompleter() # note, I couldn't just call @completer.refresh() here as I couldn't set root.Settings without errors # workaround is below, would be good for someone that understands the testing system better than me to improve - @completer.searchEngines = Settings.getSearchEngines() + @completer.searchEngines = SearchEngineCompleter.getSearchEngines() should "return search engine suggestion without description", -> results = filterCompleter(@completer, ["foo", "hello"]) diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index afe862a4..346c98da 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -73,7 +73,7 @@ context "settings", should "set search engines, retrieve them correctly and check that they have been parsed correctly", -> searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description" Settings.set 'searchEngines', searchEngines - result = Settings.getSearchEngines() + result = SearchEngineCompleter.getSearchEngines() assert.equal Object.keys(result).length, 2 assert.equal "bar?q=%s", result["foo"].url assert.isFalse result["foo"].description |
